TonyW
TonyW

Reputation: 18875

Sails.js: one-to-many relationship for a model itself

I am trying to use Sails.js and MongoDB to build a simple app that supports multi-level categories. A category can have many sub categories. So in my Category model, I set up the one-to-many relationship within itself Category:

module.exports = {

  adapter: 'mongo',

  attributes: {
    categoryTitle: {
      type: 'string',
      required: true
    },

//owner or parent of the one-to-many relationship
parentCat: {
  model: 'category'
},

//sub categories
subCategories: {
  collection: 'category',
  via: 'parentCat'
},
  }};

The categories are display in a select dropdown list using the ng-repeat directive in AngularJS, e.g.

<select class="selector3" id="categoryParent" ng-model="category.parent">
                <option>Choose a category or none</option>
                <option ng-repeat="cat in categories" value="{{cat.categoryTitle}}">{{cat.categoryTitle}}</option>
            </select>

In the AngularJS CategoryController, I use $scope.category.parent to capture the value of the selected category (as parent category). This step works, because I am able to see the selected value by console.log($scope.category.parent);.

However, when I save the new category together with its parent category to MongoDB, the parent category is null even though other fields are saved properly, as shown in the Mongo terminal. I guess the problem is with the 'create' API I wrote for saving the new category in Sails. Could you spot where might be wrong in the following code?

create: function(req, res, next) {
var params = req.allParams();

// set parent category if exists
if (params.parentCat) {

    var parentCategory = Category.findOne({categoryTitle : params.parentCat})
        .exec(function(err, category) {
        if (err) {
            return null; //not found
        }

        return category; //found, return the category
    });

    params.parentCat = parentCategory;  //set the parent category in params
}

//insert the new category to MongoDB
Category.create(params, function(err, category) {
    if (err) {
        console.log('category addition failed');
        return next(err);
    }
    console.log('successfully added the category: ' + category.categoryTitle);
    res.redirect('/category');
});

} //create

Upvotes: 1

Views: 1155

Answers (1)

Mandeep Singh
Mandeep Singh

Reputation: 8224

You are confusing a few concepts here.

First of all

var parentCategory = Category.findOne({categoryTitle : params.parentCat})

Note that waterline method findOne will not return the document from database. So, the statement above will not serve you the purpose you intend to achieve. The important thing is the callback function you have supplied in the exec function. Think of it like this:

Node.js event loop will just execute the Category.findOne and move ahead to the next statement without waiting for the completion of findOne. The next statement here is :

params.parentCat = parentCategory

Note that fetching from MongoDB is not completed yet. Even if it were completed, findOne does not return the reference to the document in database as I said above. So, this is not the way things are done in Node.js. Instead, If you want to access the database object, its present in the category variable of the callback function in exec.

Now the second problem is the statement below

return category;

return as you know, will get you out of the create action completely. Also, an interesting point to note here is that there will be race between this statement and the redirect statement at the bottom. Here is why

Node.js event loop executes each statement it encounters. If the statement is a blocking one (like var x = <some computation without callback function>), then it will stay at that statement and wait for its completion. Otherwise, if the statement is asynchronous one with callback function (like Category.findOne({}).exec(function (err, category){})) then event loop will move to the next statement and the callback will be executed as and when the findOne completes.

Now, in this case, findOne will be executed and event loop will move on to

params.parentCat = parentCategory; 

this is not an async statement but it completes instantaneously and event loop moves on to

Category.create()

Now, you can think of it like both findOne and create will be executing in parallel in mongoDB. Once their database operations complete, the callback functions will be executed. Whichever callback executes first, its return statement will be executed. So, the behaviour is unpredictable here. Either return category might execute or res.redirect('/category'); might.

I have re-written the create function below keeping all the points mentioned above

create: function(req, res, next) {
    var params = req.params.all();

    // set parent category if exists
    if (params.parentCat) {
        Category.findOne({categoryTitle : params.parentCat}).exec(function (err, parentCategory) {
            if (err) {
                return res.json(500, 'Sever error'); //not found
            }
            else{
                params.parentCat = parentCategory.id;  //set the parent category in params
                //insert the new category to MongoDB
                Category.create(params, function (err, category) {
                    if (err) {
                        console.log('category addition failed');
                        return next(err);
                    }
                    console.log('successfully added the category: ' + category.categoryTitle);
                    res.redirect('/category');
                });                     
            }
        });
    }
    else{
        return res.json(500,'Server error. Parent category required'); 
    }
}

Upvotes: 3

Related Questions