UNPKG

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