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