UNPKG

20.9 kBJavaScriptView Raw
1const _ = require('lodash');
2const Sync = require('./sync');
3const Helpers = require('./helpers');
4const EagerRelation = require('./eager');
5const Errors = require('./errors');
6const CollectionBase = require('./base/collection');
7const Promise = require('bluebird');
8const 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 */
58const 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
545BookshelfCollection.EmptyError = Errors.EmptyError;