Jason Sims
Jason Sims

Reputation: 1158

Sails 0.10 association fails to populate

I'm working on a custom adapter in [email protected] which will support associations but I am having trouble getting them working in conjunction with my adapter. My configuration is a one-to-many association between article and stats. My models and adapter are setup like this:

// api/models/article.js
module.exports = {
  connection: ['myadapter'],
  tableName: 'Knowledge_Base__kav',
  attributes: {
    KnowledgeArticleId: { type: 'string', primaryKey: true }
    stats: {
      collection: 'stats',
      via: 'parentId'
    } 
  }

// api/models/stats.js
module.exports = {
  connection: ['myadapter'],
  tableName: 'KnowledgeArticleViewStat',
  attributes: {
    count: 'integer',
    ParentId: {
      model: 'article'
    }
  }
}


// adapter.js  
find: function(connectionName, collectionName, options, cb) {  
  console.dir(options)
  // output
  // {where: null} 
  db.query(options, function(err, res)) {
    cb(err, res)
  }
}

However, when I try to populate using Article.find().populate('stats').exec(console.log()), my adapter gets {where: null} as options when I would expect it to receive {where: {parentId: [<some-article-id>]}}. It will return a list of articles to me but the field which is supposed to be populated from another model (stats) is just an empty list.

I feel like this is related to the fact that my adapter is not getting the proper where param to search for the related model on the primary key. To test this further, I setup a test one-to-many relationship using the the sails-mongo adapter. In this case the adapter did receive params I expected and the association worked fine.

Does anyone have any idea on why .populate('stats') wouldn't be sending the proper "where" params to my adapter?

Update 3/7

So it seems like what happens in associations is that SomeModel.find() will hit the adapter once and then .populate('othermodel') hits the adapter again using the primary key of the first request. Then the results of both are joined together. In my case, the second hit against the adapter isn't happening for some unknown reason.

Update

The original issue was related to an attribute naming error that's mentioned in the comments below. However, there still appears to be some issue with the final population step mentioned by particlebanana:

Final step will: Take all of the query results from all the returned query operations and combine them in-memory to build up a result set you can return in the exec callback.

I'm seeing that all required queries are now firing but they are failing to actually populate the alias. Here's the call with some added debugging output in the form of a gist for easier consumption: https://gist.github.com/jasonsims/9423170

Upvotes: 3

Views: 2370

Answers (2)

Jason Sims
Jason Sims

Reputation: 1158

Just to summarize, there were multiple issues going on here which were causing associations not to populate:

  1. Custom primary keys
    There was a problem with waterline when joining data from models using custom primary keys. @particlebanana fixed this in 8eff54b and it should be included in the next rc of waterline ([email protected]).
  2. Malformed SOQL query
    When waterline queries the adapter for a second time in order to acquire the child rows, it does so using { foreignKey: [ value ] }. Since the value was a list, jsforce was incorrectly generating the SOQL query since it expected all list values to be accompanied by either $in or $nin operators. I addressed this issue in github/jsforce#9 and it's now included in [email protected].
  3. Model attributes are case sensitive
    The models in my project were defined in snakeCase but the json response from Salesforce was using EveryWordCapitalized. This causes 1-to-many joins in waterline to reduce the many child records to one when it runs _.uniq(childRows, pk). Since the model has defined pk == id but the actual value returned from Salesforce is pk == Id, this call to uniq blows away all child records but one. I'm not entirely sure if this should be a waterline bug or not but fixing the capitalization in the model attribute definitions resolved this.

Upvotes: 0

particlebanana
particlebanana

Reputation: 2416

It looks like you are on the right track! The way the operation sets get built up, the .find() on the Article should run with the first log (empty where) and the second query should get run with the parentId criteria in the log. The second query isn't running because it can't build up that parentId array of primary keys when you don't return anything from the first query.

Short answer: you need to return something in the find callback to see the second log, which should match your expected criteria.

The query lifecycle looks something like this:

  • Check if all query pieces are on the same connection, if not break out which queries will run on which connections
  • For all queries on a single connection, check if the adapter supports native joins (has a .join() method, if so you can pass the criteria down and let the adapter handle the joins.
  • If no native join method is defined run the "parent" operation (in this case the Article.find())
  • Use the results of the parent operation to build up criteria for any populations that need to run. (The parentId array in your criteria) and run the child results.
  • Take all of the query results from all the returned query operations and combine them in-memory to build up a result set you can return in the exec callback.

I hope that helps some. Shoot me the url of your repo and I will look through it, if it's able to be open sourced, and can help some more if you come across any issues.

Upvotes: 3

Related Questions