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;
|