@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.
- package/.eslintrc.json +18 -0
- package/.nycrc.yml +3 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +764 -0
- package/LICENSE +22 -0
- package/README.md +32 -0
- package/bookshelf.js +8 -0
- package/changes.json +14 -0
- package/lib/base/collection.js +802 -0
- package/lib/base/eager.js +111 -0
- package/lib/base/events.js +129 -0
- package/lib/base/model.js +995 -0
- package/lib/base/relation.js +69 -0
- package/lib/bookshelf.js +558 -0
- package/lib/collection.js +545 -0
- package/lib/constants.js +2 -0
- package/lib/eager.js +122 -0
- package/lib/errors.js +36 -0
- package/lib/extend.js +41 -0
- package/lib/helpers.js +212 -0
- package/lib/model.js +1568 -0
- package/lib/relation.js +947 -0
- package/lib/sync.js +244 -0
- package/package.json +111 -0
|
@@ -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 > 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;
|
package/lib/constants.js
ADDED
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;
|