Sails.js populate nested associations Sails.js populate nested associations node.js node.js

Sails.js populate nested associations


Or you can use the built-in Blue Bird Promise feature to make it. (Working on Sails@v0.10.5)

See the codes below:

var _ = require('lodash');...Post  .findOne(req.param('id'))  .populate('user')  .populate('comments')  .then(function(post) {    var commentUsers = User.find({        id: _.pluck(post.comments, 'user')          //_.pluck: Retrieves the value of a 'user' property from all elements in the post.comments collection.      })      .then(function(commentUsers) {        return commentUsers;      });    return [post, commentUsers];  })  .spread(function(post, commentUsers) {    commentUsers = _.indexBy(commentUsers, 'id');    //_.indexBy: Creates an object composed of keys generated from the results of running each element of the collection through the given callback. The corresponding value of each key is the last element responsible for generating the key    post.comments = _.map(post.comments, function(comment) {      comment.user = commentUsers[comment.user];      return comment;    });    res.json(post);  })  .catch(function(err) {    return res.serverError(err);  });

Some explanation:

  1. I'm using the Lo-Dash to deal with the arrays. For more details, please refer to the Official Doc
  2. Notice the return values inside the first "then" function, those objects "[post, commentUsers]" inside the array are also "promise" objects. Which means that they didn't contain the value data when they first been executed, until they got the value. So that "spread" function will wait the acture value come and continue doing the rest stuffs.


At the moment, there's no built in way to populate nested associations. Your best bet is to use async to do a mapping:

async.auto({    // First get the post      post: function(cb) {        Post           .findOne(req.param('id'))           .populate('user')           .populate('comments')           .exec(cb);    },    // Then all of the comment users, using an "in" query by    // setting "id" criteria to an array of user IDs    commentUsers: ['post', function(cb, results) {        User.find({id: _.pluck(results.post.comments, 'user')}).exec(cb);    }],    // Map the comment users to their comments    map: ['commentUsers', function(cb, results) {        // Index comment users by ID        var commentUsers = _.indexBy(results.commentUsers, 'id');        // Get a plain object version of post & comments        var post = results.post.toObject();        // Map users onto comments        post.comments = post.comments.map(function(comment) {            comment.user = commentUsers[comment.user];            return comment;        });        return cb(null, post);    }]},    // After all the async magic is finished, return the mapped result   // (or an error if any occurred during the async block)   function finish(err, results) {       if (err) {return res.serverError(err);}       return res.json(results.map);   });

It's not as pretty as nested population (which is in the works, but probably not for v0.10), but on the bright side it's actually fairly efficient.


I created an NPM module for this called nested-pop. You can find it at the link below.

https://www.npmjs.com/package/nested-pop

Use it in the following way.

var nestedPop = require('nested-pop');User.find().populate('dogs').then(function(users) {    return nestedPop(users, {        dogs: [            'breed'        ]    }).then(function(users) {        return users    }).catch(function(err) {        throw err;    });}).catch(function(err) {    throw err;);