Search code examples
node.jsmongodbsails.jsone-to-manysails-mongo

How do I write my Sails.js model so that associated categories aren't recreated for every new document?


I feel like there must be a simple solution to this, but I can't seem to find the right way to search for it online. I'm using a Sails server on Node.js & Express. Right now I'm trying to get a basic CRUD API working, so I've been sending post & get connections with Postman.

I'm starting with an empty database (hosted on Mongolab) and trying to create two documents with different names, but the same category. Afterwards, I would expect there to be only 1 document in the category collection and two in the main collection. Instead, the second POST is triggering an error, because it's trying to create a second category document despite the {unique:true} attribute.

What can I do to fix this error and have proper one-to-many relationship?

\api\models\Beer.js:

module.exports = {
  attributes: {
    // Primitive attributes
    name: {
      type: 'string',
      required: true,
      unique: true
    },
    alcoholByVolume: {
      type: 'float',
      defaultsTo: null
    },
    beerAdvocateId: {
      type: 'integer',
      defaultsTo: null
    },

    // Associations (aka relational attributes)
    brewery: { model: 'Company' },
    style: { model: 'BeerStyle' }
  }
};

\api\models\Company.js:

module.exports = {
  attributes: {
    name: {
      type: 'string',
      required: true,
      unique: true
    },
    countryCode: {
      type: 'string',
      enum: ['ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', 'bm', 'bn', 'bo', 'bq', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cu', 'cv', 'cw', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'ss', 'st', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'za', 'zm', 'zw'],
      defaultsTo: 'us'
    }
  }
};

\api\models\BeerStyle.js:

module.exports = {
  attributes: {
    name: {
      type: 'string',
      enum: ['American Amber / Red Ale', 'American Barleywine', 'American Black Ale', 'American Blonde Ale', 'American Brown Ale', 'American Dark Wheat Ale', 'American Double / Imperial IPA', 'American Double / Imperial Stout', 'American IPA', 'American Pale Ale (APA)', 'American Pale Wheat Ale', 'American Porter', 'American Stout', 'American Strong Ale', 'American Wild Ale', 'Black & Tan', 'Chile Beer', 'Cream Ale', 'Pumpkin Ale', 'Rye Beer', 'Wheatwine', 'Belgian Dark Ale', 'Belgian IPA', 'Belgian Pale Ale', 'Belgian Strong Dark Ale', 'Belgian Strong Pale Ale', 'Bière de Champagne / Bière Brut', 'Bière de Garde', 'Dubbel', 'Faro', 'Flanders Oud Bruin', 'Flanders Red Ale', 'Gueuze', 'Lambic - Fruit', 'Lambic - Unblended', 'Quadrupel (Quad)', 'Saison / Farmhouse Ale', 'Tripel', 'Witbier', 'Baltic Porter', 'Braggot', 'English Barleywine', 'English Bitter', 'English Brown Ale', 'English Dark Mild Ale', 'English India Pale Ale (IPA)', 'English Pale Ale', 'English Pale Mild Ale', 'English Porter', 'English Stout', 'English Strong Ale', 'Extra Special / Strong Bitter (ESB)', 'Foreign / Export Stout', 'Milk / Sweet Stout', 'Oatmeal Stout', 'Old Ale', 'Russian Imperial Stout', 'Winter Warmer', 'Sahti', 'Altbier', 'Berliner Weissbier', 'Dunkelweizen', 'Gose', 'Hefeweizen', 'Kölsch', 'Kristalweizen', 'Roggenbier', 'Weizenbock', 'Irish Dry Stout', 'Irish Red Ale', 'Kvass', 'Scotch Ale / Wee Heavy', 'Scottish Ale', 'Scottish Gruit / Ancient Herbed Ale', 'Lager Styles', 'American Adjunct Lager', 'American Amber / Red Lager', 'American Double / Imperial Pilsner', 'American Malt Liquor', 'American Pale Lager', 'California Common / Steam Beer', 'Light Lager', 'Low Alcohol Beer', 'Czech Pilsener', 'Euro Dark Lager', 'Euro Pale Lager', 'Euro Strong Lager', 'Bock', 'Doppelbock', 'Dortmunder / Export Lager', 'Eisbock', 'German Pilsener', 'Kellerbier / Zwickelbier', 'Maibock / Helles Bock', 'Märzen / Oktoberfest', 'Munich Dunkel Lager', 'Munich Helles Lager', 'Rauchbier', 'Schwarzbier', 'Vienna Lager', 'Happoshu', 'Japanese Rice Lager', 'Fruit / Vegetable Beer', 'Herbed / Spiced Beer', 'Smoked Beer'],
      required: true,
      unique: true
    },
    group: {
      type: 'string',
      enum: ['American Ales', 'Belgian / French Ales', 'English Ales', 'Finnish Ales', 'German Ales', 'Irish Ales', 'Russian Ales', 'Scottish Ales', 'American Lagers', 'Czech Lagers', 'European Lagers', 'German Lagers', 'Japanese Lagers', 'Hybrid Styles'],
      required: true
    }
  }
};

\api\controllers\BeerController.js:

module.exports = {

  create: function(req, res){
    console.log(req.body);
    Beer.create(req.body).exec(function createCB(err, beer){
      if (err) return res.send(err);
      res.status(201);
      res.json(beer);
    });
  },

  // a FIND action
  read: function (req, res, next) {
    var id = req.param('id');

    if (id) {
      Beer.findOne(id, function(err, beer) {
        if(beer === undefined) return res.notFound();
        if (err) return res.send(err);
        res.json(beer);
      });

    } else {
        var where = req.param('where');
        if (_.isString(where)) where = JSON.parse(where);
        var options = {
          limit: req.param('limit') || undefined,
          skip: req.param('skip')  || undefined,
          sort: req.param('sort') || undefined,
          where: where || undefined
        };

        Beer.find(options, function(err, beer) {
          if(beer === undefined) return res.notFound();
          if (err) return res.send(err);
          res.json(beer);
        });
    }

  },

};

Within \config\routes.js:

'post /api/beers': 'BeerController.create',
'get /api/beers/:id?': 'BeerController.read',

Two requests:

POST /api/beers HTTP/1.1
Host: localhost:1337
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 0fd81643-08ce-7e43-6ed7-c3abafb22700
{"name":"Bud Light Lime","style": {"name": "Light Lager","group": "American Lagers"}, "brewery": {"name": "Anheuser-Busch","countryCode":"us"}, "alcoholByVolume": 4.2, "beerAdvocateId": 41821}

POST /api/beers HTTP/1.1
Host: localhost:1337
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 6e9efbd9-bb10-9748-7e70-229e131fe166
{"name":"Bud Light","style": {"name": "Light Lager","group": "American Lagers"}, "brewery": {"name": "Anheuser-Busch","countryCode":"us"}, "alcoholByVolume": 4.2, "beerAdvocateId": 1320}

Error on second request:

{
  "error": "E_UNKNOWN",
  "status": 500,
  "summary": "Encountered an unexpected error",
  "raw": {
    "name": "MongoError",
    "code": 11000,
    "err": "insertDocument :: caused by :: 11000 E11000 duplicate key error index: hkbooze.beerstyle.$name_1  dup key: { : \"Light Lager\" }"
  }
}

Solution

  • It seems like you are passing JSONs for style and brewery. To be honest I never tried to populate multiple models with 1 request like this. Is this really a feature of Sails? If yes, it still probably tries to create a category every time, which causes the uniqueness issue.

    My two ways of doing this would be: 1) Define the categories first if they stay the same. Pass just an id. or 2) Write some custom script probably into beforeValidate() which would take the JSON out of the request, tried to find it in categories by name. If it couldnt find anything, it would create a new category. In all cases, it would return an ID.

    this is some example code, it may contain flaws:]

    beforeValidate: function(values, cb){
    		if(typeof values.style !== 'string'){
    			Style.find({where: {name: values.style.name}}.exec(function(err, foundStyle){
                  if(!data){
                    Style.create(values.style,function(err, newstyle){
                      if(!err){
                        values.style = newstyle.id;
                        cb();
                      }
                    });
                  } else {
                    values.style  = foundStyle.id;
                    cb();
                  }
                });
    		}
    	}