offshore
Version:
An ORM for Node.js
209 lines (175 loc) • 7.54 kB
JavaScript
/**
* Module dependencies
*/
var validator = require('offshore-validator');
var _ = require('lodash');
var leftOuterJoin = require('./leftOuterJoin');
var innerJoin = require('./innerJoin');
var populate = require('./populate');
/**
* Query Integrator
*
* Combines the results from multiple child queries into
* the final return format using an in-memory join.
* Final step in fulfilling a `.find()` with one or more
* `populate(alias[n])` modifiers.
*
* > Why is this asynchronous?
* >
* > While this function isn't doing anything strictly
* > asynchronous, it still expects a callback to enable
* > future use of `process[setImmediate|nextTick]()` as
* > an optimization.
*
* @param {Object} cache
* @param {Array} joinInstructions - see JOIN_INSTRUCTIONS.md
* @callback {Function} cb(err, results)
* @param {Error}
* @param {Array} [results, complete w/ populations]
*
* @throws {Error} on invalid input
* @asynchronous
*/
module.exports = function integrate(cache, joinInstructions, primaryKey, cb) {
// Ensure valid usage
var invalid = false;
invalid = invalid || validator(cache).to({ type: 'object' });
invalid = invalid || validator(joinInstructions).to({ type: 'array' });
invalid = invalid || validator(joinInstructions[0]).to({ type: 'object' });
invalid = invalid || validator(joinInstructions[0].parent).to({ type: 'string' });
invalid = invalid || validator(cache[joinInstructions[0].parent]).to({ type: 'object' });
invalid = invalid || typeof primaryKey !== 'string';
invalid = invalid || typeof cb !== 'function';
if (invalid) return cb(invalid);
// Constant: String prepended to child attribute keys for use in namespacing.
var CHILD_ATTR_PREFIX = '.';
var GRANDCHILD_ATTR_PREFIX = '..';
// We'll reuse the cached data from the `parent` table modifying it in-place
// and returning it as our result set. (`results`)
var results = cache[ joinInstructions[0].parent ];
// Group the joinInstructions array by alias, then interate over each one
// s.t. `instructions` in our lambda function contains a list of join instructions
// for the particular `populate` on the specified key (i.e. alias).
//
// Below, `results` are mutated inline.
_.each(_.groupBy(joinInstructions, 'alias'),
function eachAssociation(instructions, alias) {
var parentPK, fkToParent, fkToChild, childPK;
// N..N Association
if (instructions.length === 2) {
// Name keys explicitly
// (makes it easier to see what's going on)
parentPK = instructions[0].parentKey;
fkToParent = instructions[0].childKey;
fkToChild = instructions[1].parentKey;
childPK = instructions[1].childKey;
// console.log('\n\n------------:: n..m leftOuterJoin ::--------\n',
// leftOuterJoin({
// left: cache[instructions[0].parent],
// right: cache[instructions[0].child],
// leftKey: parentPK,
// rightKey: fkToParent
// })
// );
// console.log('------------:: / ::--------\n');
// console.log('\n\n------------:: n..m childRows ::--------\n',innerJoin({
// left: leftOuterJoin({
// left: cache[instructions[0].parent],
// right: cache[instructions[0].child],
// leftKey: parentPK,
// rightKey: fkToParent
// }),
// right: cache[instructions[1].child],
// leftKey: CHILD_ATTR_PREFIX+fkToChild,
// rightKey: childPK,
// childNamespace: GRANDCHILD_ATTR_PREFIX
// }));
// console.log('------------:: / ::--------\n');
// Calculate and sanitize join data,
// then shove it into the parent results under `alias`
populate({
parentRows: results,
alias: alias,
childRows: innerJoin({
left: leftOuterJoin({
left: cache[instructions[0].parent],
right: cache[instructions[0].child],
leftKey: parentPK,
rightKey: fkToParent
}),
right: cache[instructions[1].child],
leftKey: CHILD_ATTR_PREFIX + fkToChild,
rightKey: childPK,
childNamespace: GRANDCHILD_ATTR_PREFIX
}),
parentPK: parentPK, // e.g. `id` (of message)
fkToChild: CHILD_ATTR_PREFIX + fkToChild, // e.g. `user_id` (of join table)
childPK: GRANDCHILD_ATTR_PREFIX + childPK, // e.g. `id` (of user)
childNamespace: GRANDCHILD_ATTR_PREFIX
});
// 1 ..N Association
} else if (instructions.length === 1) {
// Name keys explicitly
// (makes it easier to see what's going on)
parentPK = primaryKey;
fkToParent = parentPK;
fkToChild = instructions[0].parentKey;
childPK = instructions[0].childKey;
// Determine if this is a "hasOne" or a "belongsToMany"
// if the parent's primary key is the same as the fkToChild, it must be belongsToMany
if (parentPK === fkToChild) {
// In belongsToMany case, fkToChild needs prefix because it's actually the
// console.log('belongsToMany');
fkToChild = CHILD_ATTR_PREFIX + fkToChild;
// "hasOne" case
} else {
// console.log('hasOne');
}
// var childRows = innerJoin({
// left: cache[instructions[0].parent],
// right: cache[instructions[0].child],
// leftKey: instructions[0].parentKey,
// rightKey: instructions[0].childKey
// });
// console.log('1..N JOIN--------------\n',instructions,'\n^^^^^^^^^^^^^^^^^^^^^^');
// console.log('1..N KEYS--------------\n',{
// parentPK: parentPK,
// fkToParent: fkToParent,
// fkToChild: fkToChild,
// childPK: childPK,
// },'\n^^^^^^^^^^^^^^^^^^^^^^');
// console.log('1..N CHILD ROWS--------\n',childRows);
// Calculate and sanitize join data,
// then shove it into the parent results under `alias`
populate({
parentRows: results,
alias: alias,
childRows: innerJoin({
left: cache[instructions[0].parent],
right: cache[instructions[0].child],
leftKey: instructions[0].parentKey,
rightKey: instructions[0].childKey
}),
parentPK: fkToParent, // e.g. `id` (of message)
fkToChild: fkToChild, // e.g. `from`
childPK: childPK, // e.g. `id` (of user)
childNamespace: CHILD_ATTR_PREFIX
});
// If the alias isn't the same as the parent_key AND removeParentKey is set to true
// in the instructions this means that we are turning a FK into an embedded record and a
// columnName was used. We need to move the values attached to the alias property to
// the parent key value. If we don't then when we run the transformer everything would get crazy.
if (alias !== instructions[0].parentKey && instructions[0].removeParentKey === true) {
results = _.map(results, function(result) {
result[instructions[0].parentKey] = result[alias];
delete result[alias];
return result;
});
}
}
}
);
// And call the callback
// (the final joined data is in the cache -- also referenced by `results`)
return cb(null, results);
};