2
votes

I love generators in nodejs. They help nodejs look more like server-side code. I'm trying to use generators with my Sails app. This is my controller and it works when I visit 'get /hi':

/**
 * FooController
 *
 * @description :: Server-side logic for managing foos
 * @help        :: See http://sailsjs.org/#!/documentation/concepts/Controllers
 */

module.exports = {
  hi: function (req, res){
    return res.send("Hi there!");
  }
};

However when I change that hi action to a generator function...

module.exports = {
  hi: function* (req, res){
    return res.send("Hi there!");
  }
};

this action never gets to return the response. Like it's waiting for something. How does one utilize ES6 generators within SailsJS controllers and in-fact all of Sails?

4
Why would you want to do this? Now you've got a function that returns a generator (which is not magic!) instead of a function that calls res.send - of course it behaves differently. - Bergi
It can be done, with a little variation of what you've tried, check out my answer below :) - Lu Roman

4 Answers

3
votes

You can use it, in fact it'll be great if we all use this style, it adds a lot of readability and flexibility in your code (no more callback-hell), but that is not the way to use it.

As @Cohars stated, sails expect a function as controller actions, you can't pass a generator like in Koa, but that does not prevent you from using them, the thing is that a generator by itself is very useless, you need a function that calls it and iterates it and i believe koa does this for you at framework level, but you have some options available to you at library level, like co or yortus/asyncawait/ which is what i use because node-fibers implemented as functions are way more flexible than es6 generators (though they are almost the same) and i'll show you why in a sec.

So here is how you use it:

First npm install co and require it in your controller, or add it as a global in config/bootstrap.js since you will be using it a lot. Once you are done with it, you can use it in your controllers.

module.exports = {
  hi: function(req, res){
    co(function* (){
      // And you can use it with your models calls like this
      let user = yield User.findOne(req.param('id'))
      return res.send("Hi there" + user.name + "!");
    })
  }
};

That's it

Now if you rather use async/await, its pretty similar, it goes like this:

module.exports = {
  hi: function(req, res){
    async(function(){
      // Since await is a function, you can access properties
      // of the returned values like this which is nice to
      // access object properties or array indices
      let userName = await(User.findOne(req.param('id'))).name
      return res.send("Hi there" + userName + "!");
    })();
  }
};

There is a caveat though, if you call other methods of you controller via this, remember that they will refer to the generator fn or the passed regular fn if you use async/await, so be sure to save its context, or since you are already using es6 syntax, you can use fat arrows with async/await, unfortunately not with co, since there is not fat arrow version of generators (..yet?)

So it'll be like this:

module.exports = {
  hi: function(req, res){
    async(() => {
      let params = this._parseParams(req);
      let userName = await(User.findOne(params.id)).name
      return res.send("Hi there" + userName + "!");
    })();
  },
  _parseParams: function(req){
    let params = req.allParams()
    // Do some parsing here...
    return params
  }

};

I've been using the second method for months now, and works perfectly, i've tried with co too and works as well, i just liked more the async/await module (and is supposed to be a little bit faster) and its a perfect match if you are using coffeescript since your sintax will be like do async => await User.find()

UPDATE:

I've created a wrapper that you can use if you use yortus async/await or check the source code and modify it to work with co or something else if you wish.

Here is the link https://www.npmjs.com/package/async-handler, is in alpha but i'm using on my own apps and works as expected, if not submit an issue.

With that the las example would look like this:

module.exports = {
    hi: asyncHandler((req, res)->{
      let params = this._parseParams(req);
      let userName = await(User.findOne(params.id)).name
      return res.send("Hi there" + userName + "!");
    }),
    _parseParams: function(req){
      let params = req.allParams()
      // Do some parsing here...
      return params
    }
};

With this handler you get the extra benefit of having async/promise errors to propagate correctly on sails if they are not caught by a try/catch

0
votes

Sails expects a regular function here, not a generator. Maybe you could take a look at co, not sure it would really help with Sails though. If you really want to use generators, you should probably try Koa, which has several frameworks based on it

0
votes

The way I am doing it is like this:

const Promise = require('bluebird');

module.exports = {
  hi: Promise.coroutine(function* (req, res) {
    let id = req.params('id'),
        userName;
    try {
      userName = yield User.findOne(id).name;
    }
    catch (e) {
      sails.log.error(e);
      return res.serverError(e.message);
    }

    return res.ok(`Hi there ${userName}!`);
  })
}

Works great. You just need to ensure any functions you call from your controllers return promises.

0
votes

Put this code in your bootstrap.js, and every thing work like a charm!

var _ = require('lodash');
var coExpress = require('co-express');

sails.modules.loadControllers(function (err, modules) {
  if (err) {
    return callback(err);
  }
  sails.controllers = _.merge(sails.controllers, modules);

  // hacking every action of all controllers
  _.each(sails.controllers, function(controller, controllerId) {
    _.each(controller, function(action, actionId) {
      actionId = actionId.toLowerCase();
      console.log('hacking route:', controllerId, actionId);
      // co.wrap,generator => callback
      action = coExpress(action);
      sails.hooks.controllers.middleware[controllerId][actionId] = action;
    });
  });
  // reload routes
  sails.router.load(function () {
    // reload blueprints
    sails.hooks.blueprints.initialize(function () {
      sails.hooks.blueprints.extendControllerMiddleware();
      sails.hooks.blueprints.bindShadowRoutes();
      callback();
    });
  });
});