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