Merge branch 'include-db-call-spike' of https://github.com/walkonsocial/loopback-datasource-juggler into walkonsocial-include-db-call-spike
This commit is contained in:
commit
f9bd1544f9
607
lib/include.js
607
lib/include.js
|
@ -2,6 +2,7 @@ var async = require('async');
|
||||||
var utils = require('./utils');
|
var utils = require('./utils');
|
||||||
var isPlainObject = utils.isPlainObject;
|
var isPlainObject = utils.isPlainObject;
|
||||||
var defineCachedRelations = utils.defineCachedRelations;
|
var defineCachedRelations = utils.defineCachedRelations;
|
||||||
|
var debug = require('debug')('loopback:include');
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Normalize the include to be an array
|
* Normalize the include to be an array
|
||||||
|
@ -57,6 +58,52 @@ IncludeScope.prototype.include = function() {
|
||||||
return this._include;
|
return this._include;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the idKey of a Model.
|
||||||
|
* @param {ModelConstructor} m - Model Constructor
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function idName(m) {
|
||||||
|
return m.definition.idName() || 'id';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Look up a model by name from the list of given models
|
||||||
|
* @param {Object} models Models keyed by name
|
||||||
|
* @param {String} modelName The model name
|
||||||
|
* @returns {*} The matching model class
|
||||||
|
*/
|
||||||
|
function lookupModel(models, modelName) {
|
||||||
|
if (models[modelName]) {
|
||||||
|
return models[modelName];
|
||||||
|
}
|
||||||
|
var lookupClassName = modelName.toLowerCase();
|
||||||
|
for (var name in models) {
|
||||||
|
if (name.toLowerCase() === lookupClassName) {
|
||||||
|
return models[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility Function to allow interleave before and after high computation tasks
|
||||||
|
* @param tasks
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function execTasksWithInterLeave(tasks, callback) {
|
||||||
|
//let's give others some time to process.
|
||||||
|
//Context Switch BEFORE Heavy Computation
|
||||||
|
process.nextTick(function () {
|
||||||
|
//Heavy Computation
|
||||||
|
async.parallel(tasks, function (err, info) {
|
||||||
|
//Context Switch AFTER Heavy Computation
|
||||||
|
process.nextTick(function () {
|
||||||
|
callback(err, info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Include mixin for ./model.js
|
* Include mixin for ./model.js
|
||||||
*/
|
*/
|
||||||
|
@ -102,6 +149,7 @@ Inclusion.normalizeInclude = normalizeInclude;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
Inclusion.include = function (objects, include, cb) {
|
Inclusion.include = function (objects, include, cb) {
|
||||||
|
debug('include', include);
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (!include || (Array.isArray(include) && include.length === 0) ||
|
if (!include || (Array.isArray(include) && include.length === 0) ||
|
||||||
|
@ -133,8 +181,13 @@ Inclusion.include = function (objects, include, cb) {
|
||||||
subInclude = scope.include();
|
subInclude = scope.include();
|
||||||
} else {
|
} else {
|
||||||
subInclude = include[relationName];
|
subInclude = include[relationName];
|
||||||
|
//when include = {user:true}, it does not have subInclude
|
||||||
|
if (subInclude === true) {
|
||||||
|
subInclude = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
relationName = include;
|
relationName = include;
|
||||||
subInclude = null;
|
subInclude = null;
|
||||||
}
|
}
|
||||||
|
@ -145,22 +198,518 @@ Inclusion.include = function (objects, include, cb) {
|
||||||
+ self.modelName + ' model'));
|
+ self.modelName + ' model'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var polymorphic = relation.polymorphic;
|
||||||
|
//if (polymorphic && !polymorphic.discriminator) {
|
||||||
|
// cb(new Error('Relation "' + relationName + '" is polymorphic but ' +
|
||||||
|
// 'discriminator is not present'));
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
if (!relation.modelTo) {
|
||||||
|
if (!relation.polymorphic) {
|
||||||
|
cb(new Error('Relation.modelTo is not defined for relation' +
|
||||||
|
relationName + ' and is no polymorphic'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Just skip if inclusion is disabled
|
// Just skip if inclusion is disabled
|
||||||
if (relation.options.disableInclude) {
|
if (relation.options.disableInclude) {
|
||||||
cb();
|
cb();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//prepare filter and fields for making DB Call
|
||||||
// Calling the relation method for each object
|
var filter = (scope && scope.conditions()) || {};
|
||||||
async.each(objs, function (obj, callback) {
|
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
|
||||||
if(relation.type === 'belongsTo') {
|
var includeScope = {};
|
||||||
|
// make sure not to miss any fields for sub includes
|
||||||
|
if (filter.fields && Array.isArray(subInclude) &&
|
||||||
|
relation.modelTo.relations) {
|
||||||
|
includeScope.fields = [];
|
||||||
|
subInclude.forEach(function (name) {
|
||||||
|
var rel = relation.modelTo.relations[name];
|
||||||
|
if (rel && rel.type === 'belongsTo') {
|
||||||
|
includeScope.fields.push(rel.keyFrom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
utils.mergeQuery(filter, includeScope, {fields: false});
|
||||||
|
}
|
||||||
|
//Let's add a placeholder where query
|
||||||
|
filter.where = filter.where || {};
|
||||||
|
//if fields are specified, make sure target foreign key is present
|
||||||
|
var fields = filter.fields;
|
||||||
|
if (Array.isArray(fields) && fields.indexOf(relation.keyTo) === -1) {
|
||||||
|
fields.push(relation.keyTo);
|
||||||
|
}
|
||||||
|
else if (isPlainObject(fields) && !fields[relation.keyTo]) {
|
||||||
|
fields[relation.keyTo] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* call relation specific include functions
|
||||||
|
*/
|
||||||
|
if (relation.multiple) {
|
||||||
|
if (relation.modelThrough) {
|
||||||
|
//hasManyThrough needs separate handling
|
||||||
|
return includeHasManyThrough(cb);
|
||||||
|
}
|
||||||
|
//This will also include embedsMany with belongsTo.
|
||||||
|
//Might need to optimize db calls for this.
|
||||||
|
if (relation.type === 'embedsMany') {
|
||||||
|
//embedded docs are part of the objects, no need to make db call.
|
||||||
|
//proceed as implemented earlier.
|
||||||
|
return includeEmbeds(cb);
|
||||||
|
}
|
||||||
|
if (relation.type === 'referencesMany') {
|
||||||
|
return includeReferencesMany(cb);
|
||||||
|
}
|
||||||
|
//assuming all other relations with multiple=true as hasMany
|
||||||
|
return includeHasMany(cb);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (polymorphic) {
|
||||||
|
return includePolymorphic(cb);
|
||||||
|
}
|
||||||
|
if (relation.type === 'embedsOne') {
|
||||||
|
return includeEmbeds(cb);
|
||||||
|
}
|
||||||
|
//hasOne or belongsTo
|
||||||
|
return includeOneToOne(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle inclusion of HasManyThrough/HasAndBelongsToMany/Polymorphic
|
||||||
|
* HasManyThrough relations
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function includeHasManyThrough(callback) {
|
||||||
|
var debug = require('debug')('loopback:include:includeHasManyThrough');
|
||||||
|
var sourceIds = [];
|
||||||
|
//Map for Indexing objects by their id for faster retrieval
|
||||||
|
var objIdMap = {};
|
||||||
|
for (var i = 0; i < objs.length; i++) {
|
||||||
|
var obj = objs[i];
|
||||||
|
// one-to-many: foreign key reference is modelTo -> modelFrom.
|
||||||
|
// use modelFrom.keyFrom in where filter later
|
||||||
|
var sourceId = obj[relation.keyFrom];
|
||||||
|
if (sourceId) {
|
||||||
|
sourceIds.push(sourceId);
|
||||||
|
objIdMap[sourceId.toString()] = obj;
|
||||||
|
}
|
||||||
|
// sourceId can be null. but cache empty data as result
|
||||||
|
defineCachedRelations(obj);
|
||||||
|
obj.__cachedRelations[relationName] = [];
|
||||||
|
}
|
||||||
|
//default filters are not applicable on through model. should be applied
|
||||||
|
//on modelTo later in 2nd DB call.
|
||||||
|
var throughFilter = {
|
||||||
|
where: {}
|
||||||
|
};
|
||||||
|
throughFilter.where[relation.keyTo] = {
|
||||||
|
inq: sourceIds
|
||||||
|
};
|
||||||
|
if (polymorphic) {
|
||||||
|
//handle polymorphic hasMany (reverse) in which case we need to filter
|
||||||
|
//by discriminator to filter other types
|
||||||
|
throughFilter.where[polymorphic.discriminator] =
|
||||||
|
relation.modelFrom.definition.name;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 1st DB Call of 2 step process. Get through model objects first
|
||||||
|
*/
|
||||||
|
relation.modelThrough.all(throughFilter, throughFetchHandler);
|
||||||
|
/**
|
||||||
|
* Handle the results of Through model objects and fetch the modelTo items
|
||||||
|
* @param err
|
||||||
|
* @param {Array<Model>} throughObjs
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function throughFetchHandler(err, throughObjs) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// start preparing for 2nd DB call.
|
||||||
|
var targetIds = [];
|
||||||
|
var targetObjsMap = {};
|
||||||
|
for (var i = 0; i < throughObjs.length; i++) {
|
||||||
|
var throughObj = throughObjs[i];
|
||||||
|
var targetId = throughObj[relation.keyThrough];
|
||||||
|
if (targetId) {
|
||||||
|
//save targetIds for 2nd DB Call
|
||||||
|
targetIds.push(targetId);
|
||||||
|
var sourceObj = objIdMap[throughObj[relation.keyTo]];
|
||||||
|
var targetIdStr = targetId.toString();
|
||||||
|
//Since targetId can be duplicates, multiple source objs are put
|
||||||
|
//into buckets.
|
||||||
|
var objList = targetObjsMap[targetIdStr] =
|
||||||
|
targetObjsMap[targetIdStr] || [];
|
||||||
|
objList.push(sourceObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Polymorphic relation does not have idKey of modelTo. Find it manually
|
||||||
|
var modelToIdName = idName(relation.modelTo);
|
||||||
|
filter.where[modelToIdName] = {
|
||||||
|
inq: targetIds
|
||||||
|
};
|
||||||
|
//make sure that the modelToIdName is included if fields are specified
|
||||||
|
if (Array.isArray(fields) && fields.indexOf(modelToIdName) === -1) {
|
||||||
|
fields.push(modelToIdName);
|
||||||
|
}
|
||||||
|
else if (isPlainObject(fields) && !fields[modelToIdName]) {
|
||||||
|
fields[modelToIdName] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2nd DB Call of 2 step process. Get modelTo (target) objects
|
||||||
|
*/
|
||||||
|
relation.modelTo.all(filter, targetsFetchHandler);
|
||||||
|
function targetsFetchHandler(err, targets) {
|
||||||
|
var tasks = [];
|
||||||
|
//simultaneously process subIncludes. Call it first as it is an async
|
||||||
|
//process.
|
||||||
|
if (subInclude && targets) {
|
||||||
|
tasks.push(function subIncludesTask(next) {
|
||||||
|
relation.modelTo.include(targets, subInclude, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//process & link each target with object
|
||||||
|
tasks.push(targetLinkingTask);
|
||||||
|
function targetLinkingTask(next) {
|
||||||
|
async.each(targets, linkManyToMany, next);
|
||||||
|
function linkManyToMany(target, next) {
|
||||||
|
var targetId = target[modelToIdName];
|
||||||
|
var objList = targetObjsMap[targetId.toString()];
|
||||||
|
async.each(objList, function (obj, next) {
|
||||||
|
obj.__cachedRelations[relationName].push(target);
|
||||||
|
processTargetObj(obj, next);
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execTasksWithInterLeave(tasks, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle inclusion of ReferencesMany relation
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function includeReferencesMany(callback) {
|
||||||
|
var debug = require('debug')('loopback:include:includeReferencesMany');
|
||||||
|
var allTargetIds = [];
|
||||||
|
//Map for Indexing objects by their id for faster retrieval
|
||||||
|
var targetObjsMap = {};
|
||||||
|
for (var i = 0; i < objs.length; i++) {
|
||||||
|
var obj = objs[i];
|
||||||
|
// one-to-many: foreign key reference is modelTo -> modelFrom.
|
||||||
|
// use modelFrom.keyFrom in where filter later
|
||||||
|
var targetIds = obj[relation.keyFrom];
|
||||||
|
if (targetIds) {
|
||||||
|
//referencesMany has multiple targetIds per obj. We need to concat
|
||||||
|
// them into allTargetIds before DB Call
|
||||||
|
allTargetIds = allTargetIds.concat(targetIds);
|
||||||
|
for (var j = 0; j < targetIds.length; j++) {
|
||||||
|
var targetId = targetIds[j];
|
||||||
|
var targetIdStr = targetId.toString();
|
||||||
|
var objList = targetObjsMap[targetIdStr] =
|
||||||
|
targetObjsMap[targetIdStr] || [];
|
||||||
|
objList.push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sourceId can be null. but cache empty data as result
|
||||||
|
defineCachedRelations(obj);
|
||||||
|
obj.__cachedRelations[relationName] = [];
|
||||||
|
}
|
||||||
|
filter.where[relation.keyTo] = {
|
||||||
|
inq: allTargetIds
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Make the DB Call, fetch all target objects
|
||||||
|
*/
|
||||||
|
relation.modelTo.all(filter, targetFetchHandler);
|
||||||
|
/**
|
||||||
|
* Handle the fetched target objects
|
||||||
|
* @param err
|
||||||
|
* @param {Array<Model>}targets
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function targetFetchHandler(err, targets) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
var tasks = [];
|
||||||
|
//simultaneously process subIncludes
|
||||||
|
if (subInclude && targets) {
|
||||||
|
tasks.push(function subIncludesTask(next) {
|
||||||
|
relation.modelTo.include(targets, subInclude, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//process each target object
|
||||||
|
tasks.push(targetLinkingTask);
|
||||||
|
function targetLinkingTask(next) {
|
||||||
|
async.each(targets, linkManyToMany, next);
|
||||||
|
function linkManyToMany(target, next) {
|
||||||
|
var objList = targetObjsMap[target[relation.keyTo].toString()];
|
||||||
|
async.each(objList, function (obj, next) {
|
||||||
|
obj.__cachedRelations[relationName].push(target);
|
||||||
|
processTargetObj(obj, next);
|
||||||
|
}, next);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execTasksWithInterLeave(tasks, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle inclusion of HasMany relation
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function includeHasMany(callback) {
|
||||||
|
var debug = require('debug')('loopback:include:includeHasMany');
|
||||||
|
var sourceIds = [];
|
||||||
|
//Map for Indexing objects by their id for faster retrieval
|
||||||
|
var objIdMap = {};
|
||||||
|
for (var i = 0; i < objs.length; i++) {
|
||||||
|
var obj = objs[i];
|
||||||
|
// one-to-many: foreign key reference is modelTo -> modelFrom.
|
||||||
|
// use modelFrom.keyFrom in where filter later
|
||||||
|
var sourceId = obj[relation.keyFrom];
|
||||||
|
if (sourceId) {
|
||||||
|
sourceIds.push(sourceId);
|
||||||
|
objIdMap[sourceId.toString()] = obj;
|
||||||
|
}
|
||||||
|
// sourceId can be null. but cache empty data as result
|
||||||
|
defineCachedRelations(obj);
|
||||||
|
obj.__cachedRelations[relationName] = [];
|
||||||
|
}
|
||||||
|
filter.where[relation.keyTo] = {
|
||||||
|
inq: sourceIds
|
||||||
|
};
|
||||||
|
relation.modelTo.all(filter, targetFetchHandler);
|
||||||
|
/**
|
||||||
|
* Process fetched related objects
|
||||||
|
* @param err
|
||||||
|
* @param {Array<Model>} targets
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function targetFetchHandler(err, targets) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
var tasks = [];
|
||||||
|
//simultaneously process subIncludes
|
||||||
|
if (subInclude && targets) {
|
||||||
|
tasks.push(function subIncludesTask(next) {
|
||||||
|
relation.modelTo.include(targets, subInclude, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//process each target object
|
||||||
|
tasks.push(targetLinkingTask);
|
||||||
|
function targetLinkingTask(next) {
|
||||||
|
async.each(targets, linkManyToOne, next);
|
||||||
|
function linkManyToOne(target, next) {
|
||||||
|
var obj = objIdMap[target[relation.keyTo].toString()];
|
||||||
|
obj.__cachedRelations[relationName].push(target);
|
||||||
|
processTargetObj(obj, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execTasksWithInterLeave(tasks, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Inclusion of Polymorphic BelongsTo/HasOne relation
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function includePolymorphic(callback) {
|
||||||
|
var debug = require('debug')('loopback:include:includePolymorphic');
|
||||||
|
var targetIdsByType = {};
|
||||||
|
//Map for Indexing objects by their type and targetId for faster retrieval
|
||||||
|
var targetObjMapByType = {};
|
||||||
|
for (var i = 0; i < objs.length; i++) {
|
||||||
|
var obj = objs[i];
|
||||||
|
var discriminator = polymorphic.discriminator;
|
||||||
|
var modelType = obj[discriminator];
|
||||||
|
if (modelType) {
|
||||||
|
targetIdsByType[modelType] = targetIdsByType[modelType] || [];
|
||||||
|
targetObjMapByType[modelType] = targetObjMapByType[modelType] || {};
|
||||||
|
var targetIds = targetIdsByType[modelType];
|
||||||
|
var targetObjsMap = targetObjMapByType[modelType];
|
||||||
|
var targetId = obj[relation.keyFrom];
|
||||||
|
if (targetId) {
|
||||||
|
targetIds.push(targetId);
|
||||||
|
var targetIdStr = targetId.toString();
|
||||||
|
targetObjsMap[targetIdStr] = targetObjsMap[targetIdStr] || [];
|
||||||
|
//Is belongsTo. Multiple objects can have the same
|
||||||
|
//targetId and therefore map value is an array
|
||||||
|
targetObjsMap[targetIdStr].push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineCachedRelations(obj);
|
||||||
|
obj.__cachedRelations[relationName] = null;
|
||||||
|
}
|
||||||
|
async.each(Object.keys(targetIdsByType), processPolymorphicType,
|
||||||
|
callback);
|
||||||
|
/**
|
||||||
|
* Process Polymorphic objects of each type (modelType)
|
||||||
|
* @param {String} modelType
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function processPolymorphicType(modelType, callback) {
|
||||||
|
var typeFilter = {where: {}};
|
||||||
|
utils.mergeQuery(typeFilter, filter);
|
||||||
|
typeFilter.where[relation.keyTo] = {
|
||||||
|
inq: targetIds
|
||||||
|
};
|
||||||
|
var app = relation.modelFrom.app;
|
||||||
|
var Model = lookupModel(relation.modelFrom.dataSource.modelBuilder.
|
||||||
|
models, modelType);
|
||||||
|
if (!Model) {
|
||||||
|
callback(new Error('Discriminator type "' + modelType +
|
||||||
|
' specified but no model exists with such name'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Model.all(typeFilter, targetFetchHandler);
|
||||||
|
/**
|
||||||
|
* Process fetched related objects
|
||||||
|
* @param err
|
||||||
|
* @param {Array<Model>} targets
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function targetFetchHandler(err, targets) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
var tasks = [];
|
||||||
|
|
||||||
|
//simultaneously process subIncludes
|
||||||
|
if (subInclude && targets) {
|
||||||
|
tasks.push(function subIncludesTask(next) {
|
||||||
|
Model.include(targets, subInclude, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//process each target object
|
||||||
|
tasks.push(targetLinkingTask);
|
||||||
|
function targetLinkingTask(next) {
|
||||||
|
var targetObjsMap = targetObjMapByType[modelType];
|
||||||
|
async.each(targets, linkOneToMany, next);
|
||||||
|
function linkOneToMany(target, next) {
|
||||||
|
var objList = targetObjsMap[target[relation.keyTo].toString()];
|
||||||
|
async.each(objList, function (obj, next) {
|
||||||
|
obj.__cachedRelations[relationName] = target;
|
||||||
|
processTargetObj(obj, next);
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execTasksWithInterLeave(tasks, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Inclusion of BelongsTo/HasOne relation
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function includeOneToOne(callback) {
|
||||||
|
var debug = require('debug')('loopback:include:includeOneToOne');
|
||||||
|
var targetIds = [];
|
||||||
|
var objTargetIdMap = {};
|
||||||
|
for (var i = 0; i < objs.length; i++) {
|
||||||
|
var obj = objs[i];
|
||||||
|
if (relation.type === 'belongsTo') {
|
||||||
|
if (obj[relation.keyFrom] === null ||
|
||||||
|
obj[relation.keyFrom] === undefined) {
|
||||||
|
defineCachedRelations(obj);
|
||||||
|
obj.__cachedRelations[relationName] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var targetId = obj[relation.keyFrom];
|
||||||
|
if (targetId) {
|
||||||
|
targetIds.push(targetId);
|
||||||
|
var targetIdStr = targetId.toString();
|
||||||
|
objTargetIdMap[targetIdStr] = objTargetIdMap[targetIdStr] || [];
|
||||||
|
objTargetIdMap[targetIdStr].push(obj);
|
||||||
|
}
|
||||||
|
defineCachedRelations(obj);
|
||||||
|
obj.__cachedRelations[relationName] = null;
|
||||||
|
}
|
||||||
|
filter.where[relation.keyTo] = {
|
||||||
|
inq: targetIds
|
||||||
|
};
|
||||||
|
relation.modelTo.all(filter, targetFetchHandler);
|
||||||
|
/**
|
||||||
|
* Process fetched related objects
|
||||||
|
* @param err
|
||||||
|
* @param {Array<Model>} targets
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function targetFetchHandler(err, targets) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
var tasks = [];
|
||||||
|
//simultaneously process subIncludes
|
||||||
|
if (subInclude && targets) {
|
||||||
|
tasks.push(function subIncludesTask(next) {
|
||||||
|
relation.modelTo.include(targets, subInclude, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//process each target object
|
||||||
|
tasks.push(targetLinkingTask);
|
||||||
|
function targetLinkingTask(next) {
|
||||||
|
async.each(targets, linkOneToMany, next);
|
||||||
|
function linkOneToMany(target, next) {
|
||||||
|
var targetId = target[relation.keyTo];
|
||||||
|
var objList = objTargetIdMap[targetId.toString()];
|
||||||
|
async.each(objList, function (obj, next) {
|
||||||
|
obj.__cachedRelations[relationName] = target;
|
||||||
|
processTargetObj(obj, next);
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execTasksWithInterLeave(tasks, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Inclusion of EmbedsMany/EmbedsManyWithBelongsTo/EmbedsOne
|
||||||
|
* Relations. Since Embedded docs are part of parents, no need to make
|
||||||
|
* db calls. Let the related function be called for each object to fetch
|
||||||
|
* the results from cache.
|
||||||
|
*
|
||||||
|
* TODO: Optimize EmbedsManyWithBelongsTo relation DB Calls
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
function includeEmbeds(callback) {
|
||||||
|
async.each(objs, function (obj, next) {
|
||||||
|
processTargetObj(obj, next);
|
||||||
|
}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Each Model Object and make sure specified relations are included
|
||||||
|
* @param {Model} obj - Single Mode object for which inclusion is needed
|
||||||
|
* @param callback
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function processTargetObj(obj, callback) {
|
||||||
|
var inst = (obj instanceof self) ? obj : new self(obj);
|
||||||
|
// Calling the relation method on the instance
|
||||||
|
if (relation.type === 'belongsTo') {
|
||||||
// If the belongsTo relation doesn't have an owner
|
// If the belongsTo relation doesn't have an owner
|
||||||
if(obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
|
if (obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
|
||||||
defineCachedRelations(obj);
|
defineCachedRelations(obj);
|
||||||
// Set to null if the owner doesn't exist
|
// Set to null if the owner doesn't exist
|
||||||
obj.__cachedRelations[relationName] = null;
|
obj.__cachedRelations[relationName] = null;
|
||||||
if(obj === inst) {
|
if (obj === inst) {
|
||||||
obj.__data[relationName] = null;
|
obj.__data[relationName] = null;
|
||||||
} else {
|
} else {
|
||||||
obj[relationName] = null;
|
obj[relationName] = null;
|
||||||
|
@ -168,10 +717,29 @@ Inclusion.include = function (objects, include, cb) {
|
||||||
return callback();
|
return callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
var inst = (obj instanceof self) ? obj : new self(obj);
|
* Sets the related objects as a property of Parent Object
|
||||||
// Calling the relation method on the instance
|
* @param {Array<Model>|Model|null} result - Related Object/Objects
|
||||||
|
* @param cb
|
||||||
|
*/
|
||||||
|
function setIncludeData(result, cb) {
|
||||||
|
if (obj === inst) {
|
||||||
|
obj.__data[relationName] = result;
|
||||||
|
obj.setStrict(false);
|
||||||
|
} else {
|
||||||
|
obj[relationName] = result;
|
||||||
|
}
|
||||||
|
cb(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
//obj.__cachedRelations[relationName] can be null if no data was returned
|
||||||
|
if (obj.__cachedRelations &&
|
||||||
|
obj.__cachedRelations[relationName] !== undefined) {
|
||||||
|
return setIncludeData(obj.__cachedRelations[relationName],
|
||||||
|
callback);
|
||||||
|
}
|
||||||
|
//If related objects are not cached by include Handlers, directly call
|
||||||
|
//related accessor function even though it is not very efficient
|
||||||
var related; // relation accessor function
|
var related; // relation accessor function
|
||||||
|
|
||||||
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
|
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
|
||||||
|
@ -204,23 +772,10 @@ Inclusion.include = function (objects, include, cb) {
|
||||||
defineCachedRelations(obj);
|
defineCachedRelations(obj);
|
||||||
obj.__cachedRelations[relationName] = result;
|
obj.__cachedRelations[relationName] = result;
|
||||||
|
|
||||||
if(obj === inst) {
|
return setIncludeData(result, callback);
|
||||||
obj.__data[relationName] = result;
|
|
||||||
obj.setStrict(false);
|
|
||||||
} else {
|
|
||||||
obj[relationName] = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subInclude && result) {
|
|
||||||
var subItems = relation.multiple ? result : [result];
|
|
||||||
// Recursively include the related models
|
|
||||||
relation.modelTo.include(subItems, subInclude, callback);
|
|
||||||
} else {
|
|
||||||
callback(null, result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, cb);
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bluebird": "^2.9.9",
|
"bluebird": "^2.9.9",
|
||||||
"mocha": "^2.1.0",
|
"mocha": "^2.1.0",
|
||||||
"should": "^5.0.0"
|
"should": "^5.0.0",
|
||||||
|
"sinon": "^1.14.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^0.9.0",
|
"async": "^0.9.0",
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// This test written in mocha+should.js
|
// This test written in mocha+should.js
|
||||||
var should = require('./init.js');
|
var should = require('./init.js');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
var db, User, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
|
var db, User, Profile, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
|
||||||
|
|
||||||
describe('include', function () {
|
describe('include', function () {
|
||||||
|
|
||||||
|
@ -334,20 +336,203 @@ describe('include', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Not implemented correctly, see: loopback-datasource-juggler/issues/166
|
it('should fetch User - Profile (HasOne)', function (done) {
|
||||||
//
|
User.find({include: ['profile']}, function (err, users) {
|
||||||
// it('should support include scope on hasAndBelongsToMany', function (done) {
|
should.not.exist(err);
|
||||||
// Assembly.find({include: { relation: 'parts', scope: {
|
should.exist(users);
|
||||||
// where: { partNumber: 'engine' }
|
users.length.should.be.ok;
|
||||||
// }}}, function (err, assemblies) {
|
var usersWithProfile = 0;
|
||||||
// assemblies.length.should.equal(1);
|
users.forEach(function (user) {
|
||||||
// var parts = assemblies[0].parts();
|
// The relation should be promoted as the 'owner' property
|
||||||
// parts.should.have.length(1);
|
user.should.have.property('profile');
|
||||||
// parts[0].partNumber.should.equal('engine');
|
var userObj = user.toJSON();
|
||||||
// done();
|
var profile = user.profile();
|
||||||
// });
|
if (profile) {
|
||||||
// });
|
profile.should.be.an.instanceOf(Profile);
|
||||||
|
usersWithProfile++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
(profile === null).should.be.true;
|
||||||
|
}
|
||||||
|
// The __cachedRelations should be removed from json output
|
||||||
|
userObj.should.not.have.property('__cachedRelations');
|
||||||
|
user.__cachedRelations.should.have.property('profile');
|
||||||
|
if (user.__cachedRelations.profile) {
|
||||||
|
user.__cachedRelations.profile.userId.should.eql(user.id);
|
||||||
|
usersWithProfile++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
usersWithProfile.should.equal(2 * 2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Not implemented correctly, see: loopback-datasource-juggler/issues/166
|
||||||
|
// fixed by DB optimization
|
||||||
|
it('should support include scope on hasAndBelongsToMany', function (done) {
|
||||||
|
Assembly.find({include: { relation: 'parts', scope: {
|
||||||
|
where: { partNumber: 'engine' }
|
||||||
|
}}}, function (err, assemblies) {
|
||||||
|
assemblies.length.should.equal(1);
|
||||||
|
var parts = assemblies[0].parts();
|
||||||
|
parts.should.have.length(1);
|
||||||
|
parts[0].partNumber.should.equal('engine');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(' performance - ', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.callSpy = sinon.spy(db.connector, 'all');
|
||||||
|
});
|
||||||
|
afterEach(function () {
|
||||||
|
db.connector.all.restore();
|
||||||
|
});
|
||||||
|
it('including belongsTo should make only 2 db calls', function (done) {
|
||||||
|
var self = this;
|
||||||
|
Passport.find({include: 'owner'}, function (err, passports) {
|
||||||
|
passports.length.should.be.ok;
|
||||||
|
passports.forEach(function (p) {
|
||||||
|
p.__cachedRelations.should.have.property('owner');
|
||||||
|
// The relation should be promoted as the 'owner' property
|
||||||
|
p.should.have.property('owner');
|
||||||
|
// The __cachedRelations should be removed from json output
|
||||||
|
p.toJSON().should.not.have.property('__cachedRelations');
|
||||||
|
var owner = p.__cachedRelations.owner;
|
||||||
|
if (!p.ownerId) {
|
||||||
|
should.not.exist(owner);
|
||||||
|
} else {
|
||||||
|
should.exist(owner);
|
||||||
|
owner.id.should.eql(p.ownerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.callSpy.calledTwice.should.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('including hasManyThrough should make only 3 db calls', function (done) {
|
||||||
|
var self = this;
|
||||||
|
Assembly.create([{name: 'sedan'}, {name: 'hatchback'},
|
||||||
|
{name: 'SUV'}],
|
||||||
|
function (err, assemblies) {
|
||||||
|
Part.create([{partNumber: 'engine'}, {partNumber: 'bootspace'},
|
||||||
|
{partNumber: 'silencer'}],
|
||||||
|
function (err, parts) {
|
||||||
|
async.each(parts, function (part, next) {
|
||||||
|
async.each(assemblies, function (assembly, next) {
|
||||||
|
if (assembly.name === 'SUV') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
if (assembly.name === 'hatchback' &&
|
||||||
|
part.partNumber === 'bootspace') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
assembly.parts.add(part, function (err, data) {
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}, next);
|
||||||
|
}, function (err) {
|
||||||
|
self.callSpy.reset();
|
||||||
|
Assembly.find({
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
inq: ['sedan', 'hatchback', 'SUV']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: 'parts'
|
||||||
|
}, function (err, result) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exists(result);
|
||||||
|
result.length.should.equal(3);
|
||||||
|
//sedan
|
||||||
|
result[0].parts().should.have.length(3);
|
||||||
|
//hatcback
|
||||||
|
result[1].parts().should.have.length(2);
|
||||||
|
//SUV
|
||||||
|
result[2].parts().should.have.length(0);
|
||||||
|
self.callSpy.calledThrice.should.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('including hasMany should make only 2 db calls', function (done) {
|
||||||
|
var self = this;
|
||||||
|
User.find({include: ['posts', 'passports']}, function (err, users) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(users);
|
||||||
|
users.length.should.be.ok;
|
||||||
|
users.forEach(function (user) {
|
||||||
|
// The relation should be promoted as the 'owner' property
|
||||||
|
user.should.have.property('posts');
|
||||||
|
user.should.have.property('passports');
|
||||||
|
|
||||||
|
var userObj = user.toJSON();
|
||||||
|
userObj.should.have.property('posts');
|
||||||
|
userObj.should.have.property('passports');
|
||||||
|
userObj.posts.should.be.an.instanceOf(Array);
|
||||||
|
userObj.passports.should.be.an.instanceOf(Array);
|
||||||
|
|
||||||
|
// The __cachedRelations should be removed from json output
|
||||||
|
userObj.should.not.have.property('__cachedRelations');
|
||||||
|
|
||||||
|
user.__cachedRelations.should.have.property('posts');
|
||||||
|
user.__cachedRelations.should.have.property('passports');
|
||||||
|
user.__cachedRelations.posts.forEach(function (p) {
|
||||||
|
p.userId.should.eql(user.id);
|
||||||
|
});
|
||||||
|
user.__cachedRelations.passports.forEach(function (pp) {
|
||||||
|
pp.ownerId.should.eql(user.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.callSpy.calledThrice.should.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should not make n+1 db calls in relation syntax',
|
||||||
|
function (done) {
|
||||||
|
var self = this;
|
||||||
|
User.find({include: [{ relation: 'posts', scope: {
|
||||||
|
where: {title: 'Post A'}
|
||||||
|
}}, 'passports']}, function (err, users) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(users);
|
||||||
|
users.length.should.be.ok;
|
||||||
|
users.forEach(function (user) {
|
||||||
|
// The relation should be promoted as the 'owner' property
|
||||||
|
user.should.have.property('posts');
|
||||||
|
user.should.have.property('passports');
|
||||||
|
|
||||||
|
var userObj = user.toJSON();
|
||||||
|
userObj.should.have.property('posts');
|
||||||
|
userObj.should.have.property('passports');
|
||||||
|
userObj.posts.should.be.an.instanceOf(Array);
|
||||||
|
userObj.passports.should.be.an.instanceOf(Array);
|
||||||
|
|
||||||
|
// The __cachedRelations should be removed from json output
|
||||||
|
userObj.should.not.have.property('__cachedRelations');
|
||||||
|
|
||||||
|
user.__cachedRelations.should.have.property('posts');
|
||||||
|
user.__cachedRelations.should.have.property('passports');
|
||||||
|
user.__cachedRelations.posts.forEach(function (p) {
|
||||||
|
p.userId.should.eql(user.id);
|
||||||
|
p.title.should.be.equal('Post A');
|
||||||
|
});
|
||||||
|
user.__cachedRelations.passports.forEach(function (pp) {
|
||||||
|
pp.ownerId.should.eql(user.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.callSpy.calledThrice.should.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup(done) {
|
function setup(done) {
|
||||||
|
@ -359,6 +544,9 @@ function setup(done) {
|
||||||
name: String,
|
name: String,
|
||||||
age: Number
|
age: Number
|
||||||
});
|
});
|
||||||
|
Profile = db.define('Profile', {
|
||||||
|
profileName: String
|
||||||
|
});
|
||||||
AccessToken = db.define('AccessToken', {
|
AccessToken = db.define('AccessToken', {
|
||||||
token: String
|
token: String
|
||||||
});
|
});
|
||||||
|
@ -376,6 +564,8 @@ function setup(done) {
|
||||||
foreignKey: 'userId',
|
foreignKey: 'userId',
|
||||||
options: {disableInclude: true}
|
options: {disableInclude: true}
|
||||||
});
|
});
|
||||||
|
Profile.belongsTo('user', {model: User});
|
||||||
|
User.hasOne('profile', {foreignKey: 'userId'});
|
||||||
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
|
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
|
||||||
|
|
||||||
Assembly = db.define('Assembly', {
|
Assembly = db.define('Assembly', {
|
||||||
|
@ -392,6 +582,7 @@ function setup(done) {
|
||||||
db.automigrate(function () {
|
db.automigrate(function () {
|
||||||
var createdUsers = [];
|
var createdUsers = [];
|
||||||
var createdPassports = [];
|
var createdPassports = [];
|
||||||
|
var createdProfiles = [];
|
||||||
var createdPosts = [];
|
var createdPosts = [];
|
||||||
createUsers();
|
createUsers();
|
||||||
function createUsers() {
|
function createUsers() {
|
||||||
|
@ -438,6 +629,21 @@ function setup(done) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createProfiles() {
|
||||||
|
clearAndCreate(
|
||||||
|
Profile,
|
||||||
|
[
|
||||||
|
{profileName: 'Profile A', userId: createdUsers[0].id},
|
||||||
|
{profileName: 'Profile B', userId: createdUsers[1].id},
|
||||||
|
{profileName: 'Profile Z'}
|
||||||
|
],
|
||||||
|
function (items) {
|
||||||
|
createdProfiles = items
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createPosts() {
|
function createPosts() {
|
||||||
clearAndCreate(
|
clearAndCreate(
|
||||||
Post,
|
Post,
|
||||||
|
@ -450,7 +656,7 @@ function setup(done) {
|
||||||
],
|
],
|
||||||
function (items) {
|
function (items) {
|
||||||
createdPosts = items;
|
createdPosts = items;
|
||||||
done();
|
createProfiles();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue