UNPKG

4.87 kBJavaScriptView Raw
1// EagerRelation
2// ---------------
3'use strict';
4
5const _ = require('lodash');
6const Helpers = require('./helpers');
7const Promise = require('./base/promise');
8const EagerBase = require('./base/eager');
9const getAttributeUnique = (models, attribute) => _.uniq(_.map(models, (m) => m.get(attribute)));
10
11// An `EagerRelation` object temporarily stores the models from an eager load,
12// and handles matching eager loaded objects with their parent(s). The
13// `tempModel` is only used to retrieve the value of the relation method, to
14// know the constraints for the eager query.
15class EagerRelation extends EagerBase {
16 // Handles an eager loaded fetch, passing the name of the item we're fetching
17 // for, and any options needed for the current fetch.
18 eagerFetch(relationName, handled, options) {
19 const relatedData = handled.relatedData;
20
21 // skip eager loading for rows where the foreign key isn't set
22 if (relatedData.parentFk === null) return;
23
24 if (relatedData.type === 'morphTo') {
25 return this.morphToFetch(relationName, relatedData, options);
26 }
27
28 return handled
29 .sync(Object.assign({}, options, {parentResponse: this.parentResponse}))
30 .select()
31 .tap((response) => this._eagerLoadHelper(response, relationName, handled, _.omit(options, 'parentResponse')));
32 }
33
34 // Special handler for the eager loaded morph-to relations, this handles the
35 // fact that there are several potential models that we need to be fetching
36 // against. pairing them up onto a single response for the eager loading.
37 morphToFetch(relationName, relatedData, options) {
38 const columnNames = relatedData.columnNames || [];
39 const morphName = relatedData.morphName;
40 const typeColumn = columnNames[0] === undefined ? `${morphName}_type` : columnNames[0];
41 const idColumn = columnNames[1] === undefined ? `${morphName}_id` : columnNames[1];
42
43 const parentsByType = _.groupBy(this.parent, (model) => {
44 const type = model.get(typeColumn);
45
46 if (!type)
47 throw new Error("The target polymorphic model could not be determined because it's missing the type attribute");
48
49 return type;
50 });
51 const TargetByType = _.mapValues(parentsByType, (parents, type) =>
52 Helpers.morphCandidate(relatedData.candidates, type)
53 );
54
55 return Promise.all(
56 _.map(parentsByType, (parents, type) => {
57 const Target = TargetByType[type];
58 const idAttribute = _.result(Target.prototype, 'idAttribute');
59 const ids = getAttributeUnique(parents, idColumn);
60
61 return Target.query('whereIn', idAttribute, ids)
62 .sync(options)
63 .select()
64 .tap((response) => {
65 const clone = relatedData.instance('morphTo', Target, {
66 morphName,
67 columnNames,
68 morphValue: type
69 });
70 return this._eagerLoadHelper(response, relationName, {relatedData: clone}, options);
71 });
72 })
73 ).then(_.flatten);
74 }
75
76 // Handles the eager load for both the `morphTo` and regular cases.
77 _eagerLoadHelper(response, relationName, handled, options) {
78 const relatedData = handled.relatedData;
79 const isEmptyHasOne = response.length === 0 && relatedData.type === 'hasOne';
80 const relatedModels = isEmptyHasOne ? [] : this.pushModels(relationName, handled, response, options);
81
82 return Promise.try(() => {
83 // If there is a response, fetch additional nested eager relations, if any.
84 if (response.length > 0 && options.withRelated) {
85 const relatedModel = relatedData.createModel();
86
87 // If this is a `morphTo` relation, we need to do additional processing
88 // to ensure we don't try to load any relations that don't look to exist.
89 if (relatedData.type === 'morphTo') {
90 const withRelated = this._filterRelated(relatedModel, options);
91 if (withRelated.length === 0) return;
92 options = _.extend({}, options, {withRelated: withRelated});
93 }
94
95 return new EagerRelation(relatedModels, response, relatedModel).fetch(options).return(response);
96 }
97 }).tap(() => {
98 return Promise.map(relatedModels, (model) => model.triggerThen('fetched', model, model.attributes, options));
99 });
100 }
101
102 // Filters the `withRelated` on a `morphTo` relation, to ensure that only valid
103 // relations are attempted for loading.
104 _filterRelated(relatedModel, options) {
105 // By this point, all withRelated should be turned into a hash, so it should
106 // be fairly simple to process by splitting on the dots.
107 return _.reduce(
108 options.withRelated,
109 function(memo, val) {
110 for (const key in val) {
111 const seg = key.split('.')[0];
112 if (_.isFunction(relatedModel[seg])) memo.push(val);
113 }
114 return memo;
115 },
116 []
117 );
118 }
119}
120
121module.exports = EagerRelation;