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 | * @tutorial events
|
141 | *
|
142 | * @description
|
143 | * Fired after a `fetch` operation. A promise may be returned from the
|
144 | * event handler for async behaviour.
|
145 | *
|
146 | * @param {Collection} collection The collection performing the {@link Collection#fetch}.
|
147 | * @param {Object} response Knex query response.
|
148 | * @param {Object} options Options object passed to {@link Collection#fetch fetch}.
|
149 | * @returns {Promise}
|
150 | */
|
151 | return this.triggerThen('fetched', this, response, options);
|
152 | })
|
153 | .catch(this.constructor.EmptyError, function(err) {
|
154 | if (options.require) {
|
155 | throw err;
|
156 | }
|
157 | this.reset([], {silent: true});
|
158 | })
|
159 | .return(this)
|
160 | );
|
161 | }),
|
162 |
|
163 | /**
|
164 | * @method Collection#count
|
165 | * @since 0.8.2
|
166 | * @description
|
167 | *
|
168 | * Get the number of records in the collection's table.
|
169 | *
|
170 | * @example
|
171 | *
|
172 | * // select count(*) from shareholders where company_id = 1 and share > 0.1;
|
173 | * Company.forge({id:1})
|
174 | * .shareholders()
|
175 | * .query('where', 'share', '>', '0.1')
|
176 | * .count()
|
177 | * .then(function(count) {
|
178 | * assert(count === 3);
|
179 | * });
|
180 | *
|
181 | * @param {string} [column='*']
|
182 | * Specify a column to count - rows with null values in this column will be excluded.
|
183 | * @param {Object=} options
|
184 | * Hash of options.
|
185 | * @returns {Promise<Number>}
|
186 | * A promise resolving to the number of matching rows.
|
187 | */
|
188 | count: Promise.method(function(column, options) {
|
189 | if (!_.isString(column)) {
|
190 | options = column;
|
191 | column = undefined;
|
192 | }
|
193 | if (options) options = _.clone(options);
|
194 | return this.sync(options).count(column);
|
195 | }),
|
196 |
|
197 | /**
|
198 | * @method Collection#fetchOne
|
199 | * @description
|
200 | *
|
201 | * Fetch and return a single {@link Model model} from the collection,
|
202 | * maintaining any {@link Relation relation} data from the collection, and
|
203 | * any {@link Collection#query query} parameters that have already been passed
|
204 | * to the collection. Especially helpful on relations, where you would only
|
205 | * like to return a single model from the associated collection.
|
206 | *
|
207 | * @example
|
208 | *
|
209 | * // select * from authors where site_id = 1 and id = 2 limit 1;
|
210 | * new Site({id:1})
|
211 | * .authors()
|
212 | * .query({where: {id: 2}})
|
213 | * .fetchOne()
|
214 | * .then(function(model) {
|
215 | * // ...
|
216 | * });
|
217 | *
|
218 | * @param {Object=} options
|
219 | * @param {Boolean} [options.require=false]
|
220 | * If `true`, will reject the returned response with a {@link
|
221 | * Model.NotFoundError NotFoundError} if no result is found.
|
222 | * @param {(string|string[])} [options.columns='*']
|
223 | * Limit the number of columns fetched.
|
224 | * @param {Transaction} [options.transacting]
|
225 | * Optionally run the query in a transaction.
|
226 | * @param {string} [options.lock]
|
227 | * Type of row-level lock to use. Valid options are `forShare` and
|
228 | * `forUpdate`. This only works in conjunction with the `transacting`
|
229 | * option, and requires a database that supports it.
|
230 | *
|
231 | * @throws {Model.NotFoundError}
|
232 | * @returns {Promise<Model|null>}
|
233 | * A promise resolving to the fetched {@link Model model} or `null` if none exists.
|
234 | */
|
235 | fetchOne: Promise.method(function(options) {
|
236 | const model = new this.model();
|
237 | model._knex = this.query().clone();
|
238 | this.resetQuery();
|
239 | if (this.relatedData) model.relatedData = this.relatedData;
|
240 | return model.fetch(options);
|
241 | }),
|
242 |
|
243 | /**
|
244 | * @method Collection#load
|
245 | * @description
|
246 | * `load` is used to eager load relations onto a Collection, in a similar way
|
247 | * that the `withRelated` property works on {@link Collection#fetch fetch}.
|
248 | * Nested eager loads can be specified by separating the nested relations with
|
249 | * `'.'`.
|
250 | *
|
251 | * @param {string|string[]} relations The relation, or relations, to be loaded.
|
252 | * @param {Object=} options Hash of options.
|
253 | * @param {Transaction=} options.transacting
|
254 | * @param {string=} options.lock
|
255 | * Type of row-level lock to use. Valid options are `forShare` and
|
256 | * `forUpdate`. This only works in conjunction with the `transacting`
|
257 | * option, and requires a database that supports it.
|
258 | *
|
259 | * @returns {Promise<Collection>} A promise resolving to this {@link
|
260 | * Collection collection}
|
261 | */
|
262 | load: Promise.method(function(relations, options) {
|
263 | if (!Array.isArray(relations)) relations = [relations];
|
264 | options = _.assignIn({}, options, {
|
265 | shallow: true,
|
266 | withRelated: relations
|
267 | });
|
268 | return new EagerRelation(this.models, this.toJSON(options), new this.model()).fetch(options).return(this);
|
269 | }),
|
270 |
|
271 | /**
|
272 | * @method Collection#create
|
273 | * @description
|
274 | *
|
275 | * Convenience method to create a new {@link Model model} instance within a
|
276 | * collection. Equivalent to instantiating a model with a hash of {@link
|
277 | * Model#attributes attributes}, {@link Model#save saving} the model to the
|
278 | * database then adding the model to the collection.
|
279 | *
|
280 | * When used on a relation, `create` will automatically set foreign key
|
281 | * attributes before persisting the `Model`.
|
282 | *
|
283 | * ```
|
284 | * const { courses, ...attributes } = req.body;
|
285 | *
|
286 | * Student.forge(attributes).save().tap(student =>
|
287 | * Promise.map(courses, course => student.related('courses').create(course))
|
288 | * ).then(student =>
|
289 | * res.status(200).send(student)
|
290 | * ).catch(error =>
|
291 | * res.status(500).send(error.message)
|
292 | * );
|
293 | * ```
|
294 | *
|
295 | * @param {Object} model A set of attributes to be set on the new model.
|
296 | * @param {Object=} options
|
297 | * @param {Transaction=} options.transacting
|
298 | *
|
299 | * @returns {Promise<Model>} A promise resolving with the new {@link Modle
|
300 | * model}.
|
301 | */
|
302 | create: Promise.method(function(model, options) {
|
303 | options = options != null ? _.clone(options) : {};
|
304 | const relatedData = this.relatedData;
|
305 | model = this._prepareModel(model, options);
|
306 |
|
307 | // If we've already added things on the query chain,
|
308 | // these are likely intended for the model.
|
309 | if (this._knex) {
|
310 | model._knex = this._knex;
|
311 | this.resetQuery();
|
312 | }
|
313 | return Helpers.saveConstraints(model, relatedData)
|
314 | .save(null, options)
|
315 | .bind(this)
|
316 | .then(function() {
|
317 | if (relatedData && relatedData.type === 'belongsToMany') {
|
318 | return this.attach(model, _.omit(options, 'query'));
|
319 | }
|
320 | })
|
321 | .then(function() {
|
322 | this.add(model, options);
|
323 | })
|
324 | .return(model);
|
325 | }),
|
326 |
|
327 | /**
|
328 | * @method Collection#resetQuery
|
329 | * @description
|
330 | * Used to reset the internal state of the current query builder instance.
|
331 | * This method is called internally each time a database action is completed
|
332 | * by {@link Sync}.
|
333 | *
|
334 | * @returns {Collection} Self, this method is chainable.
|
335 | */
|
336 | resetQuery: function() {
|
337 | this._knex = null;
|
338 | return this;
|
339 | },
|
340 |
|
341 | /**
|
342 | * @method Collection#query
|
343 | * @description
|
344 | *
|
345 | * `query` is used to tap into the underlying Knex query builder instance for
|
346 | * the current collection. If called with no arguments, it will return the
|
347 | * query builder directly. Otherwise, it will call the specified `method` on
|
348 | * the query builder, applying any additional arguments from the
|
349 | * `collection.query` call. If the `method` argument is a function, it will be
|
350 | * called with the Knex query builder as the context and the first argument.
|
351 | *
|
352 | * @example
|
353 | *
|
354 | * let qb = collection.query();
|
355 | * qb.where({id: 1}).select().then(function(resp) {
|
356 | * // ...
|
357 | * });
|
358 | *
|
359 | * collection.query(function(qb) {
|
360 | * qb.where('id', '>', 5).andWhere('first_name', '=', 'Test');
|
361 | * }).fetch()
|
362 | * .then(function(collection) {
|
363 | * // ...
|
364 | * });
|
365 | *
|
366 | * collection
|
367 | * .query('where', 'other_id', '=', '5')
|
368 | * .fetch()
|
369 | * .then(function(collection) {
|
370 | * // ...
|
371 | * });
|
372 | *
|
373 | * @param {function|Object|...string=} arguments The query method.
|
374 | * @returns {Collection|QueryBuilder}
|
375 | * Will return this model or, if called with no arguments, the underlying query builder.
|
376 | *
|
377 | * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`}
|
378 | */
|
379 | query: function() {
|
380 | return Helpers.query(this, Array.from(arguments));
|
381 | },
|
382 |
|
383 | /**
|
384 | * @method Collection#orderBy
|
385 | * @since 0.9.3
|
386 | * @description
|
387 | *
|
388 | * Specifies the column to sort on and sort order.
|
389 | *
|
390 | * The order parameter is optional, and defaults to 'ASC'. You may
|
391 | * also specify 'DESC' order by prepending a hyphen to the sort column
|
392 | * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`.
|
393 | *
|
394 | * Unless specified using dot notation (i.e., "table.column"), the default
|
395 | * table will be the table name of the model `orderBy` was called on.
|
396 | *
|
397 | * @example
|
398 | *
|
399 | * Cars.forge().orderBy('color', 'ASC').fetch()
|
400 | * .then(function (rows) { // ...
|
401 | *
|
402 | * @param sort {string}
|
403 | * Column to sort on
|
404 | * @param order {string}
|
405 | * Ascending ('ASC') or descending ('DESC') order
|
406 | */
|
407 | orderBy() {
|
408 | return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments)));
|
409 | },
|
410 |
|
411 | /**
|
412 | * @method Collection#query
|
413 | * @private
|
414 | * @description Creates and returns a new `Bookshelf.Sync` instance.
|
415 | */
|
416 | sync: function(options) {
|
417 | return new Sync(this, options);
|
418 | },
|
419 |
|
420 | /* Ensure that QueryBuilder is copied on clone. */
|
421 | clone() {
|
422 | const cloned = BookshelfCollection.__super__.clone.apply(this, arguments);
|
423 | if (this._knex != null) {
|
424 | cloned._knex = cloned._builder(this._knex.clone());
|
425 | }
|
426 | return cloned;
|
427 | },
|
428 |
|
429 | /**
|
430 | * @method Collection#_handleResponse
|
431 | * @private
|
432 | * @description
|
433 | * Handles the response data for the collection, returning from the
|
434 | * collection's `fetch` call.
|
435 | */
|
436 | _handleResponse: function(response, options) {
|
437 | const relatedData = this.relatedData;
|
438 |
|
439 | this.set(response, {
|
440 | merge: options.merge,
|
441 | remove: options.remove,
|
442 | silent: true,
|
443 | parse: true
|
444 | }).invokeMap(function() {
|
445 | this.formatTimestamps();
|
446 | this._reset();
|
447 | this._previousAttributes = _.cloneDeep(this.attributes);
|
448 | });
|
449 |
|
450 | if (relatedData && relatedData.isJoined()) {
|
451 | relatedData.parsePivot(this.models);
|
452 | }
|
453 | },
|
454 |
|
455 | /**
|
456 | * @method Collection#_handleEager
|
457 | * @private
|
458 | * @description
|
459 | * Handle the related data loading on the collection.
|
460 | */
|
461 | _handleEager: function(response, options) {
|
462 | return new EagerRelation(this.models, response, new this.model()).fetch(options);
|
463 | }
|
464 | },
|
465 | {
|
466 | extended: function(child) {
|
467 | /**
|
468 | * @class Collection.EmptyError
|
469 | * @description
|
470 | * Thrown when no records are found by {@link Collection#fetch fetch},
|
471 | * {@link Model#fetchAll}, or {@link Model.fetchAll} when called with
|
472 | * the `{require: true}` option.
|
473 | */
|
474 | child.EmptyError = createError(this.EmptyError);
|
475 | }
|
476 | }
|
477 | ));
|
478 |
|
479 | BookshelfCollection.EmptyError = Errors.EmptyError;
|