@depup/bookshelf 1.2.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,545 @@
1
+ const _ = require('lodash');
2
+ const Sync = require('./sync');
3
+ const Helpers = require('./helpers');
4
+ const EagerRelation = require('./eager');
5
+ const Errors = require('./errors');
6
+ const CollectionBase = require('./base/collection');
7
+ const Promise = require('bluebird');
8
+ const createError = require('create-error');
9
+
10
+ /**
11
+ * When creating a {@link Collection}, you may choose to pass in the initial array of
12
+ * {@link Model models}. The collection's {@link Collection#comparator comparator} may be included
13
+ * as an option. Passing `false` as the comparator option will prevent sorting. If you define an
14
+ * {@link Collection#initialize initialize} function, it will be invoked when the collection is
15
+ * created.
16
+ *
17
+ * If you would like to customize the Collection used by your models when calling
18
+ * {@link Model#fetchAll} or {@link Model#fetchPage} you can use the following process:
19
+ *
20
+ * const Test = bookshelf.model('Test', {
21
+ * tableName: 'test'
22
+ * }, {
23
+ * collection(...args) {
24
+ * return new Tests(...args)
25
+ * }
26
+ * })
27
+ * const Tests = bookshelf.collection('Tests', {
28
+ * get model() {
29
+ * return Test
30
+ * },
31
+ * initialize () {
32
+ * this.constructor.__super__.initialize.apply(this, arguments)
33
+ * // Collection will emit fetching event as expected even on eager queries.
34
+ * this.on('fetching', () => {})
35
+ * },
36
+ * doStuff() {
37
+ * // This method will be available in the results collection returned
38
+ * // by Test.fetchAll() and Test.fetchPage()
39
+ * }
40
+ * })
41
+ *
42
+ * @example
43
+ * const TabSet = bookshelf.collection('TabSet', {
44
+ * model: Tab
45
+ * })
46
+ * const tabs = new TabSet([tab1, tab2, tab3])
47
+ *
48
+ * @class Collection
49
+ * @extends CollectionBase
50
+ * @classdesc
51
+ * Collections are ordered sets of models returned from the database, from a
52
+ * {@link Model#fetchAll fetchAll} call.
53
+ * @param {(Model[])=} models Initial array of models.
54
+ * @param {Object=} options
55
+ * @param {Boolean} [options.comparator=false]
56
+ * {@link Collection#comparator Comparator} for collection, or `false` to disable sorting.
57
+ */
58
+ const BookshelfCollection = (module.exports = CollectionBase.extend(
59
+ /** @lends Collection.prototype */
60
+ {
61
+ /**
62
+ * Used to define relationships where a {@link Model#hasMany hasMany} or
63
+ * {@link Model#belongsToMany belongsToMany} relation passes "through" an `Interim` model. This
64
+ * is exactly like the equivalent {@link Model#through model method} except that it applies to
65
+ * the collections that the above mentioned relation methods return instead of individual
66
+ * models.
67
+ *
68
+ * A good example of where this would be useful is if a book {@link Model#hasMany hasMany}
69
+ * paragraphs *through* chapters. See the example above for how this can be used.
70
+ *
71
+ * @example
72
+ * const Chapter = bookshelf.model('Chapter', {
73
+ * tableName: 'chapters',
74
+ * paragraphs() {
75
+ * return this.hasMany(Paragraph)
76
+ * }
77
+ * })
78
+ *
79
+ * const Paragraph = bookshelf.model('Paragraph', {
80
+ * tableName: 'paragraphs',
81
+ * chapter() {
82
+ * return this.belongsTo(Chapter)
83
+ * }
84
+ * })
85
+ *
86
+ * const Book = bookshelf.model('Book', {
87
+ * tableName: 'books',
88
+
89
+ * // Find all paragraphs associated with this book, by
90
+ * // passing through the "Chapter" model.
91
+ * paragraphs() {
92
+ * return this.hasMany(Paragraph).through(Chapter)
93
+ * }
94
+ * })
95
+ *
96
+ * @param {Model} Interim Pivot model.
97
+ * @param {string} [throughForeignKey]
98
+ * Foreign key in this collection's model. This is the model that the `hasMany` or
99
+ * `belongsToMany` relations return. By default, the `foreignKey` is assumed to be the
100
+ * singular form of the `Target` model's tableName, followed by `_id` /
101
+ * `_{{{@link Model#idAttribute idAttribute}}}`.
102
+ * @param {string} [otherKey]
103
+ * Foreign key in the `Interim` model. By default, the `otherKey` is assumed to be the
104
+ * singular form of this model's tableName, followed by `_id` /
105
+ * `_{{{@link Model#idAttribute idAttribute}}}`.
106
+ * @param {string} [throughForeignKeyTarget]
107
+ * Column in this collection's model which `throughForeignKey` references, if other than the
108
+ * default of the model's `id` / `{@link Model#idAttribute idAttribute}`.
109
+ * @param {string} [otherKeyTarget]
110
+ * Column in the `Interim` model which `otherKey` references, if other than `id` /
111
+ * `{@link Model#idAttribute idAttribute}`.
112
+ * @returns {Collection} The related but empty collection.
113
+ */
114
+ through: function(Interim, throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget) {
115
+ return this.relatedData.through(this, Interim, {
116
+ throughForeignKey,
117
+ otherKey,
118
+ throughForeignKeyTarget,
119
+ otherKeyTarget
120
+ });
121
+ },
122
+
123
+ /**
124
+ * Fetch the default set of models for this collection from the database,
125
+ * resetting the collection when they arrive. If you wish to trigger an
126
+ * error if the fetched collection is empty, pass `{require: true}` as one
127
+ * of the options to the {@link Collection#fetch fetch} call. A {@link
128
+ * Collection#fetched "fetched"} event will be fired when records are
129
+ * successfully retrieved. If you need to constrain the query performed by
130
+ * `fetch`, you can call the {@link Collection#query query} method before
131
+ * calling `fetch`.
132
+ *
133
+ * If you'd like to only fetch specific columns, you may specify a `columns`
134
+ * property in the options for the `fetch` call.
135
+ *
136
+ * The `withRelated` option may be specified to fetch the models of the
137
+ * collection, eager loading any specified {@link Relation relations} named on
138
+ * the model. A single property, or an array of properties can be specified as
139
+ * a value for the `withRelated` property. The results of these relation
140
+ * queries will be loaded into a relations property on the respective models,
141
+ * may be retrieved with the {@link Model#related related} method.
142
+ *
143
+ * @fires Collection#fetched
144
+ * @throws {Collection.EmptyError} Thrown if no records are found.
145
+ * @param {Object=} options
146
+ * @param {Boolean} [options.require=false]
147
+ * Whether or not to throw a {@link Collection.EmptyError} if no records are found.
148
+ * You can pass the `require: true` option to override this behavior.
149
+ * @param {string|string[]} [options.withRelated=[]]
150
+ * A relation, or list of relations, to be eager loaded as part of the `fetch` operation.
151
+ * @param {boolean} [options.debug=false]
152
+ * Whether to enable debugging mode or not. When enabled will show information about the
153
+ * queries being run.
154
+ * @returns {Promise<Collection>}
155
+ */
156
+ fetch: Promise.method(function(options) {
157
+ options = options ? _.clone(options) : {};
158
+ return (
159
+ this.sync(options)
160
+ .select()
161
+ .bind(this)
162
+ .tap(function(response) {
163
+ if (!response || response.length === 0) {
164
+ throw new this.constructor.EmptyError('EmptyResponse');
165
+ }
166
+ })
167
+
168
+ // Now, load all of the data onto the collection as necessary.
169
+ .tap(function(response) {
170
+ return this._handleResponse(response, options);
171
+ })
172
+
173
+ // If the "withRelated" is specified, we also need to eager load all of the
174
+ // data on the collection, as a side-effect, before we ultimately jump into the
175
+ // next step of the collection. Since the `columns` are only relevant to the current
176
+ // level, ensure those are omitted from the options.
177
+ .tap(function(response) {
178
+ if (options.withRelated) {
179
+ return this._handleEager(response, _.omit(options, 'columns'));
180
+ }
181
+ })
182
+ .tap(function(response) {
183
+ /**
184
+ * @event Collection#fetched
185
+ * @tutorial events
186
+ *
187
+ * @description
188
+ * Fired after a `fetch` operation. A promise may be returned from the
189
+ * event handler for async behaviour.
190
+ *
191
+ * @param {Collection} collection The collection performing the {@link Collection#fetch}.
192
+ * @param {Object} response Knex query response.
193
+ * @param {Object} options Options object passed to {@link Collection#fetch fetch}.
194
+ * @returns {Promise}
195
+ */
196
+ return this.triggerThen('fetched', this, response, options);
197
+ })
198
+ .catch(this.constructor.EmptyError, function(err) {
199
+ if (options.require) throw err;
200
+ this.reset([], {silent: true});
201
+ })
202
+ .return(this)
203
+ );
204
+ }),
205
+
206
+ fetchPage(options) {
207
+ if (!options) options = {};
208
+ return Helpers.fetchPage.call(this, options);
209
+ },
210
+
211
+ /**
212
+ * Get the number of records in the collection's table.
213
+ *
214
+ * @example
215
+ * // select count(*) from shareholders where company_id = 1 and share &gt; 0.1;
216
+ * new Company({id: 1})
217
+ * .shareholders()
218
+ * .where('share', '>', '0.1')
219
+ * .count()
220
+ * .then((count) => {
221
+ * assert(count === 3)
222
+ * })
223
+ *
224
+ * @since 0.8.2
225
+ * @see Model#count
226
+ * @param {string} [column='*']
227
+ * Specify a column to count. Rows with `null` values in this column will be excluded.
228
+ * @param {Object} [options] Hash of options.
229
+ * @returns {Promise<number|string>}
230
+ */
231
+ count: Promise.method(function(column, options) {
232
+ if (!_.isString(column)) {
233
+ options = column;
234
+ column = undefined;
235
+ }
236
+ if (options) options = _.clone(options);
237
+ return this.sync(options).count(column);
238
+ }),
239
+
240
+ /**
241
+ * Fetch and return a single {@link Model model} from the collection,
242
+ * maintaining any {@link Relation relation} data from the collection, and
243
+ * any {@link Collection#query query} parameters that have already been passed
244
+ * to the collection. Especially helpful on relations, where you would only
245
+ * like to return a single model from the associated collection.
246
+ *
247
+ * @example
248
+ * // select * from authors where site_id = 1 and id = 2 limit 1;
249
+ * new Site({id:1})
250
+ * .authors()
251
+ * .query({where: {id: 2}})
252
+ * .fetchOne()
253
+ * .then(function(model) {
254
+ * // ...
255
+ * });
256
+ *
257
+ * @param {Object=} options
258
+ * @param {Boolean} [options.require=true]
259
+ * Whether or not to reject the returned Promise with a {@link Model.NotFoundError} if no
260
+ * records can be fetched from the database.
261
+ * @param {(string|string[])} [options.columns='*']
262
+ * Limit the number of columns fetched.
263
+ * @param {Transaction} [options.transacting] Optionally run the query in a transaction.
264
+ * @param {string} [options.lock]
265
+ * Type of row-level lock to use. Valid options are `forShare` and
266
+ * `forUpdate`. This only works in conjunction with the `transacting`
267
+ * option, and requires a database that supports it.
268
+ * @param {boolean} [options.debug=false]
269
+ * Whether to enable debugging mode or not. When enabled will show information about the
270
+ * queries being run.
271
+ * @throws {Model.NotFoundError}
272
+ * @returns {Promise<Model|null>}
273
+ * A promise resolving to the fetched {@link Model} or `null` if none exists and the
274
+ * `require: false` option is passed or {@link Model#requireFetch requireFetch} is set to
275
+ * `false`.
276
+ */
277
+ fetchOne: Promise.method(function(options) {
278
+ const model = new this.model();
279
+ model._knex = this.query().clone();
280
+ this.resetQuery();
281
+ if (this.relatedData) model.relatedData = this.relatedData;
282
+ return model.fetch(options);
283
+ }),
284
+
285
+ /**
286
+ * This method is used to eager load relations onto a Collection, in a similar way that the
287
+ * `withRelated` property works on {@link Collection#fetch fetch}. Nested eager loads can be
288
+ * specified by separating the nested relations with `.`.
289
+ *
290
+ * @param {string|string[]} relations The relation, or relations, to be loaded.
291
+ * @param {Object} [options] Hash of options.
292
+ * @param {Transaction} [options.transacting]
293
+ * @param {string} [options.lock]
294
+ * Type of row-level lock to use. Valid options are `forShare` and `forUpdate`. This only
295
+ * works in conjunction with the `transacting` option, and requires a database that supports
296
+ * it.
297
+ * @param {boolean} [options.debug=false]
298
+ * Whether to enable debugging mode or not. When enabled will show information about the
299
+ * queries being run.
300
+ * @returns {Promise<Collection>} A promise resolving to this {@link Collection collection}.
301
+ */
302
+ load: Promise.method(function(relations, options) {
303
+ if (!Array.isArray(relations)) relations = [relations];
304
+ options = _.assignIn({}, options, {
305
+ shallow: true,
306
+ withRelated: relations
307
+ });
308
+ return new EagerRelation(this.models, this.toJSON(options), new this.model()).fetch(options).return(this);
309
+ }),
310
+
311
+ /**
312
+ * Convenience method to create a new {@link Model model} instance within a
313
+ * collection. Equivalent to instantiating a model with a hash of {@link
314
+ * Model#attributes attributes}, {@link Model#save saving} the model to the
315
+ * database then adding the model to the collection.
316
+ *
317
+ * When used on a relation, `create` will automatically set foreign key
318
+ * attributes before persisting the `Model`.
319
+ *
320
+ * @example
321
+ * const { courses, ...attributes } = req.body;
322
+ *
323
+ * Student.forge(attributes).save().tap(student =>
324
+ * Promise.map(courses, course => student.related('courses').create(course))
325
+ * ).then(student =>
326
+ * res.status(200).send(student)
327
+ * ).catch(error =>
328
+ * res.status(500).send(error.message)
329
+ * );
330
+ *
331
+ * @param {Object} model A set of attributes to be set on the new model.
332
+ * @param {Object} [options]
333
+ * @param {Transaction} [options.transacting]
334
+ * @param {boolean} [options.debug=false]
335
+ * Whether to enable debugging mode or not. When enabled will show information about the
336
+ * queries being run.
337
+ * @returns {Promise<Model>} A promise resolving with the new {@link Model model}.
338
+ */
339
+ create: Promise.method(function(model, options) {
340
+ options = options != null ? _.clone(options) : {};
341
+ const relatedData = this.relatedData;
342
+ model = this._prepareModel(model, options);
343
+
344
+ // If we've already added things on the query chain, these are likely intended for the model.
345
+ if (this._knex) {
346
+ model._knex = this._knex;
347
+ this.resetQuery();
348
+ }
349
+ return Helpers.saveConstraints(model, relatedData)
350
+ .save(null, options)
351
+ .bind(this)
352
+ .then(function() {
353
+ if (relatedData && relatedData.type === 'belongsToMany') {
354
+ return this.attach(model, _.omit(options, 'query'));
355
+ }
356
+ })
357
+ .then(function() {
358
+ this.add(model, options);
359
+ })
360
+ .return(model);
361
+ }),
362
+
363
+ /**
364
+ * Used to reset the internal state of the current query builder instance. This method is called
365
+ * internally each time a database action is completed by {@link Sync}.
366
+ *
367
+ * @private
368
+ * @returns {Collection} Self, this method is chainable.
369
+ */
370
+ resetQuery: function() {
371
+ this._knex = null;
372
+ return this;
373
+ },
374
+
375
+ /**
376
+ * This method is used to tap into the underlying Knex query builder instance for the current
377
+ * collection.
378
+ *
379
+ * If called with no arguments, it will return the query builder directly, otherwise it will
380
+ * call the specified `method` on the query builder, applying any additional arguments from the
381
+ * `collection.query` call.
382
+ *
383
+ * If the `method` argument is a function, it will be called with the Knex query builder as the
384
+ * context and the first argument.
385
+ *
386
+ * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`}
387
+ * @example
388
+ * let qb = collection.query();
389
+ * qb.where({id: 1}).select().then(function(resp) {
390
+ * // ...
391
+ * });
392
+ *
393
+ * collection.query(function(qb) {
394
+ * qb.where('id', '>', 5).andWhere('first_name', '=', 'Test');
395
+ * }).fetch()
396
+ * .then(function(collection) {
397
+ * // ...
398
+ * });
399
+ *
400
+ * collection
401
+ * .query('where', 'other_id', '=', '5')
402
+ * .fetch()
403
+ * .then(function(collection) {
404
+ * // ...
405
+ * });
406
+ *
407
+ * @param {function|Object|...string=} arguments The query method.
408
+ * @returns {Collection|QueryBuilder}
409
+ * This collection or, if called with no arguments, the underlying query builder.
410
+ */
411
+ query: function() {
412
+ return Helpers.query(this, Array.from(arguments));
413
+ },
414
+
415
+ /**
416
+ * This is used as convenience for the most common {@link Collection#query query} method:
417
+ * adding a `WHERE` clause to the builder. Any additional knex methods may be accessed using
418
+ * {@link Collection#query query}.
419
+ *
420
+ * Accepts either `key, value` syntax, or a hash of attributes to constrain the results.
421
+ *
422
+ * @example
423
+ * collection
424
+ * .where('favorite_color', '<>', 'green')
425
+ * .fetch()
426
+ * .then(results => {
427
+ * // ...
428
+ * })
429
+ *
430
+ * // or
431
+ *
432
+ * collection
433
+ * .where('favorite_color', 'red')
434
+ * .fetch()
435
+ * .then(results => {
436
+ * // ...
437
+ * })
438
+ *
439
+ * collection
440
+ * .where({favorite_color: 'red', shoe_size: 12})
441
+ * .fetch()
442
+ * .then(results => {
443
+ * // ...
444
+ * })
445
+ *
446
+ * @see Collection#query
447
+ * @param {Object|...string} conditions
448
+ * Either `key, [operator], value` syntax, or a hash of attributes to match. Note that these
449
+ * must be formatted as they are in the database, not how they are stored after
450
+ * {@link Model#parse}.
451
+ * @returns {Collection} Self, this method is chainable.
452
+ */
453
+ where() {
454
+ return this.query.apply(this, ['where'].concat(Array.from(arguments)));
455
+ },
456
+
457
+ /**
458
+ * Specifies the column to sort on and sort order.
459
+ *
460
+ * The order parameter is optional, and defaults to 'ASC'. You may
461
+ * also specify 'DESC' order by prepending a hyphen to the sort column
462
+ * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`.
463
+ *
464
+ * Unless specified using dot notation (i.e., "table.column"), the default
465
+ * table will be the table name of the model `orderBy` was called on.
466
+ *
467
+ * @since 0.9.3
468
+ * @example
469
+ * Cars.forge().orderBy('color', 'ASC').fetch()
470
+ * .then(function (rows) { // ...
471
+ *
472
+ * @param {string} column Column to sort on.
473
+ * @param {string} order Ascending (`'ASC'`) or descending (`'DESC'`) order.
474
+ */
475
+ orderBy() {
476
+ return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments)));
477
+ },
478
+
479
+ /**
480
+ * Creates and returns a new `Bookshelf.Sync` instance.
481
+ *
482
+ * @private
483
+ */
484
+ sync: function(options) {
485
+ return new Sync(this, options);
486
+ },
487
+
488
+ /* Ensure that QueryBuilder is copied on clone. */
489
+ clone() {
490
+ const cloned = BookshelfCollection.__super__.clone.apply(this, arguments);
491
+ if (this._knex != null) {
492
+ cloned._knex = cloned._builder(this._knex.clone());
493
+ }
494
+ return cloned;
495
+ },
496
+
497
+ /**
498
+ * Handles the response data for the collection, returning from the collection's `fetch` call.
499
+ *
500
+ * @private
501
+ */
502
+ _handleResponse: function(response, options) {
503
+ const relatedData = this.relatedData;
504
+
505
+ this.set(response, {
506
+ merge: options.merge,
507
+ remove: options.remove,
508
+ silent: true,
509
+ parse: true
510
+ }).invokeMap(function() {
511
+ this.formatTimestamps();
512
+ this._reset();
513
+ this._previousAttributes = _.cloneDeep(this.attributes);
514
+ });
515
+
516
+ if (relatedData && relatedData.isJoined()) {
517
+ relatedData.parsePivot(this.models);
518
+ }
519
+ },
520
+
521
+ /**
522
+ * Handle the related data loading on the collection.
523
+ *
524
+ * @private
525
+ */
526
+ _handleEager: function(response, options) {
527
+ return new EagerRelation(this.models, response, new this.model()).fetch(options);
528
+ }
529
+ },
530
+ /** @lends Collection */
531
+ {
532
+ extended: function(child) {
533
+ /**
534
+ * @class Collection.EmptyError
535
+ * @description
536
+ * Thrown by default when no records are found by {@link Collection#fetch fetch} or
537
+ * {@link Collection#fetchOne}. This behavior can be overrided with the
538
+ * {@link Model#requireFetch} option.
539
+ */
540
+ child.EmptyError = createError(this.EmptyError);
541
+ }
542
+ }
543
+ ));
544
+
545
+ BookshelfCollection.EmptyError = Errors.EmptyError;
@@ -0,0 +1,2 @@
1
+ exports.PIVOT_PREFIX = '_pivot_';
2
+ exports.DEFAULT_TIMESTAMP_KEYS = ['created_at', 'updated_at'];
package/lib/eager.js ADDED
@@ -0,0 +1,122 @@
1
+ // EagerRelation
2
+ // ---------------
3
+ 'use strict';
4
+
5
+ const _ = require('lodash');
6
+ const Helpers = require('./helpers');
7
+ const Promise = require('bluebird');
8
+ const EagerBase = require('./base/eager');
9
+ const 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.
15
+ class 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
+ // Remove `query` from options to not send the same query to all candidates
62
+ return Target.query('whereIn', idAttribute, ids)
63
+ .sync(_.assign({}, options, {query: undefined}))
64
+ .select()
65
+ .tap((response) => {
66
+ const clone = relatedData.instance('morphTo', Target, {
67
+ morphName,
68
+ columnNames,
69
+ morphValue: type
70
+ });
71
+ return this._eagerLoadHelper(response, relationName, {relatedData: clone}, options);
72
+ });
73
+ })
74
+ ).then(_.flatten);
75
+ }
76
+
77
+ // Handles the eager load for both the `morphTo` and regular cases.
78
+ _eagerLoadHelper(response, relationName, handled, options) {
79
+ const relatedData = handled.relatedData;
80
+ const isEmptyHasOne = response.length === 0 && relatedData.type === 'hasOne';
81
+ const relatedModels = isEmptyHasOne ? [] : this.pushModels(relationName, handled, response, options);
82
+
83
+ return Promise.try(() => {
84
+ // If there is a response, fetch additional nested eager relations, if any.
85
+ if (response.length > 0 && options.withRelated) {
86
+ const relatedModel = relatedData.createModel();
87
+
88
+ // If this is a `morphTo` relation, we need to do additional processing
89
+ // to ensure we don't try to load any relations that don't look to exist.
90
+ if (relatedData.type === 'morphTo') {
91
+ const withRelated = this._filterRelated(relatedModel, options);
92
+ if (withRelated.length === 0) return;
93
+ options = _.extend({}, options, {withRelated: withRelated});
94
+ }
95
+
96
+ return new EagerRelation(relatedModels, response, relatedModel).fetch(options).return(response);
97
+ }
98
+ }).tap(() => {
99
+ return Promise.map(relatedModels, (model) => model.triggerThen('fetched', model, model.attributes, options));
100
+ });
101
+ }
102
+
103
+ // Filters the `withRelated` on a `morphTo` relation, to ensure that only valid
104
+ // relations are attempted for loading.
105
+ _filterRelated(relatedModel, options) {
106
+ // By this point, all withRelated should be turned into a hash, so it should
107
+ // be fairly simple to process by splitting on the dots.
108
+ return _.reduce(
109
+ options.withRelated,
110
+ function(memo, val) {
111
+ for (const key in val) {
112
+ const seg = key.split('.')[0];
113
+ if (_.isFunction(relatedModel[seg])) memo.push(val);
114
+ }
115
+ return memo;
116
+ },
117
+ []
118
+ );
119
+ }
120
+ }
121
+
122
+ module.exports = EagerRelation;