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('./base/promise');
|
8 | const createError = require('create-error');
|
9 |
|
10 | /**
|
11 | * @class Collection
|
12 | * @extends CollectionBase
|
13 | * @inheritdoc
|
14 | * @classdesc
|
15 | *
|
16 | * Collections are ordered sets of models returned from the database, from a
|
17 | * {@link Model#fetchAll fetchAll} call. They may be used with a suite of
|
18 | * {@link http://lodash.com/ Lodash} methods.
|
19 | *
|
20 | * @constructor
|
21 | * @description
|
22 | *
|
23 | * When creating a {@link Collection}, you may choose to pass in the initial
|
24 | * array of {@link Model models}. The collection's {@link Collection#comparator
|
25 | * comparator} may be included as an option. Passing `false` as the comparator
|
26 | * option will prevent sorting. If you define an {@link Collection#initialize
|
27 | * initialize} function, it will be invoked when the collection is created.
|
28 | *
|
29 | * @example
|
30 | * let tabs = new TabSet([tab1, tab2, tab3]);
|
31 | *
|
32 | * @param {(Model[])=} models Initial array of models.
|
33 | * @param {Object=} options
|
34 | * @param {Boolean} [options.comparator=false]
|
35 | * {@link Collection#comparator Comparator} for collection, or `false` to disable sorting.
|
36 | */
|
37 | const BookshelfCollection = (module.exports = CollectionBase.extend(
|
38 | {
|
39 | /**
|
40 | * @method Collection#through
|
41 | * @description
|
42 | * Used to define passthrough relationships - `hasOne`, `hasMany`, `belongsTo`
|
43 | * or `belongsToMany`, "through" an `Interim` model or collection.
|
44 | *
|
45 | * @param {Model} Interim Pivot model.
|
46 | *
|
47 | * @param {string=} throughForeignKey
|
48 | *
|
49 | * Foreign key in this collection model. By default, the `foreignKey` is assumed to
|
50 | * be the singular form of the `Target` model's tableName, followed by `_id` /
|
51 | * `_{{{@link Model#idAttribute idAttribute}}}`.
|
52 | *
|
53 | * @param {string=} otherKey
|
54 | *
|
55 | * Foreign key in the `Interim` model. By default, the `otherKey` is assumed to
|
56 | * be the singular form of this model's tableName, followed by `_id` /
|
57 | * `_{{{@link Model#idAttribute idAttribute}}}`.
|
58 | *
|
59 | * @param {string=} throughForeignKeyTarget
|
60 | *
|
61 | * Column in the `Target` model which `throughForeignKey` references, if other
|
62 | * than `Target` model's `id` / `{@link Model#idAttribute idAttribute}`.
|
63 | *
|
64 | * @param {string=} otherKeyTarget
|
65 | *
|
66 | * Column in this collection model which `otherKey` references, if other
|
67 | * than `id` / `{@link Model#idAttribute idAttribute}`.
|
68 | *
|
69 | * @returns {Collection}
|
70 | */
|
71 | through: function(Interim, throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget) {
|
72 | return this.relatedData.through(this, Interim, {
|
73 | throughForeignKey,
|
74 | otherKey,
|
75 | throughForeignKeyTarget,
|
76 | otherKeyTarget
|
77 | });
|
78 | },
|
79 |
|
80 | /**
|
81 | * @method Collection#fetch
|
82 | * @description
|
83 | * Fetch the default set of models for this collection from the database,
|
84 | * resetting the collection when they arrive. If you wish to trigger an error
|
85 | * if the fetched collection is empty, pass `{require: true}` as one of the
|
86 | * options to the {@link Collection#fetch fetch} call. A {@link
|
87 | * Collection#fetched "fetched"} event will be fired when records are
|
88 | * successfully retrieved. If you need to constrain the query performed by
|
89 | * `fetch`, you can call the {@link Collection#query query} method before
|
90 | * calling `fetch`.
|
91 | *
|
92 | * *If you'd like to only fetch specific columns, you may specify a `columns`
|
93 | * property in the options for the `fetch` call.*
|
94 | *
|
95 | * The `withRelated` option may be specified to fetch the models of the
|
96 | * collection, eager loading any specified {@link Relation relations} named on
|
97 | * the model. A single property, or an array of properties can be specified as
|
98 | * a value for the `withRelated` property. The results of these relation
|
99 | * queries will be loaded into a relations property on the respective models,
|
100 | * may be retrieved with the {@link Model#related related} method.
|
101 | *
|
102 | * @fires Collection#fetched
|
103 | * @throws {Collection.EmptyError}
|
104 | * Upon a sucessful query resulting in no records returns. Only fired if `require: true` is passed as an option.
|
105 | *
|
106 | * @param {Object=} options
|
107 | * @param {Boolean} [options.require=false] Trigger a {@link Collection.EmptyError} if no records are found.
|
108 | * @param {string|string[]} [options.withRelated=[]] A relation, or list of relations, to be eager loaded as part of the `fetch` operation.
|
109 | * @returns {Promise<Collection>}
|
110 | */
|
111 | fetch: Promise.method(function(options) {
|
112 | options = options ? _.clone(options) : {};
|
113 | return (
|
114 | this.sync(options)
|
115 | .select()
|
116 | .bind(this)
|
117 | .tap(function(response) {
|
118 | if (!response || response.length === 0) {
|
119 | throw new this.constructor.EmptyError('EmptyResponse');
|
120 | }
|
121 | })
|
122 |
|
123 | // Now, load all of the data onto the collection as necessary.
|
124 | .tap(function(response) {
|
125 | return this._handleResponse(response, options);
|
126 | })
|
127 |
|
128 | // If the "withRelated" is specified, we also need to eager load all of the
|
129 | // data on the collection, as a side-effect, before we ultimately jump into the
|
130 | // next step of the collection. Since the `columns` are only relevant to the current
|
131 | // level, ensure those are omitted from the options.
|
132 | .tap(function(response) {
|
133 | if (options.withRelated) {
|
134 | return this._handleEager(response, _.omit(options, 'columns'));
|
135 | }
|
136 | })
|
137 | .tap(function(response) {
|
138 | /**
|
139 | * @event Collection#fetched
|
140 | *
|
141 | * @description
|
142 | * Fired after a `fetch` operation. A promise may be returned from the
|
143 | * event handler for async behaviour.
|
144 | *
|
145 | * @param {Collection} collection The collection performing the {@link Collection#fetch}.
|
146 | * @param {Object} response Knex query response.
|
147 | * @param {Object} options Options object passed to {@link Collection#fetch fetch}.
|
148 | * @returns {Promise}
|
149 | */
|
150 | return this.triggerThen('fetched', this, response, options);
|
151 | })
|
152 | .catch(this.constructor.EmptyError, function(err) {
|
153 | if (options.require) {
|
154 | throw err;
|
155 | }
|
156 | this.reset([], {silent: true});
|
157 | })
|
158 | .return(this)
|
159 | );
|
160 | }),
|
161 |
|
162 | /**
|
163 | * @method Collection#count
|
164 | * @since 0.8.2
|
165 | * @description
|
166 | *
|
167 | * Get the number of records in the collection's table.
|
168 | *
|
169 | * @example
|
170 | *
|
171 | * // select count(*) from shareholders where company_id = 1 and share > 0.1;
|
172 | * Company.forge({id:1})
|
173 | * .shareholders()
|
174 | * .query('where', 'share', '>', '0.1')
|
175 | * .count()
|
176 | * .then(function(count) {
|
177 | * assert(count === 3);
|
178 | * });
|
179 | *
|
180 | * @param {string} [column='*']
|
181 | * Specify a column to count - rows with null values in this column will be excluded.
|
182 | * @param {Object=} options
|
183 | * Hash of options.
|
184 | * @returns {Promise<Number>}
|
185 | * A promise resolving to the number of matching rows.
|
186 | */
|
187 | count: Promise.method(function(column, options) {
|
188 | if (!_.isString(column)) {
|
189 | options = column;
|
190 | column = undefined;
|
191 | }
|
192 | if (options) options = _.clone(options);
|
193 | return this.sync(options).count(column);
|
194 | }),
|
195 |
|
196 | /**
|
197 | * @method Collection#fetchOne
|
198 | * @description
|
199 | *
|
200 | * Fetch and return a single {@link Model model} from the collection,
|
201 | * maintaining any {@link Relation relation} data from the collection, and
|
202 | * any {@link Collection#query query} parameters that have already been passed
|
203 | * to the collection. Especially helpful on relations, where you would only
|
204 | * like to return a single model from the associated collection.
|
205 | *
|
206 | * @example
|
207 | *
|
208 | * // select * from authors where site_id = 1 and id = 2 limit 1;
|
209 | * new Site({id:1})
|
210 | * .authors()
|
211 | * .query({where: {id: 2}})
|
212 | * .fetchOne()
|
213 | * .then(function(model) {
|
214 | * // ...
|
215 | * });
|
216 | *
|
217 | * @param {Object=} options
|
218 | * @param {Boolean} [options.require=false]
|
219 | * If `true`, will reject the returned response with a {@link
|
220 | * Model.NotFoundError NotFoundError} if no result is found.
|
221 | * @param {(string|string[])} [options.columns='*']
|
222 | * Limit the number of columns fetched.
|
223 | * @param {Transaction} [options.transacting]
|
224 | * Optionally run the query in a transaction.
|
225 | * @param {string} [options.lock]
|
226 | * Type of row-level lock to use. Valid options are `forShare` and
|
227 | * `forUpdate`. This only works in conjunction with the `transacting`
|
228 | * option, and requires a database that supports it.
|
229 | *
|
230 | * @throws {Model.NotFoundError}
|
231 | * @returns {Promise<Model|null>}
|
232 | * A promise resolving to the fetched {@link Model model} or `null` if none exists.
|
233 | */
|
234 | fetchOne: Promise.method(function(options) {
|
235 | const model = new this.model();
|
236 | model._knex = this.query().clone();
|
237 | this.resetQuery();
|
238 | if (this.relatedData) model.relatedData = this.relatedData;
|
239 | return model.fetch(options);
|
240 | }),
|
241 |
|
242 | /**
|
243 | * @method Collection#load
|
244 | * @description
|
245 | * `load` is used to eager load relations onto a Collection, in a similar way
|
246 | * that the `withRelated` property works on {@link Collection#fetch fetch}.
|
247 | * Nested eager loads can be specified by separating the nested relations with
|
248 | * `'.'`.
|
249 | *
|
250 | * @param {string|string[]} relations The relation, or relations, to be loaded.
|
251 | * @param {Object=} options Hash of options.
|
252 | * @param {Transaction=} options.transacting
|
253 | * @param {string=} options.lock
|
254 | * Type of row-level lock to use. Valid options are `forShare` and
|
255 | * `forUpdate`. This only works in conjunction with the `transacting`
|
256 | * option, and requires a database that supports it.
|
257 | *
|
258 | * @returns {Promise<Collection>} A promise resolving to this {@link
|
259 | * Collection collection}
|
260 | */
|
261 | load: Promise.method(function(relations, options) {
|
262 | if (!Array.isArray(relations)) relations = [relations];
|
263 | options = _.assignIn({}, options, {
|
264 | shallow: true,
|
265 | withRelated: relations
|
266 | });
|
267 | return new EagerRelation(this.models, this.toJSON(options), new this.model()).fetch(options).return(this);
|
268 | }),
|
269 |
|
270 | /**
|
271 | * @method Collection#create
|
272 | * @description
|
273 | *
|
274 | * Convenience method to create a new {@link Model model} instance within a
|
275 | * collection. Equivalent to instantiating a model with a hash of {@link
|
276 | * Model#attributes attributes}, {@link Model#save saving} the model to the
|
277 | * database then adding the model to the collection.
|
278 | *
|
279 | * When used on a relation, `create` will automatically set foreign key
|
280 | * attributes before persisting the `Model`.
|
281 | *
|
282 | * ```
|
283 | * const { courses, ...attributes } = req.body;
|
284 | *
|
285 | * Student.forge(attributes).save().tap(student =>
|
286 | * Promise.map(courses, course => student.related('courses').create(course))
|
287 | * ).then(student =>
|
288 | * res.status(200).send(student)
|
289 | * ).catch(error =>
|
290 | * res.status(500).send(error.message)
|
291 | * );
|
292 | * ```
|
293 | *
|
294 | * @param {Object} model A set of attributes to be set on the new model.
|
295 | * @param {Object=} options
|
296 | * @param {Transaction=} options.transacting
|
297 | *
|
298 | * @returns {Promise<Model>} A promise resolving with the new {@link Modle
|
299 | * model}.
|
300 | */
|
301 | create: Promise.method(function(model, options) {
|
302 | options = options != null ? _.clone(options) : {};
|
303 | const relatedData = this.relatedData;
|
304 | model = this._prepareModel(model, options);
|
305 |
|
306 | // If we've already added things on the query chain,
|
307 | // these are likely intended for the model.
|
308 | if (this._knex) {
|
309 | model._knex = this._knex;
|
310 | this.resetQuery();
|
311 | }
|
312 | return Helpers.saveConstraints(model, relatedData)
|
313 | .save(null, options)
|
314 | .bind(this)
|
315 | .then(function() {
|
316 | if (relatedData && relatedData.type === 'belongsToMany') {
|
317 | return this.attach(model, _.omit(options, 'query'));
|
318 | }
|
319 | })
|
320 | .then(function() {
|
321 | this.add(model, options);
|
322 | })
|
323 | .return(model);
|
324 | }),
|
325 |
|
326 | /**
|
327 | * @method Collection#resetQuery
|
328 | * @description
|
329 | * Used to reset the internal state of the current query builder instance.
|
330 | * This method is called internally each time a database action is completed
|
331 | * by {@link Sync}.
|
332 | *
|
333 | * @returns {Collection} Self, this method is chainable.
|
334 | */
|
335 | resetQuery: function() {
|
336 | this._knex = null;
|
337 | return this;
|
338 | },
|
339 |
|
340 | /**
|
341 | * @method Collection#query
|
342 | * @description
|
343 | *
|
344 | * `query` is used to tap into the underlying Knex query builder instance for
|
345 | * the current collection. If called with no arguments, it will return the
|
346 | * query builder directly. Otherwise, it will call the specified `method` on
|
347 | * the query builder, applying any additional arguments from the
|
348 | * `collection.query` call. If the `method` argument is a function, it will be
|
349 | * called with the Knex query builder as the context and the first argument.
|
350 | *
|
351 | * @example
|
352 | *
|
353 | * let qb = collection.query();
|
354 | * qb.where({id: 1}).select().then(function(resp) {
|
355 | * // ...
|
356 | * });
|
357 | *
|
358 | * collection.query(function(qb) {
|
359 | * qb.where('id', '>', 5).andWhere('first_name', '=', 'Test');
|
360 | * }).fetch()
|
361 | * .then(function(collection) {
|
362 | * // ...
|
363 | * });
|
364 | *
|
365 | * collection
|
366 | * .query('where', 'other_id', '=', '5')
|
367 | * .fetch()
|
368 | * .then(function(collection) {
|
369 | * // ...
|
370 | * });
|
371 | *
|
372 | * @param {function|Object|...string=} arguments The query method.
|
373 | * @returns {Collection|QueryBuilder}
|
374 | * Will return this model or, if called with no arguments, the underlying query builder.
|
375 | *
|
376 | * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`}
|
377 | */
|
378 | query: function() {
|
379 | return Helpers.query(this, Array.from(arguments));
|
380 | },
|
381 |
|
382 | /**
|
383 | * @method Collection#orderBy
|
384 | * @since 0.9.3
|
385 | * @description
|
386 | *
|
387 | * Specifies the column to sort on and sort order.
|
388 | *
|
389 | * The order parameter is optional, and defaults to 'ASC'. You may
|
390 | * also specify 'DESC' order by prepending a hyphen to the sort column
|
391 | * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`.
|
392 | *
|
393 | * Unless specified using dot notation (i.e., "table.column"), the default
|
394 | * table will be the table name of the model `orderBy` was called on.
|
395 | *
|
396 | * @example
|
397 | *
|
398 | * Cars.forge().orderBy('color', 'ASC').fetch()
|
399 | * .then(function (rows) { // ...
|
400 | *
|
401 | * @param sort {string}
|
402 | * Column to sort on
|
403 | * @param order {string}
|
404 | * Ascending ('ASC') or descending ('DESC') order
|
405 | */
|
406 | orderBy() {
|
407 | return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments)));
|
408 | },
|
409 |
|
410 | /**
|
411 | * @method Collection#query
|
412 | * @private
|
413 | * @description Creates and returns a new `Bookshelf.Sync` instance.
|
414 | */
|
415 | sync: function(options) {
|
416 | return new Sync(this, options);
|
417 | },
|
418 |
|
419 | /* Ensure that QueryBuilder is copied on clone. */
|
420 | clone() {
|
421 | const cloned = BookshelfCollection.__super__.clone.apply(this, arguments);
|
422 | if (this._knex != null) {
|
423 | cloned._knex = cloned._builder(this._knex.clone());
|
424 | }
|
425 | return cloned;
|
426 | },
|
427 |
|
428 | /**
|
429 | * @method Collection#_handleResponse
|
430 | * @private
|
431 | * @description
|
432 | * Handles the response data for the collection, returning from the
|
433 | * collection's `fetch` call.
|
434 | */
|
435 | _handleResponse: function(response, options) {
|
436 | const relatedData = this.relatedData;
|
437 |
|
438 | this.set(response, {
|
439 | merge: options.merge,
|
440 | remove: options.remove,
|
441 | silent: true,
|
442 | parse: true
|
443 | }).invokeMap(function() {
|
444 | this.formatTimestamps();
|
445 | this._reset();
|
446 | this._previousAttributes = _.cloneDeep(this.attributes);
|
447 | });
|
448 |
|
449 | if (relatedData && relatedData.isJoined()) {
|
450 | relatedData.parsePivot(this.models);
|
451 | }
|
452 | },
|
453 |
|
454 | /**
|
455 | * @method Collection#_handleEager
|
456 | * @private
|
457 | * @description
|
458 | * Handle the related data loading on the collection.
|
459 | */
|
460 | _handleEager: function(response, options) {
|
461 | return new EagerRelation(this.models, response, new this.model()).fetch(options);
|
462 | }
|
463 | },
|
464 | {
|
465 | extended: function(child) {
|
466 | /**
|
467 | * @class Collection.EmptyError
|
468 | * @description
|
469 | * Thrown when no records are found by {@link Collection#fetch fetch},
|
470 | * {@link Model#fetchAll}, or {@link Model.fetchAll} when called with
|
471 | * the `{require: true}` option.
|
472 | */
|
473 | child.EmptyError = createError(this.EmptyError);
|
474 | }
|
475 | }
|
476 | ));
|
477 |
|
478 | BookshelfCollection.EmptyError = Errors.EmptyError;
|