4

I am new to sails.js and I am using "sails.js with Mongodb". I am having problem with deep associations using populate in my sails app.

I have a relationship like this:

 Category has many to many relationship with Article.

 City has one to many relationship with Areas.

 Article has one to one relationship with City and Areas.

Category.js

module.exports = {

schema: true,

attributes: {

    //add referecnce to other article
    Articles: {
            collection: 'Article', 
            via:'ref_category_id' 
      },

    category_name: { 
        type:'string',  
        required: true, 
        unique: true },
  }
};

Article.js

module.exports = {

schema: true,

attributes: {

       //adding reference to category
        ref_category_id: {
                    collection:'Category',
                    via:'Articles'
         },

    //adding reference to city
     ref_city_id: {
                    model:'City'
       },

    //add a reference to area
         ref_area_id: {
                  model:'Areas'
        },

      //adding reference to tags
      tags: {
           collection: 'Tag', 
           via:'articles'
       },

      title:{ type:'string',required:true},

      blurb: { type: 'string'},

      description:{ type:'string'}
    }
 }; 

City.js

module.exports = {

        schema: true,

        attributes: {


        Areas: {
                collection: 'Areas', 
                via:'ref_city_id'
        },

         ref_article_id:{
                    model:'Article'
         },


        city_name: { type:'string',
                   required:true,
                   unique:true }
    }
 };

Areas.js

 module.exports = {

   schema: true, 

   attributes: {

        area_name: { type:'string',required:true},

        latitude: { type:'float'},

        longitude: { type:'float'},

        ref_city_id: {
                model: 'City' 
        },

        ref_article_id:{
             model:'Article'
         }

     }
 };

Tag.js

   module.exports = {

      schema:false,

      attributes: {

         //adding reference to article 
          articles: {
                  collection: 'Article', 
                  via:'tags'
          },

      tag_name:{type:'string',
                required:true,
                unique:true
         }
     }
  };

CategoryController.js

  searchCategory: function(req, res, next) {

    var category_name = req.param('category_name');

    Category.find({category_name:{'like': '%'+category_name+'%'}}).populate('Articles').exec(function(err, category) 
    {

        if(err)
        {
         return res.json({'status':486,'status_message':'Server Error'});
        }
        else
        { 
            if(category.length > 0)
            {
                 var c = parseInt(category[0].Articles.length,10);

                console.log(c);
                var i = parseInt('0',10);

                for (i=0; i<c; i++)
                {
                    console.log('i value in loop = ' + i);
                    Article.find({id:category[0].Article[i].id}).populateAll().exec(function(err,article_info) {
                        if(err)
                        {
                            return res.send(err);
                        }
                        else
                        {
                            console.log(article_info);
                            console.log('-------------------------------------------');

                            res.json(article_info); 
                            console.log(' I value = ' + i); 
                        }
                    }); 

                } 
                //console.log(category);
                //return res.json({'status':479,'status_message':'Success..!!','category_info':category});
            }
            else
            {
                return res.json({'status':489,'status_message':'failure..!! No categories found..!!'});
            }
        }
    });
}

POSTMAN Request:

 http://localhost:1337/category/searchCategory

 {"category_name":"travel"}

This is my json response: Here I am getting only article which is mapped with category. but i wanted to display the values of city, areas and tags which are mapped to article.

  {
   "status": 479,
   "status_message": "Success..!!",
   "category_info": [
     {
        "Articles": [
            {
                "ref_city_id": "55a766d0a29811e875cb96a1",
                "ref_area_id": "55a78b69578393e0049dec43",
                "title": "title",
                "blurb": "blurb",
                "description": "Description",
                "createdAt": "2015-07-16T12:36:36.778Z",
                "updatedAt": "2015-07-16T12:48:20.609Z",
                "id": "55a7a55439ace79e0512269d"
            },
        ],
        "category_name": "Cooking ",
        "id": "55a6b26aee9b41de747547bb",
        "createdAt": "2015-07-15T19:20:10.670Z",
        "updatedAt": "2015-07-15T19:20:10.670Z"
    }
  ]
}

How to do the deep nested associations using populate? or Is any other way to achieve this?

Please Can anyone help me to achieve this.

Thanks in advance.

Anil Kumar
  • 187
  • 1
  • 9

2 Answers2

3

It is not currently supported, but is seems that the waterline team is working on it.

