loopback-datasource-juggler/lib/include.js

1061 lines
35 KiB
JavaScript
Raw Normal View History

2016-04-01 22:25:16 +00:00
// Copyright IBM Corp. 2013,2015. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2016-08-22 19:55:22 +00:00
'use strict';
2018-12-07 16:13:48 +00:00
const async = require('async');
const g = require('strong-globalize')();
const utils = require('./utils');
const List = require('./list');
const includeUtils = require('./include_utils');
const isPlainObject = utils.isPlainObject;
const defineCachedRelations = utils.defineCachedRelations;
const uniq = utils.uniq;
const idName = utils.idName;
const debug = require('debug')('loopback:include');
2014-10-10 10:28:39 +00:00
/*!
* Normalize the include to be an array
* @param include
* @returns {*}
*/
function normalizeInclude(include) {
2018-12-07 16:13:48 +00:00
let newInclude;
2014-10-10 10:28:39 +00:00
if (typeof include === 'string') {
return [include];
} else if (isPlainObject(include)) {
// Build an array of key/value pairs
newInclude = [];
2018-12-07 16:13:48 +00:00
const rel = include.rel || include.relation;
const obj = {};
if (typeof rel === 'string') {
2014-10-10 12:32:14 +00:00
obj[rel] = new IncludeScope(include.scope);
2014-10-10 10:28:39 +00:00
newInclude.push(obj);
} else {
2018-12-07 16:13:48 +00:00
for (const key in include) {
2014-10-10 10:28:39 +00:00
obj[key] = include[key];
newInclude.push(obj);
}
}
return newInclude;
} else if (Array.isArray(include)) {
newInclude = [];
2018-12-07 16:13:48 +00:00
for (let i = 0, n = include.length; i < n; i++) {
const subIncludes = normalizeInclude(include[i]);
newInclude = newInclude.concat(subIncludes);
}
return newInclude;
2014-10-10 10:28:39 +00:00
} else {
return include;
}
}
function IncludeScope(scope) {
this._scope = utils.deepMerge({}, scope || {});
if (this._scope.include) {
this._include = normalizeInclude(this._scope.include);
delete this._scope.include;
} else {
this._include = null;
}
}
2014-10-10 10:28:39 +00:00
IncludeScope.prototype.conditions = function() {
return utils.deepMerge({}, this._scope);
};
IncludeScope.prototype.include = function() {
return this._include;
};
/*!
* 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];
}
2018-12-07 16:13:48 +00:00
const lookupClassName = modelName.toLowerCase();
for (const 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
2016-04-01 11:48:17 +00:00
process.nextTick(function() {
// Heavy Computation
try {
async.parallel(tasks, function(err, info) {
// Context Switch AFTER Heavy Computation
process.nextTick(function() {
callback(err, info);
});
});
} catch (err) {
callback(err);
}
});
}
2014-03-12 23:28:46 +00:00
/*!
2013-04-11 23:23:34 +00:00
* Include mixin for ./model.js
*/
2013-05-28 05:20:30 +00:00
module.exports = Inclusion;
2014-03-12 23:28:46 +00:00
/**
* Inclusion - Model mixin.
*
* @class
*/
2013-05-28 05:20:30 +00:00
function Inclusion() {
}
2013-04-11 23:23:34 +00:00
/**
* Normalize includes - used in DataAccessObject
*
*/
Inclusion.normalizeInclude = normalizeInclude;
2013-04-11 23:23:34 +00:00
/**
2014-03-12 23:28:46 +00:00
* Enables you to load relations of several objects and optimize numbers of requests.
2013-04-11 23:23:34 +00:00
*
* Examples:
*
2014-03-12 23:28:46 +00:00
* Load all users' posts with only one additional request:
* `User.include(users, 'posts', function() {});`
* Or
* `User.include(users, ['posts'], function() {});`
*
* Load all users posts and passports with two additional requests:
* `User.include(users, ['posts', 'passports'], function() {});`
*
* Load all passports owner (users), and all posts of each owner loaded:
2015-05-13 23:10:07 +00:00
*```Passport.include(passports, {owner: 'posts'}, function() {});
*``` Passport.include(passports, {owner: ['posts', 'passports']});
*``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']});
2013-04-11 23:23:34 +00:00
*
2014-03-12 23:28:46 +00:00
* @param {Array} objects Array of instances
2014-06-18 23:42:00 +00:00
* @param {String|Object|Array} include Which relations to load.
* @param {Object} [options] Options for CRUD
2014-03-12 23:28:46 +00:00
* @param {Function} cb Callback called when relations are loaded
2015-05-13 23:10:07 +00:00
*
2013-04-11 23:23:34 +00:00
*/
2016-04-01 11:48:17 +00:00
Inclusion.include = function(objects, include, options, cb) {
if (typeof options === 'function' && cb === undefined) {
cb = options;
options = {};
}
2018-12-07 16:13:48 +00:00
const self = this;
2015-05-13 23:10:07 +00:00
2014-03-13 23:43:38 +00:00
if (!include || (Array.isArray(include) && include.length === 0) ||
2015-11-05 01:13:12 +00:00
(Array.isArray(objects) && objects.length === 0) ||
2014-03-13 23:43:38 +00:00
(isPlainObject(include) && Object.keys(include).length === 0)) {
// The objects are empty
return process.nextTick(function() {
cb && cb(null, objects);
});
2014-01-24 17:09:53 +00:00
}
2014-03-13 23:43:38 +00:00
include = normalizeInclude(include);
debug('include: %j', include);
2015-05-13 23:10:07 +00:00
// Find the limit of items for `inq`
2018-12-07 16:13:48 +00:00
let inqLimit = 256;
if (self.dataSource && self.dataSource.settings &&
self.dataSource.settings.inqLimit) {
inqLimit = self.dataSource.settings.inqLimit;
}
2014-03-13 23:43:38 +00:00
async.each(include, function(item, callback) {
try {
processIncludeItem(objects, item, options, callback);
} catch (err) {
// async does not catch the error and report to the outer callback
callback(err);
}
2014-03-13 23:43:38 +00:00
}, function(err) {
debug(err, objects);
2014-03-13 23:43:38 +00:00
cb && cb(err, objects);
});
/**
* Find related items with an array of foreign keys by page
* @param model The model class
* @param filter The query filter
* @param fkName The name of the foreign key property
* @param pageSize The size of page
* @param options Options
* @param cb
*/
function findWithForeignKeysByPage(model, filter, fkName, pageSize, options, cb) {
try {
2018-11-12 21:54:22 +00:00
const opts = Object.assign({prohibitProtectedPropertiesInQuery: true}, options);
model._sanitizeQuery(filter.where, opts);
model._coerce(filter.where, options);
} catch (e) {
return cb(e);
}
2018-12-07 16:13:48 +00:00
let foreignKeys = [];
if (filter.where[fkName]) {
foreignKeys = filter.where[fkName].inq;
} else if (filter.where.and) {
// The inq can be embedded inside 'and: []'. No or: [] is needed as
// include only uses and. We only deal with the generated inq for include.
2018-12-07 16:13:48 +00:00
for (const j in filter.where.and) {
if (filter.where.and[j][fkName] &&
Array.isArray(filter.where.and[j][fkName].inq)) {
foreignKeys = filter.where.and[j][fkName].inq;
break;
}
}
}
if (!foreignKeys.length) {
return cb(null, []);
}
if (filter.limit || filter.skip || filter.offset) {
// Force the find to be performed per FK to honor the pagination
pageSize = 1;
}
2018-12-07 16:13:48 +00:00
const size = foreignKeys.length;
if (size > inqLimit && pageSize <= 0) {
pageSize = inqLimit;
}
if (pageSize <= 0) {
return model.find(filter, options, cb);
}
2018-12-07 16:13:48 +00:00
let listOfFKs = [];
2018-12-07 16:13:48 +00:00
for (let i = 0; i < size; i += pageSize) {
let end = i + pageSize;
if (end > size) {
end = size;
}
listOfFKs.push(foreignKeys.slice(i, end));
}
2018-12-07 16:13:48 +00:00
let items = [];
// Optimization: no need to resolve keys that are an empty array
listOfFKs = listOfFKs.filter(function(keys) {
return keys.length > 0;
});
async.each(listOfFKs, function(foreignKeys, done) {
2018-12-07 16:13:48 +00:00
const newFilter = {};
for (const f in filter) {
newFilter[f] = filter[f];
}
if (filter.where) {
newFilter.where = {};
2018-12-07 16:13:48 +00:00
for (const w in filter.where) {
newFilter.where[w] = filter.where[w];
}
}
newFilter.where[fkName] = foreignKeys.length === 1 ? foreignKeys[0] : {
inq: foreignKeys,
};
model.find(newFilter, options, function(err, results) {
if (err) return done(err);
items = items.concat(results);
done();
});
}, function(err) {
if (err) return cb(err);
cb(null, items);
});
}
function processIncludeItem(objs, include, options, cb) {
2018-12-07 16:13:48 +00:00
const relations = self.relations;
2015-05-13 23:10:07 +00:00
2018-12-07 16:13:48 +00:00
let relationName;
let subInclude = null, scope = null;
2015-05-13 23:10:07 +00:00
if (isPlainObject(include)) {
relationName = Object.keys(include)[0];
2014-10-10 10:28:39 +00:00
if (include[relationName] instanceof IncludeScope) {
scope = include[relationName];
subInclude = scope.include();
} else {
subInclude = include[relationName];
// when include = {user:true}, it does not have subInclude
if (subInclude === true) {
subInclude = null;
}
2014-10-10 10:28:39 +00:00
}
2016-04-01 13:23:42 +00:00
} else {
relationName = include;
2014-03-13 23:43:38 +00:00
subInclude = null;
2014-01-24 17:09:53 +00:00
}
2015-05-13 23:10:07 +00:00
2018-12-07 16:13:48 +00:00
const relation = relations[relationName];
2014-01-24 17:09:53 +00:00
if (!relation) {
2016-07-22 19:26:07 +00:00
cb(new Error(g.f('Relation "%s" is not defined for %s model', relationName, self.modelName)));
return;
2013-04-11 23:23:34 +00:00
}
debug('Relation: %j', relation);
2018-12-07 16:13:48 +00:00
const 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) {
2016-08-08 19:34:41 +00:00
cb(new Error(g.f('{{Relation.modelTo}} is not defined for relation %s and is no {{polymorphic}}',
2016-07-22 19:26:07 +00:00
relationName)));
return;
}
}
2013-04-11 23:23:34 +00:00
// Just skip if inclusion is disabled
if (relation.options.disableInclude) {
debug('Relation is disabled from include', relation);
return cb();
}
// prepare filter and fields for making DB Call
2018-12-07 16:13:48 +00:00
const filter = (scope && scope.conditions()) || {};
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
2018-12-07 16:13:48 +00:00
const includeScope = {};
// make sure not to miss any fields for sub includes
if (filter.fields && Array.isArray(subInclude) &&
relation.modelTo.relations) {
includeScope.fields = [];
2016-04-01 11:48:17 +00:00
subInclude.forEach(function(name) {
2018-12-07 16:13:48 +00:00
const rel = relation.modelTo.relations[name];
if (rel && rel.type === 'belongsTo') {
includeScope.fields.push(rel.keyFrom);
}
});
}
2016-08-19 17:46:59 +00:00
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
2018-12-07 16:13:48 +00:00
let fields = filter.fields;
if (typeof fields === 'string') {
// transform string into array containing this string
filter.fields = fields = [fields];
}
if (Array.isArray(fields) && fields.indexOf(relation.keyTo) === -1) {
fields.push(relation.keyTo);
2016-04-01 13:23:42 +00:00
} 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);
}
2015-08-24 13:07:43 +00:00
// This handles exactly hasMany. Fast and straightforward. Without parallel, each and other boilerplate.
2016-04-01 11:48:17 +00:00
if (relation.type === 'hasMany' && relation.multiple && !subInclude) {
2015-08-24 13:07:43 +00:00
return includeHasManySimple(cb);
}
// assuming all other relations with multiple=true as hasMany
return includeHasMany(cb);
2016-04-01 13:23:42 +00:00
} else {
if (polymorphic) {
if (relation.type === 'hasOne') {
return includePolymorphicHasOne(cb);
}
return includePolymorphicBelongsTo(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) {
2018-12-07 16:13:48 +00:00
const sourceIds = [];
// Map for Indexing objects by their id for faster retrieval
2018-12-07 16:13:48 +00:00
const objIdMap = {};
for (let i = 0; i < objs.length; i++) {
const obj = objs[i];
// one-to-many: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
2018-12-07 16:13:48 +00:00
const 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.
2018-12-07 16:13:48 +00:00
const throughFilter = {
2016-04-01 11:48:17 +00:00
where: {},
};
throughFilter.where[relation.keyTo] = {
2016-04-01 11:48:17 +00:00
inq: uniq(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
findWithForeignKeysByPage(relation.modelThrough, throughFilter,
relation.keyTo, 0, options, 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.
2018-12-07 16:13:48 +00:00
const targetIds = [];
const targetObjsMap = {};
for (let i = 0; i < throughObjs.length; i++) {
const throughObj = throughObjs[i];
const targetId = throughObj[relation.keyThrough];
if (targetId) {
// save targetIds for 2nd DB Call
targetIds.push(targetId);
2018-12-07 16:13:48 +00:00
const sourceObj = objIdMap[throughObj[relation.keyTo]];
const targetIdStr = targetId.toString();
// Since targetId can be duplicates, multiple source objs are put
// into buckets.
2018-12-07 16:13:48 +00:00
const objList = targetObjsMap[targetIdStr] =
targetObjsMap[targetIdStr] || [];
objList.push(sourceObj);
}
}
// Polymorphic relation does not have idKey of modelTo. Find it manually
2018-12-07 16:13:48 +00:00
const modelToIdName = idName(relation.modelTo);
filter.where[modelToIdName] = {
2016-04-01 11:48:17 +00:00
inq: uniq(targetIds),
};
// make sure that the modelToIdName is included if fields are specified
if (Array.isArray(fields) && fields.indexOf(modelToIdName) === -1) {
fields.push(modelToIdName);
2016-04-01 13:23:42 +00:00
} else if (isPlainObject(fields) && !fields[modelToIdName]) {
fields[modelToIdName] = true;
}
// 2nd DB Call of 2-step process. Get modelTo (target) objects
findWithForeignKeysByPage(relation.modelTo, filter,
modelToIdName, 0, options, targetsFetchHandler);
// relation.modelTo.find(filter, options, targetsFetchHandler);
function targetsFetchHandler(err, targets) {
2015-05-13 23:10:07 +00:00
if (err) {
return callback(err);
}
2018-12-07 16:13:48 +00:00
const 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, options, next);
});
}
// process & link each target with object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkManyToMany, next);
function linkManyToMany(target, next) {
2018-12-07 16:13:48 +00:00
const targetId = target[modelToIdName];
if (!targetId) {
const err = new Error(g.f(
'LinkManyToMany received target that doesn\'t contain required "%s"',
modelToIdName
));
return next(err);
}
2018-12-07 16:13:48 +00:00
const objList = targetObjsMap[targetId.toString()];
2016-04-01 11:48:17 +00:00
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName].push(target);
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
}
/**
* Handle inclusion of ReferencesMany relation
* @param callback
*/
function includeReferencesMany(callback) {
2018-12-07 16:13:48 +00:00
const modelToIdName = idName(relation.modelTo);
let allTargetIds = [];
// Map for Indexing objects by their id for faster retrieval
2018-12-07 16:13:48 +00:00
const targetObjsMap = {};
for (let i = 0; i < objs.length; i++) {
const obj = objs[i];
// one-to-many: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
2018-12-07 16:13:48 +00:00
let targetIds = obj[relation.keyFrom];
if (targetIds) {
2015-05-13 23:10:07 +00:00
if (typeof targetIds === 'string') {
// For relational DBs, the array is stored as stringified json
// Please note obj is a plain object at this point
targetIds = JSON.parse(targetIds);
}
// referencesMany has multiple targetIds per obj. We need to concat
// them into allTargetIds before DB Call
allTargetIds = allTargetIds.concat(targetIds);
2018-12-07 16:13:48 +00:00
for (let j = 0; j < targetIds.length; j++) {
const targetId = targetIds[j];
const targetIdStr = targetId.toString();
const 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] = {
2016-04-01 11:48:17 +00:00
inq: uniq(allTargetIds),
};
relation.applyScope(null, filter);
/**
* Make the DB Call, fetch all target objects
*/
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Handle the fetched target objects
* @param err
* @param {Array<Model>}targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
2018-12-07 16:13:48 +00:00
const tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
2015-06-30 18:48:09 +00:00
targets = utils.sortObjectsByIds(modelToIdName, allTargetIds, targets);
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkManyToMany, next);
function linkManyToMany(target, next) {
2018-12-07 16:13:48 +00:00
const objList = targetObjsMap[target[relation.keyTo].toString()];
2016-04-01 11:48:17 +00:00
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName].push(target);
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
2015-08-24 13:07:43 +00:00
/**
* Handle inclusion of HasMany relation
* @param callback
*/
function includeHasManySimple(callback) {
// Map for Indexing objects by their id for faster retrieval
2018-12-07 16:13:48 +00:00
const objIdMap2 = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, relation.keyFrom);
2015-08-24 13:07:43 +00:00
filter.where[relation.keyTo] = {
2016-04-01 11:48:17 +00:00
inq: uniq(objIdMap2.getKeys()),
2015-08-24 13:07:43 +00:00
};
relation.applyScope(null, filter);
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
2015-08-24 13:07:43 +00:00
2015-08-24 17:45:41 +00:00
function targetFetchHandler(err, targets) {
2016-04-01 11:48:17 +00:00
if (err) {
2015-08-24 13:07:43 +00:00
return callback(err);
}
2018-12-07 16:13:48 +00:00
const targetsIdMap = includeUtils.buildOneToManyIdentityMapWithOrigKeys(targets, relation.keyTo);
2016-04-01 11:48:17 +00:00
includeUtils.join(objIdMap2, targetsIdMap, function(obj1, valueToMergeIn) {
2015-08-24 17:45:41 +00:00
defineCachedRelations(obj1);
obj1.__cachedRelations[relationName] = valueToMergeIn;
2016-04-01 11:48:17 +00:00
processTargetObj(obj1, function() {});
2015-08-24 13:07:43 +00:00
});
callback(err, objs);
}
}
/**
* Handle inclusion of HasMany relation
* @param callback
*/
function includeHasMany(callback) {
2018-12-07 16:13:48 +00:00
const sourceIds = [];
// Map for Indexing objects by their id for faster retrieval
2018-12-07 16:13:48 +00:00
const objIdMap = {};
for (let i = 0; i < objs.length; i++) {
const obj = objs[i];
// one-to-many: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
2018-12-07 16:13:48 +00:00
const 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] = {
2016-04-01 11:48:17 +00:00
inq: uniq(sourceIds),
};
relation.applyScope(null, filter);
options.partitionBy = relation.keyTo;
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
2018-12-07 16:13:48 +00:00
const tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
if (targets.length === 0) {
return async.each(objs, function(obj, next) {
processTargetObj(obj, next);
}, next);
}
async.each(targets, linkManyToOne, next);
function linkManyToOne(target, next) {
// fix for bug in hasMany with referencesMany
2018-12-07 16:13:48 +00:00
const targetIds = [].concat(target[relation.keyTo]);
2016-04-01 11:48:17 +00:00
async.each(targetIds, function(targetId, next) {
2018-12-07 16:13:48 +00:00
const obj = objIdMap[targetId.toString()];
if (!obj) return next();
obj.__cachedRelations[relationName].push(target);
processTargetObj(obj, next);
}, function(err, processedTargets) {
if (err) {
return next(err);
}
2018-12-07 16:13:48 +00:00
const objsWithEmptyRelation = objs.filter(function(obj) {
return obj.__cachedRelations[relationName].length === 0;
});
async.each(objsWithEmptyRelation, function(obj, next) {
processTargetObj(obj, next);
}, function(err) {
next(err, processedTargets);
});
});
}
}
execTasksWithInterLeave(tasks, callback);
}
}
/**
* Handle Inclusion of Polymorphic BelongsTo relation
* @param callback
*/
function includePolymorphicBelongsTo(callback) {
2018-12-07 16:13:48 +00:00
const targetIdsByType = {};
// Map for Indexing objects by their type and targetId for faster retrieval
2018-12-07 16:13:48 +00:00
const targetObjMapByType = {};
for (let i = 0; i < objs.length; i++) {
const obj = objs[i];
const discriminator = polymorphic.discriminator;
const modelType = obj[discriminator];
if (modelType) {
targetIdsByType[modelType] = targetIdsByType[modelType] || [];
targetObjMapByType[modelType] = targetObjMapByType[modelType] || {};
2018-12-07 16:13:48 +00:00
const targetIds = targetIdsByType[modelType];
const targetObjsMap = targetObjMapByType[modelType];
const targetId = obj[relation.keyFrom];
if (targetId) {
targetIds.push(targetId);
2018-12-07 16:13:48 +00:00
const 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) {
2018-12-07 16:13:48 +00:00
const typeFilter = {where: {}};
utils.mergeQuery(typeFilter, filter);
2018-12-07 16:13:48 +00:00
const targetIds = targetIdsByType[modelType];
typeFilter.where[relation.keyTo] = {
2016-04-01 11:48:17 +00:00
inq: uniq(targetIds),
};
2018-12-07 16:13:48 +00:00
const Model = lookupModel(relation.modelFrom.dataSource.modelBuilder.
models, modelType);
if (!Model) {
2016-07-22 19:26:07 +00:00
callback(new Error(g.f('Discriminator type %s specified but no model exists with such name',
modelType)));
return;
}
relation.applyScope(null, typeFilter);
findWithForeignKeysByPage(Model, typeFilter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
2018-12-07 16:13:48 +00:00
const tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
Model.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
2018-12-07 16:13:48 +00:00
const targetObjsMap = targetObjMapByType[modelType];
async.each(targets, linkOneToMany, next);
function linkOneToMany(target, next) {
2018-12-07 16:13:48 +00:00
const objList = targetObjsMap[target[relation.keyTo].toString()];
2016-04-01 11:48:17 +00:00
async.each(objList, function(obj, next) {
if (!obj) return next();
obj.__cachedRelations[relationName] = target;
processTargetObj(obj, next);
}, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
}
/**
* Handle Inclusion of Polymorphic HasOne relation
* @param callback
*/
function includePolymorphicHasOne(callback) {
2018-12-07 16:13:48 +00:00
const sourceIds = [];
// Map for Indexing objects by their id for faster retrieval
2018-12-07 16:13:48 +00:00
const objIdMap = {};
for (let i = 0; i < objs.length; i++) {
const obj = objs[i];
// one-to-one: foreign key reference is modelTo -> modelFrom.
// use modelFrom.keyFrom in where filter later
2018-12-07 16:13:48 +00:00
const 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] = null;
}
filter.where[relation.keyTo] = {
2016-04-01 11:48:17 +00:00
inq: uniq(sourceIds),
};
relation.applyScope(null, filter);
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
2018-12-07 16:13:48 +00:00
const tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkOneToOne, next);
function linkOneToOne(target, next) {
2018-12-07 16:13:48 +00:00
const sourceId = target[relation.keyTo];
if (!sourceId) return next();
2018-12-07 16:13:48 +00:00
const obj = objIdMap[sourceId.toString()];
if (!obj) return next();
obj.__cachedRelations[relationName] = target;
processTargetObj(obj, next);
}
}
execTasksWithInterLeave(tasks, callback);
}
}
/**
* Handle Inclusion of BelongsTo/HasOne relation
* @param callback
*/
function includeOneToOne(callback) {
2018-12-07 16:13:48 +00:00
const targetIds = [];
const objTargetIdMap = {};
for (let i = 0; i < objs.length; i++) {
const obj = objs[i];
if (relation.type === 'belongsTo') {
if (obj[relation.keyFrom] == null) {
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = null;
debug('ID property "%s" is missing in item %j', relation.keyFrom, obj);
continue;
}
}
2018-12-07 16:13:48 +00:00
const targetId = obj[relation.keyFrom];
if (targetId) {
targetIds.push(targetId);
2018-12-07 16:13:48 +00:00
const targetIdStr = targetId.toString();
objTargetIdMap[targetIdStr] = objTargetIdMap[targetIdStr] || [];
objTargetIdMap[targetIdStr].push(obj);
} else {
debug('ID property "%s" is missing in item %j', relation.keyFrom, obj);
}
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = null;
}
filter.where[relation.keyTo] = {
2016-04-01 11:48:17 +00:00
inq: uniq(targetIds),
};
relation.applyScope(null, filter);
findWithForeignKeysByPage(relation.modelTo, filter,
relation.keyTo, 0, options, targetFetchHandler);
/**
* Process fetched related objects
* @param err
* @param {Array<Model>} targets
* @returns {*}
*/
function targetFetchHandler(err, targets) {
if (err) {
return callback(err);
}
2018-12-07 16:13:48 +00:00
const tasks = [];
// simultaneously process subIncludes
if (subInclude && targets) {
tasks.push(function subIncludesTask(next) {
relation.modelTo.include(targets, subInclude, options, next);
});
}
// process each target object
tasks.push(targetLinkingTask);
function targetLinkingTask(next) {
async.each(targets, linkOneToMany, next);
function linkOneToMany(target, next) {
2018-12-07 16:13:48 +00:00
const targetId = target[relation.keyTo];
const objList = objTargetIdMap[targetId.toString()];
2016-04-01 11:48:17 +00:00
async.each(objList, function(obj, next) {
if (!obj) return 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) {
2016-04-01 11:48:17 +00:00
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) {
2018-12-07 16:13:48 +00:00
const isInst = obj instanceof self;
// Calling the relation method on the instance
if (relation.type === 'belongsTo') {
2014-03-13 23:43:38 +00:00
// If the belongsTo relation doesn't have an owner
if (obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
2014-03-13 23:43:38 +00:00
defineCachedRelations(obj);
// Set to null if the owner doesn't exist
obj.__cachedRelations[relationName] = null;
if (isInst) {
obj.__data[relationName] = null;
} else {
obj[relationName] = null;
}
2014-03-13 23:43:38 +00:00
return callback();
2013-04-11 23:23:34 +00:00
}
2014-01-24 17:09:53 +00:00
}
/**
* Sets the related objects as a property of Parent Object
* @param {Array<Model>|Model|null} result - Related Object/Objects
* @param cb
*/
function setIncludeData(result, cb) {
if (isInst) {
if (Array.isArray(result) && !(result instanceof List)) {
result = new List(result, relation.modelTo);
}
obj.__data[relationName] = result;
// obj.setStrict(false); issue #1252
} 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);
}
2018-12-07 16:13:48 +00:00
const inst = (obj instanceof self) ? obj : new self(obj);
// If related objects are not cached by include Handlers, directly call
// related accessor function even though it is not very efficient
2018-12-07 16:13:48 +00:00
let related; // relation accessor function
2015-05-13 23:10:07 +00:00
2014-10-14 20:47:59 +00:00
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
2018-12-07 16:13:48 +00:00
const includeScope = {};
const filter = scope.conditions();
2015-05-13 23:10:07 +00:00
2014-10-10 10:28:39 +00:00
// 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) {
2018-12-07 16:13:48 +00:00
const rel = relation.modelTo.relations[name];
2014-10-10 10:28:39 +00:00
if (rel && rel.type === 'belongsTo') {
includeScope.fields.push(rel.keyFrom);
}
});
}
2015-05-13 23:10:07 +00:00
2016-08-19 17:46:59 +00:00
utils.mergeQuery(filter, includeScope, {fields: false});
2015-05-13 23:10:07 +00:00
2014-10-10 10:28:39 +00:00
related = inst[relationName].bind(inst, filter);
} else {
related = inst[relationName].bind(inst, undefined);
2014-10-10 10:28:39 +00:00
}
2015-05-13 23:10:07 +00:00
2016-04-01 11:48:17 +00:00
related(options, function(err, result) {
2014-03-13 23:43:38 +00:00
if (err) {
return callback(err);
} else {
defineCachedRelations(obj);
obj.__cachedRelations[relationName] = result;
2015-05-13 23:10:07 +00:00
return setIncludeData(result, callback);
2014-03-13 23:43:38 +00:00
}
});
}
2014-01-24 17:09:53 +00:00
}
};