UNPKG

35.9 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const async = require('async');
9const g = require('strong-globalize')();
10const utils = require('./utils');
11const List = require('./list');
12const includeUtils = require('./include_utils');
13const isPlainObject = utils.isPlainObject;
14const defineCachedRelations = utils.defineCachedRelations;
15const uniq = utils.uniq;
16const idName = utils.idName;
17const debug = require('debug')('loopback:include');
18
19const DISALLOWED_TYPES = ['boolean', 'number', 'symbol', 'function'];
20
21/*!
22 * Normalize the include to be an array
23 * @param include
24 * @returns {*}
25 */
26function normalizeInclude(include) {
27 let newInclude;
28 if (typeof include === 'string') {
29 return [include];
30 } else if (isPlainObject(include)) {
31 // Build an array of key/value pairs
32 newInclude = [];
33 const rel = include.rel || include.relation;
34 const obj = {};
35 if (typeof rel === 'string') {
36 obj[rel] = new IncludeScope(include.scope);
37 newInclude.push(obj);
38 } else {
39 for (const key in include) {
40 obj[key] = include[key];
41 newInclude.push(obj);
42 }
43 }
44 return newInclude;
45 } else if (Array.isArray(include)) {
46 newInclude = [];
47 for (let i = 0, n = include.length; i < n; i++) {
48 const subIncludes = normalizeInclude(include[i]);
49 newInclude = newInclude.concat(subIncludes);
50 }
51 return newInclude;
52 } else if (DISALLOWED_TYPES.includes(typeof include)) {
53 debug('Ignoring invalid "include" value of type %s:', typeof include, include);
54 return [];
55 } else {
56 return include;
57 }
58}
59
60function IncludeScope(scope) {
61 this._scope = utils.deepMerge({}, scope || {});
62 if (this._scope.include) {
63 this._include = normalizeInclude(this._scope.include);
64 delete this._scope.include;
65 } else {
66 this._include = null;
67 }
68}
69
70IncludeScope.prototype.conditions = function() {
71 return utils.deepMerge({}, this._scope);
72};
73
74IncludeScope.prototype.include = function() {
75 return this._include;
76};
77
78/*!
79 * Look up a model by name from the list of given models
80 * @param {Object} models Models keyed by name
81 * @param {String} modelName The model name
82 * @returns {*} The matching model class
83 */
84function lookupModel(models, modelName) {
85 if (models[modelName]) {
86 return models[modelName];
87 }
88 const lookupClassName = modelName.toLowerCase();
89 for (const name in models) {
90 if (name.toLowerCase() === lookupClassName) {
91 return models[name];
92 }
93 }
94}
95
96/**
97 * Utility Function to allow interleave before and after high computation tasks
98 * @param tasks
99 * @param callback
100 */
101function execTasksWithInterLeave(tasks, callback) {
102 // let's give others some time to process.
103 // Context Switch BEFORE Heavy Computation
104 process.nextTick(function() {
105 // Heavy Computation
106 try {
107 async.parallel(tasks, function(err, info) {
108 // Context Switch AFTER Heavy Computation
109 process.nextTick(function() {
110 callback(err, info);
111 });
112 });
113 } catch (err) {
114 callback(err);
115 }
116 });
117}
118
119/*!
120 * Include mixin for ./model.js
121 */
122module.exports = Inclusion;
123
124/**
125 * Inclusion - Model mixin.
126 *
127 * @class
128 */
129
130function Inclusion() {
131}
132
133/**
134 * Normalize includes - used in DataAccessObject
135 *
136 */
137
138Inclusion.normalizeInclude = normalizeInclude;
139
140/**
141 * Enables you to load relations of several objects and optimize numbers of requests.
142 *
143 * Examples:
144 *
145 * Load all users' posts with only one additional request:
146 * `User.include(users, 'posts', function() {});`
147 * Or
148 * `User.include(users, ['posts'], function() {});`
149 *
150 * Load all users posts and passports with two additional requests:
151 * `User.include(users, ['posts', 'passports'], function() {});`
152 *
153 * Load all passports owner (users), and all posts of each owner loaded:
154 *```Passport.include(passports, {owner: 'posts'}, function() {});
155 *``` Passport.include(passports, {owner: ['posts', 'passports']});
156 *``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']});
157 *
158 * @param {Array} objects Array of instances
159 * @param {String|Object|Array} include Which relations to load.
160 * @param {Object} [options] Options for CRUD
161 * @param {Function} cb Callback called when relations are loaded
162 *
163 */
164Inclusion.include = function(objects, include, options, cb) {
165 if (typeof options === 'function' && cb === undefined) {
166 cb = options;
167 options = {};
168 }
169 const self = this;
170
171 if (!include || (Array.isArray(include) && include.length === 0) ||
172 (Array.isArray(objects) && objects.length === 0) ||
173 (isPlainObject(include) && Object.keys(include).length === 0)) {
174 // The objects are empty
175 return process.nextTick(function() {
176 cb && cb(null, objects);
177 });
178 }
179
180 include = normalizeInclude(include);
181 debug('include: %j', include);
182
183 // Find the limit of items for `inq`
184 let inqLimit = 256;
185 if (self.dataSource && self.dataSource.settings &&
186 self.dataSource.settings.inqLimit) {
187 inqLimit = self.dataSource.settings.inqLimit;
188 }
189
190 async.each(include, function(item, callback) {
191 try {
192 processIncludeItem(objects, item, options, callback);
193 } catch (err) {
194 // async does not catch the error and report to the outer callback
195 callback(err);
196 }
197 }, function(err) {
198 debug(err, objects);
199 cb && cb(err, objects);
200 });
201
202 /**
203 * Find related items with an array of foreign keys by page
204 * @param model The model class
205 * @param filter The query filter
206 * @param fkName The name of the foreign key property
207 * @param pageSize The size of page
208 * @param options Options
209 * @param cb
210 */
211 function findWithForeignKeysByPage(model, filter, fkName, pageSize, options, cb) {
212 try {
213 const opts = Object.assign({prohibitProtectedPropertiesInQuery: true}, options);
214 model._sanitizeQuery(filter.where, opts);
215 model._coerce(filter.where, options);
216 } catch (e) {
217 return cb(e);
218 }
219
220 let foreignKeys = [];
221 if (filter.where[fkName]) {
222 foreignKeys = filter.where[fkName].inq;
223 } else if (filter.where.and) {
224 // The inq can be embedded inside 'and: []'. No or: [] is needed as
225 // include only uses and. We only deal with the generated inq for include.
226 for (const j in filter.where.and) {
227 if (filter.where.and[j][fkName] &&
228 Array.isArray(filter.where.and[j][fkName].inq)) {
229 foreignKeys = filter.where.and[j][fkName].inq;
230 break;
231 }
232 }
233 }
234 if (!foreignKeys.length) {
235 return cb(null, []);
236 }
237 if (filter.limit || filter.skip || filter.offset) {
238 // Force the find to be performed per FK to honor the pagination
239 pageSize = 1;
240 }
241 const size = foreignKeys.length;
242 if (size > inqLimit && pageSize <= 0) {
243 pageSize = inqLimit;
244 }
245 if (pageSize <= 0) {
246 return model.find(filter, options, cb);
247 }
248
249 let listOfFKs = [];
250
251 for (let i = 0; i < size; i += pageSize) {
252 let end = i + pageSize;
253 if (end > size) {
254 end = size;
255 }
256 listOfFKs.push(foreignKeys.slice(i, end));
257 }
258
259 let items = [];
260 // Optimization: no need to resolve keys that are an empty array
261 listOfFKs = listOfFKs.filter(function(keys) {
262 return keys.length > 0;
263 });
264 async.each(listOfFKs, function(foreignKeys, done) {
265 const newFilter = {};
266 for (const f in filter) {
267 newFilter[f] = filter[f];
268 }
269 if (filter.where) {
270 newFilter.where = {};
271 for (const w in filter.where) {
272 newFilter.where[w] = filter.where[w];
273 }
274 }
275 newFilter.where[fkName] = foreignKeys.length === 1 ? foreignKeys[0] : {
276 inq: foreignKeys,
277 };
278 model.find(newFilter, options, function(err, results) {
279 if (err) return done(err);
280 items = items.concat(results);
281 done();
282 });
283 }, function(err) {
284 if (err) return cb(err);
285 cb(null, items);
286 });
287 }
288
289 function processIncludeItem(objs, include, options, cb) {
290 const relations = self.relations;
291
292 let relationName;
293 let subInclude = null, scope = null;
294
295 if (isPlainObject(include)) {
296 relationName = Object.keys(include)[0];
297 if (include[relationName] instanceof IncludeScope) {
298 scope = include[relationName];
299 subInclude = scope.include();
300 } else {
301 subInclude = include[relationName];
302 // when include = {user:true}, it does not have subInclude
303 if (subInclude === true) {
304 subInclude = null;
305 }
306 }
307 } else {
308 relationName = include;
309 subInclude = null;
310 }
311
312 const relation = relations[relationName];
313 if (!relation) {
314 cb(new Error(g.f('Relation "%s" is not defined for %s model', relationName, self.modelName)));
315 return;
316 }
317 debug('Relation: %j', relation);
318 const polymorphic = relation.polymorphic;
319 // if (polymorphic && !polymorphic.discriminator) {
320 // cb(new Error('Relation "' + relationName + '" is polymorphic but ' +
321 // 'discriminator is not present'));
322 // return;
323 // }
324 if (!relation.modelTo) {
325 if (!relation.polymorphic) {
326 cb(new Error(g.f('{{Relation.modelTo}} is not defined for relation %s and is no {{polymorphic}}',
327 relationName)));
328 return;
329 }
330 }
331
332 // Just skip if inclusion is disabled
333 if (relation.options.disableInclude) {
334 debug('Relation is disabled from include', relation);
335 return cb();
336 }
337 // prepare filter and fields for making DB Call
338 const filter = (scope && scope.conditions()) || {};
339 if ((relation.multiple || relation.type === 'belongsTo') && scope) {
340 const includeScope = {};
341 // make sure not to miss any fields for sub includes
342 if (filter.fields && Array.isArray(subInclude) &&
343 relation.modelTo.relations) {
344 includeScope.fields = [];
345 subInclude.forEach(function(name) {
346 const rel = relation.modelTo.relations[name];
347 if (rel && rel.type === 'belongsTo') {
348 includeScope.fields.push(rel.keyFrom);
349 }
350 });
351 }
352 utils.mergeQuery(filter, includeScope, {fields: false});
353 }
354 // Let's add a placeholder where query
355 filter.where = filter.where || {};
356 // if fields are specified, make sure target foreign key is present
357 let fields = filter.fields;
358 if (typeof fields === 'string') {
359 // transform string into array containing this string
360 filter.fields = fields = [fields];
361 }
362 if (Array.isArray(fields) && fields.indexOf(relation.keyTo) === -1) {
363 fields.push(relation.keyTo);
364 } else if (isPlainObject(fields) && !fields[relation.keyTo]) {
365 fields[relation.keyTo] = true;
366 }
367
368 //
369 // call relation specific include functions
370 //
371 if (relation.multiple) {
372 if (relation.modelThrough) {
373 // hasManyThrough needs separate handling
374 return includeHasManyThrough(cb);
375 }
376 // This will also include embedsMany with belongsTo.
377 // Might need to optimize db calls for this.
378 if (relation.type === 'embedsMany') {
379 // embedded docs are part of the objects, no need to make db call.
380 // proceed as implemented earlier.
381 return includeEmbeds(cb);
382 }
383 if (relation.type === 'referencesMany') {
384 return includeReferencesMany(cb);
385 }
386
387 // This handles exactly hasMany. Fast and straightforward. Without parallel, each and other boilerplate.
388 if (relation.type === 'hasMany' && relation.multiple && !subInclude) {
389 return includeHasManySimple(cb);
390 }
391 // assuming all other relations with multiple=true as hasMany
392 return includeHasMany(cb);
393 } else {
394 if (polymorphic) {
395 if (relation.type === 'hasOne') {
396 return includePolymorphicHasOne(cb);
397 }
398 return includePolymorphicBelongsTo(cb);
399 }
400 if (relation.type === 'embedsOne') {
401 return includeEmbeds(cb);
402 }
403 // hasOne or belongsTo
404 return includeOneToOne(cb);
405 }
406
407 /**
408 * Handle inclusion of HasManyThrough/HasAndBelongsToMany/Polymorphic
409 * HasManyThrough relations
410 * @param callback
411 */
412 function includeHasManyThrough(callback) {
413 const sourceIds = [];
414 // Map for Indexing objects by their id for faster retrieval
415 const objIdMap = {};
416 for (let i = 0; i < objs.length; i++) {
417 const obj = objs[i];
418 // one-to-many: foreign key reference is modelTo -> modelFrom.
419 // use modelFrom.keyFrom in where filter later
420 const sourceId = obj[relation.keyFrom];
421 if (sourceId) {
422 sourceIds.push(sourceId);
423 objIdMap[sourceId.toString()] = obj;
424 }
425 // sourceId can be null. but cache empty data as result
426 defineCachedRelations(obj);
427 obj.__cachedRelations[relationName] = [];
428 }
429 // default filters are not applicable on through model. should be applied
430 // on modelTo later in 2nd DB call.
431 const throughFilter = {
432 where: {},
433 };
434 throughFilter.where[relation.keyTo] = {
435 inq: uniq(sourceIds),
436 };
437 if (polymorphic) {
438 // handle polymorphic hasMany (reverse) in which case we need to filter
439 // by discriminator to filter other types
440 const throughModel = polymorphic.invert ? relation.modelTo : relation.modelFrom;
441 throughFilter.where[polymorphic.discriminator] =
442 throughModel.definition.name;
443 }
444
445 // 1st DB Call of 2-step process. Get through model objects first
446 findWithForeignKeysByPage(relation.modelThrough, throughFilter,
447 relation.keyTo, 0, options, throughFetchHandler);
448
449 /**
450 * Handle the results of Through model objects and fetch the modelTo items
451 * @param err
452 * @param {Array<Model>} throughObjs
453 * @returns {*}
454 */
455 function throughFetchHandler(err, throughObjs) {
456 if (err) {
457 return callback(err);
458 }
459 // start preparing for 2nd DB call.
460 const targetIds = [];
461 const targetObjsMap = {};
462 for (let i = 0; i < throughObjs.length; i++) {
463 const throughObj = throughObjs[i];
464 const targetId = throughObj[relation.keyThrough];
465 if (targetId) {
466 // save targetIds for 2nd DB Call
467 targetIds.push(targetId);
468 const sourceObj = objIdMap[throughObj[relation.keyTo]];
469 const targetIdStr = targetId.toString();
470 // Since targetId can be duplicates, multiple source objs are put
471 // into buckets.
472 const objList = targetObjsMap[targetIdStr] =
473 targetObjsMap[targetIdStr] || [];
474 objList.push(sourceObj);
475 }
476 }
477 // Polymorphic relation does not have idKey of modelTo. Find it manually
478 const modelToIdName = idName(relation.modelTo);
479 filter.where[modelToIdName] = {
480 inq: uniq(targetIds),
481 };
482
483 // make sure that the modelToIdName is included if fields are specified
484 if (Array.isArray(fields) && fields.indexOf(modelToIdName) === -1) {
485 fields.push(modelToIdName);
486 } else if (isPlainObject(fields) && !fields[modelToIdName]) {
487 fields[modelToIdName] = true;
488 }
489
490 // 2nd DB Call of 2-step process. Get modelTo (target) objects
491 findWithForeignKeysByPage(relation.modelTo, filter,
492 modelToIdName, 0, options, targetsFetchHandler);
493
494 // relation.modelTo.find(filter, options, targetsFetchHandler);
495 function targetsFetchHandler(err, targets) {
496 if (err) {
497 return callback(err);
498 }
499 const tasks = [];
500 // simultaneously process subIncludes. Call it first as it is an async
501 // process.
502 if (subInclude && targets) {
503 tasks.push(function subIncludesTask(next) {
504 relation.modelTo.include(targets, subInclude, options, next);
505 });
506 }
507 // process & link each target with object
508 tasks.push(targetLinkingTask);
509 function targetLinkingTask(next) {
510 async.each(targets, linkManyToMany, next);
511 function linkManyToMany(target, next) {
512 const targetId = target[modelToIdName];
513 if (!targetId) {
514 const err = new Error(g.f(
515 'LinkManyToMany received target that doesn\'t contain required "%s"',
516 modelToIdName,
517 ));
518 return next(err);
519 }
520 const objList = targetObjsMap[targetId.toString()];
521 async.each(objList, function(obj, next) {
522 if (!obj) return next();
523 obj.__cachedRelations[relationName].push(target);
524 processTargetObj(obj, next);
525 }, next);
526 }
527 }
528
529 execTasksWithInterLeave(tasks, callback);
530 }
531 }
532 }
533
534 /**
535 * Handle inclusion of ReferencesMany relation
536 * @param callback
537 */
538 function includeReferencesMany(callback) {
539 const modelToIdName = idName(relation.modelTo);
540 let allTargetIds = [];
541 // Map for Indexing objects by their id for faster retrieval
542 const targetObjsMap = {};
543 for (let i = 0; i < objs.length; i++) {
544 const obj = objs[i];
545 // one-to-many: foreign key reference is modelTo -> modelFrom.
546 // use modelFrom.keyFrom in where filter later
547 let targetIds = obj[relation.keyFrom];
548 if (targetIds) {
549 if (typeof targetIds === 'string') {
550 // For relational DBs, the array is stored as stringified json
551 // Please note obj is a plain object at this point
552 targetIds = JSON.parse(targetIds);
553 }
554 // referencesMany has multiple targetIds per obj. We need to concat
555 // them into allTargetIds before DB Call
556 allTargetIds = allTargetIds.concat(targetIds);
557 for (let j = 0; j < targetIds.length; j++) {
558 const targetId = targetIds[j];
559 const targetIdStr = targetId.toString();
560 const objList = targetObjsMap[targetIdStr] =
561 targetObjsMap[targetIdStr] || [];
562 objList.push(obj);
563 }
564 }
565 // sourceId can be null. but cache empty data as result
566 defineCachedRelations(obj);
567 obj.__cachedRelations[relationName] = [];
568 }
569 filter.where[relation.keyTo] = {
570 inq: uniq(allTargetIds),
571 };
572 relation.applyScope(null, filter);
573 /**
574 * Make the DB Call, fetch all target objects
575 */
576 findWithForeignKeysByPage(relation.modelTo, filter,
577 relation.keyTo, 0, options, targetFetchHandler);
578 /**
579 * Handle the fetched target objects
580 * @param err
581 * @param {Array<Model>}targets
582 * @returns {*}
583 */
584 function targetFetchHandler(err, targets) {
585 if (err) {
586 return callback(err);
587 }
588 const tasks = [];
589 // simultaneously process subIncludes
590 if (subInclude && targets) {
591 tasks.push(function subIncludesTask(next) {
592 relation.modelTo.include(targets, subInclude, options, next);
593 });
594 }
595 targets = utils.sortObjectsByIds(modelToIdName, allTargetIds, targets);
596 // process each target object
597 tasks.push(targetLinkingTask);
598 function targetLinkingTask(next) {
599 async.each(targets, linkManyToMany, next);
600 function linkManyToMany(target, next) {
601 const objList = targetObjsMap[target[relation.keyTo].toString()];
602 async.each(objList, function(obj, next) {
603 if (!obj) return next();
604 obj.__cachedRelations[relationName].push(target);
605 processTargetObj(obj, next);
606 }, next);
607 }
608 }
609
610 execTasksWithInterLeave(tasks, callback);
611 }
612 }
613
614 /**
615 * Handle inclusion of HasMany relation
616 * @param callback
617 */
618 function includeHasManySimple(callback) {
619 // Map for Indexing objects by their id for faster retrieval
620 const objIdMap2 = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, relation.keyFrom);
621
622 filter.where[relation.keyTo] = {
623 inq: uniq(objIdMap2.getKeys()),
624 };
625
626 relation.applyScope(null, filter);
627
628 findWithForeignKeysByPage(relation.modelTo, filter,
629 relation.keyTo, 0, options, targetFetchHandler);
630
631 function targetFetchHandler(err, targets) {
632 if (err) {
633 return callback(err);
634 }
635 const targetsIdMap = includeUtils.buildOneToManyIdentityMapWithOrigKeys(targets, relation.keyTo);
636 includeUtils.join(objIdMap2, targetsIdMap, function(obj1, valueToMergeIn) {
637 defineCachedRelations(obj1);
638 obj1.__cachedRelations[relationName] = valueToMergeIn;
639 processTargetObj(obj1, function() {});
640 });
641 callback(err, objs);
642 }
643 }
644
645 /**
646 * Handle inclusion of HasMany relation
647 * @param callback
648 */
649 function includeHasMany(callback) {
650 const sourceIds = [];
651 // Map for Indexing objects by their id for faster retrieval
652 const objIdMap = {};
653 for (let i = 0; i < objs.length; i++) {
654 const obj = objs[i];
655 // one-to-many: foreign key reference is modelTo -> modelFrom.
656 // use modelFrom.keyFrom in where filter later
657 const sourceId = obj[relation.keyFrom];
658 if (sourceId) {
659 sourceIds.push(sourceId);
660 objIdMap[sourceId.toString()] = obj;
661 }
662 // sourceId can be null. but cache empty data as result
663 defineCachedRelations(obj);
664 obj.__cachedRelations[relationName] = [];
665 }
666 filter.where[relation.keyTo] = {
667 inq: uniq(sourceIds),
668 };
669 relation.applyScope(null, filter);
670 options.partitionBy = relation.keyTo;
671
672 findWithForeignKeysByPage(relation.modelTo, filter,
673 relation.keyTo, 0, options, targetFetchHandler);
674
675 /**
676 * Process fetched related objects
677 * @param err
678 * @param {Array<Model>} targets
679 * @returns {*}
680 */
681 function targetFetchHandler(err, targets) {
682 if (err) {
683 return callback(err);
684 }
685 const tasks = [];
686 // simultaneously process subIncludes
687 if (subInclude && targets) {
688 tasks.push(function subIncludesTask(next) {
689 relation.modelTo.include(targets, subInclude, options, next);
690 });
691 }
692 // process each target object
693 tasks.push(targetLinkingTask);
694 function targetLinkingTask(next) {
695 if (targets.length === 0) {
696 return async.each(objs, function(obj, next) {
697 processTargetObj(obj, next);
698 }, next);
699 }
700
701 async.each(targets, linkManyToOne, next);
702 function linkManyToOne(target, next) {
703 // fix for bug in hasMany with referencesMany
704 const targetIds = [].concat(target[relation.keyTo]);
705 async.each(targetIds, function(targetId, next) {
706 const obj = objIdMap[targetId.toString()];
707 if (!obj) return next();
708 obj.__cachedRelations[relationName].push(target);
709 processTargetObj(obj, next);
710 }, function(err, processedTargets) {
711 if (err) {
712 return next(err);
713 }
714
715 const objsWithEmptyRelation = objs.filter(function(obj) {
716 return obj.__cachedRelations[relationName].length === 0;
717 });
718 async.each(objsWithEmptyRelation, function(obj, next) {
719 processTargetObj(obj, next);
720 }, function(err) {
721 next(err, processedTargets);
722 });
723 });
724 }
725 }
726
727 execTasksWithInterLeave(tasks, callback);
728 }
729 }
730
731 /**
732 * Handle Inclusion of Polymorphic BelongsTo relation
733 * @param callback
734 */
735 function includePolymorphicBelongsTo(callback) {
736 const targetIdsByType = {};
737 // Map for Indexing objects by their type and targetId for faster retrieval
738 const targetObjMapByType = {};
739 for (let i = 0; i < objs.length; i++) {
740 const obj = objs[i];
741 const discriminator = polymorphic.discriminator;
742 const modelType = obj[discriminator];
743 if (modelType) {
744 targetIdsByType[modelType] = targetIdsByType[modelType] || [];
745 targetObjMapByType[modelType] = targetObjMapByType[modelType] || {};
746 const targetIds = targetIdsByType[modelType];
747 const targetObjsMap = targetObjMapByType[modelType];
748 const targetId = obj[relation.keyFrom];
749 if (targetId) {
750 targetIds.push(targetId);
751 const targetIdStr = targetId.toString();
752 targetObjsMap[targetIdStr] = targetObjsMap[targetIdStr] || [];
753 // Is belongsTo. Multiple objects can have the same
754 // targetId and therefore map value is an array
755 targetObjsMap[targetIdStr].push(obj);
756 }
757 }
758 defineCachedRelations(obj);
759 obj.__cachedRelations[relationName] = null;
760 }
761 async.each(Object.keys(targetIdsByType), processPolymorphicType,
762 callback);
763 /**
764 * Process Polymorphic objects of each type (modelType)
765 * @param {String} modelType
766 * @param callback
767 */
768 function processPolymorphicType(modelType, callback) {
769 const typeFilter = {where: {}};
770 utils.mergeQuery(typeFilter, filter);
771 const targetIds = targetIdsByType[modelType];
772 typeFilter.where[relation.keyTo] = {
773 inq: uniq(targetIds),
774 };
775 const Model = lookupModel(relation.modelFrom.dataSource.modelBuilder.
776 models, modelType);
777 if (!Model) {
778 callback(new Error(g.f('Discriminator type %s specified but no model exists with such name',
779 modelType)));
780 return;
781 }
782 relation.applyScope(null, typeFilter);
783
784 findWithForeignKeysByPage(Model, typeFilter,
785 relation.keyTo, 0, options, targetFetchHandler);
786
787 /**
788 * Process fetched related objects
789 * @param err
790 * @param {Array<Model>} targets
791 * @returns {*}
792 */
793 function targetFetchHandler(err, targets) {
794 if (err) {
795 return callback(err);
796 }
797 const tasks = [];
798
799 // simultaneously process subIncludes
800 if (subInclude && targets) {
801 tasks.push(function subIncludesTask(next) {
802 Model.include(targets, subInclude, options, next);
803 });
804 }
805 // process each target object
806 tasks.push(targetLinkingTask);
807 function targetLinkingTask(next) {
808 const targetObjsMap = targetObjMapByType[modelType];
809 async.each(targets, linkOneToMany, next);
810 function linkOneToMany(target, next) {
811 const objList = targetObjsMap[target[relation.keyTo].toString()];
812 async.each(objList, function(obj, next) {
813 if (!obj) return next();
814 obj.__cachedRelations[relationName] = target;
815 processTargetObj(obj, next);
816 }, next);
817 }
818 }
819
820 execTasksWithInterLeave(tasks, callback);
821 }
822 }
823 }
824
825 /**
826 * Handle Inclusion of Polymorphic HasOne relation
827 * @param callback
828 */
829 function includePolymorphicHasOne(callback) {
830 const sourceIds = [];
831 // Map for Indexing objects by their id for faster retrieval
832 const objIdMap = {};
833 for (let i = 0; i < objs.length; i++) {
834 const obj = objs[i];
835 // one-to-one: foreign key reference is modelTo -> modelFrom.
836 // use modelFrom.keyFrom in where filter later
837 const sourceId = obj[relation.keyFrom];
838 if (sourceId) {
839 sourceIds.push(sourceId);
840 objIdMap[sourceId.toString()] = obj;
841 }
842 // sourceId can be null. but cache empty data as result
843 defineCachedRelations(obj);
844 obj.__cachedRelations[relationName] = null;
845 }
846 filter.where[relation.keyTo] = {
847 inq: uniq(sourceIds),
848 };
849 relation.applyScope(null, filter);
850
851 findWithForeignKeysByPage(relation.modelTo, filter,
852 relation.keyTo, 0, options, targetFetchHandler);
853
854 /**
855 * Process fetched related objects
856 * @param err
857 * @param {Array<Model>} targets
858 * @returns {*}
859 */
860 function targetFetchHandler(err, targets) {
861 if (err) {
862 return callback(err);
863 }
864 const tasks = [];
865 // simultaneously process subIncludes
866 if (subInclude && targets) {
867 tasks.push(function subIncludesTask(next) {
868 relation.modelTo.include(targets, subInclude, options, next);
869 });
870 }
871 // process each target object
872 tasks.push(targetLinkingTask);
873 function targetLinkingTask(next) {
874 async.each(targets, linkOneToOne, next);
875 function linkOneToOne(target, next) {
876 const sourceId = target[relation.keyTo];
877 if (!sourceId) return next();
878 const obj = objIdMap[sourceId.toString()];
879 if (!obj) return next();
880 obj.__cachedRelations[relationName] = target;
881 processTargetObj(obj, next);
882 }
883 }
884
885 execTasksWithInterLeave(tasks, callback);
886 }
887 }
888
889 /**
890 * Handle Inclusion of BelongsTo/HasOne relation
891 * @param callback
892 */
893 function includeOneToOne(callback) {
894 const targetIds = [];
895 const objTargetIdMap = {};
896 for (let i = 0; i < objs.length; i++) {
897 const obj = objs[i];
898 if (relation.type === 'belongsTo') {
899 if (obj[relation.keyFrom] == null) {
900 defineCachedRelations(obj);
901 obj.__cachedRelations[relationName] = null;
902 debug('ID property "%s" is missing in item %j', relation.keyFrom, obj);
903 continue;
904 }
905 }
906 const targetId = obj[relation.keyFrom];
907 if (targetId) {
908 targetIds.push(targetId);
909 const targetIdStr = targetId.toString();
910 objTargetIdMap[targetIdStr] = objTargetIdMap[targetIdStr] || [];
911 objTargetIdMap[targetIdStr].push(obj);
912 } else {
913 debug('ID property "%s" is missing in item %j', relation.keyFrom, obj);
914 }
915 defineCachedRelations(obj);
916 obj.__cachedRelations[relationName] = null;
917 }
918 filter.where[relation.keyTo] = {
919 inq: uniq(targetIds),
920 };
921 relation.applyScope(null, filter);
922
923 findWithForeignKeysByPage(relation.modelTo, filter,
924 relation.keyTo, 0, options, targetFetchHandler);
925
926 /**
927 * Process fetched related objects
928 * @param err
929 * @param {Array<Model>} targets
930 * @returns {*}
931 */
932 function targetFetchHandler(err, targets) {
933 if (err) {
934 return callback(err);
935 }
936 const tasks = [];
937 // simultaneously process subIncludes
938 if (subInclude && targets) {
939 tasks.push(function subIncludesTask(next) {
940 relation.modelTo.include(targets, subInclude, options, next);
941 });
942 }
943 // process each target object
944 tasks.push(targetLinkingTask);
945 function targetLinkingTask(next) {
946 async.each(targets, linkOneToMany, next);
947 function linkOneToMany(target, next) {
948 const targetId = target[relation.keyTo];
949 const objList = objTargetIdMap[targetId.toString()];
950 async.each(objList, function(obj, next) {
951 if (!obj) return next();
952 obj.__cachedRelations[relationName] = target;
953 processTargetObj(obj, next);
954 }, next);
955 }
956 }
957
958 execTasksWithInterLeave(tasks, callback);
959 }
960 }
961
962 /**
963 * Handle Inclusion of EmbedsMany/EmbedsManyWithBelongsTo/EmbedsOne
964 * Relations. Since Embedded docs are part of parents, no need to make
965 * db calls. Let the related function be called for each object to fetch
966 * the results from cache.
967 *
968 * TODO: Optimize EmbedsManyWithBelongsTo relation DB Calls
969 * @param callback
970 */
971 function includeEmbeds(callback) {
972 async.each(objs, function(obj, next) {
973 processTargetObj(obj, next);
974 }, callback);
975 }
976
977 /**
978 * Process Each Model Object and make sure specified relations are included
979 * @param {Model} obj - Single Mode object for which inclusion is needed
980 * @param callback
981 * @returns {*}
982 */
983 function processTargetObj(obj, callback) {
984 const isInst = obj instanceof self;
985
986 // Calling the relation method on the instance
987 if (relation.type === 'belongsTo') {
988 // If the belongsTo relation doesn't have an owner
989 if (obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
990 defineCachedRelations(obj);
991 // Set to null if the owner doesn't exist
992 obj.__cachedRelations[relationName] = null;
993 if (isInst) {
994 obj.__data[relationName] = null;
995 } else {
996 obj[relationName] = null;
997 }
998 return callback();
999 }
1000 }
1001 /**
1002 * Sets the related objects as a property of Parent Object
1003 * @param {Array<Model>|Model|null} result - Related Object/Objects
1004 * @param cb
1005 */
1006 function setIncludeData(result, cb) {
1007 if (isInst) {
1008 if (Array.isArray(result) && !(result instanceof List)) {
1009 result = new List(result, relation.modelTo);
1010 }
1011 obj.__data[relationName] = result;
1012 // obj.setStrict(false); issue #1252
1013 } else {
1014 obj[relationName] = result;
1015 }
1016 cb(null, result);
1017 }
1018
1019 // obj.__cachedRelations[relationName] can be null if no data was returned
1020 if (obj.__cachedRelations &&
1021 obj.__cachedRelations[relationName] !== undefined) {
1022 return setIncludeData(obj.__cachedRelations[relationName],
1023 callback);
1024 }
1025
1026 const inst = (obj instanceof self) ? obj : new self(obj);
1027
1028 // If related objects are not cached by include Handlers, directly call
1029 // related accessor function even though it is not very efficient
1030 let related; // relation accessor function
1031
1032 if ((relation.multiple || relation.type === 'belongsTo') && scope) {
1033 const includeScope = {};
1034 const filter = scope.conditions();
1035
1036 // make sure not to miss any fields for sub includes
1037 if (filter.fields && Array.isArray(subInclude) && relation.modelTo.relations) {
1038 includeScope.fields = [];
1039 subInclude.forEach(function(name) {
1040 const rel = relation.modelTo.relations[name];
1041 if (rel && rel.type === 'belongsTo') {
1042 includeScope.fields.push(rel.keyFrom);
1043 }
1044 });
1045 }
1046
1047 utils.mergeQuery(filter, includeScope, {fields: false});
1048
1049 related = inst[relationName].bind(inst, filter);
1050 } else {
1051 related = inst[relationName].bind(inst, undefined);
1052 }
1053
1054 related(options, function(err, result) {
1055 if (err) {
1056 return callback(err);
1057 } else {
1058 defineCachedRelations(obj);
1059 obj.__cachedRelations[relationName] = result;
1060
1061 return setIncludeData(result, callback);
1062 }
1063 });
1064 }
1065 }
1066};