If you read the comments on this issue, you will see that there is an opened pull request based on this gist.


You could also do it manually, but you want to retrieve many information and the asynchronous code could quickly be very messy.

There are many tools to help you to keep the code readable:

Use the ones you feel more confortable with. There is an example using promises and lodash in this answer. I will not assume which tools you want to use, but you can update the question if you are still blocked later.

Category has only one relation while Article has many. I think in your situation, you should load the the categories without using populate(). Then you could loop threw the articles ids of each category, load each article with populate() (or populateAll()) and override category.Articles with the result.


EDIT. To avoid the Can\'t set headers after they are sent. error, you can use a counter to be sure to send the response only when all asynchronous functions have been executed. You are currently sending the response twice.

var articles = [];
var cpt = 0;    
for (i=0; i<c; i++) {
    Article.find({id:category[0].Article[i].id}).populate('ref_category_id').populate('tags').populate('ref_city_id').populate('ref_area_id').exec(function(err,article_info) {
        // Simplified callback function
        cpt++;  // Increment every time the callback function is executed
        articles.push(article_info);
        if (cpt === c) {  // We should have "c" executions of the callback function before sending the result in the response
            // This is working if `category` is an instance of `Category`,
            // but in your controller, `category` contains an array of `Category`.
            // Do you want to return a single Category or should you rename `category` into `categories` ?
            category.Articles = articles;
            res.json(category);
        }
    });
}

Once you understand how works the asynchronous execution flow, you should improve the code with the tools I mentioned above.

Community
  • 1
  • 1
Alexis N-o
  • 3,954
  • 26
  • 34
  • Thanks Alexis.. manually how can i achieve this.. can you show me with piece of code. it helps me a lot. – Anil Kumar Jul 23 '15 at 05:40
  • There are many options and I don't have a lot of time right now, but I tried to give you more information about how to archive this. – Alexis N-o Jul 23 '15 at 14:26
  • i renamed Article attribute from Category as Articles – Anil Kumar Jul 23 '15 at 14:46
  • alexis. i added some extra piece of code to category controller action. everything is fine in console. i mean i am getting the expected results in console but when i checked in postman, one article is missing, i.e there are totally 2 articles, in console i am getting 2 articles but whereas in postman i am getting only one article. it is not showing second article. can you please check the searchCategory action in category controller. – Anil Kumar Jul 23 '15 at 14:55
  • even i am getting this error `http.js:690 throw new Error('Can\'t set headers after they are sent.'); ^ Error: Can't set headers after they are sent. ` in the console. – Anil Kumar Jul 23 '15 at 15:08
  • I updated the response and deleted useless comment now – Alexis N-o Jul 23 '15 at 16:33
2

Yes, there is another way to do this.

Make sure you require the following npm module.

var nestedPop = require('nested-pop');

 

searchCategory: function(req, res, next) {
  var category_name = req.param('category_name');
  Category.find({category_name:{'like': '%'+category_name+'%'}})
  .populate('Articles')
  .exec(function(err, category) {
    if(err) return res.json({'status':486,'status_message':'Server Error'});
    if(category.length > 0) {
      var c = parseInt(category[0].Articles.length,10);
      console.log(c);
      var i = parseInt('0',10);
      for (var i = 0; i < c; i++) {
        console.log('i value in loop = ' + i);
        Article.find({id:category[0].Article[i].id})
        .populateAll()
        .exec(function(err, article_info) {
          if(err) return res.send(err);
          return nestedPop(article_info, {
            ref_city_id: {
              as: 'City',
              populate: [
                'things',
                'you',
                'want',
                'to',
                'populate',
                'for',
                'city'
              ],
            },
            ref_area_id: {
              as: 'Area', // or Areas (whatever the model name is)
              populate: [
                'things',
                'you',
                'want',
                'to',
                'populate',
                'for',
                'area'
              ]
            }
          }).then(function(article_info) {
            console.log(article_info);
            console.log('------------------------------------------');
            res.json(article_info); 
            console.log(' I value = ' + i);
          });
        }); 
      }
    } else {
      return res.json({'status':489,'status_message':'failure..!! No categories found..!!'});
    }
  });
}

Go to https://www.npmjs.com/package/nested-pop for more documentation.

Clay Risser
  • 3,272
  • 1
  • 25
  • 28