@depup/bookshelf 1.2.0-depup.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +18 -0
- package/.nycrc.yml +3 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +764 -0
- package/LICENSE +22 -0
- package/README.md +32 -0
- package/bookshelf.js +8 -0
- package/changes.json +14 -0
- package/lib/base/collection.js +802 -0
- package/lib/base/eager.js +111 -0
- package/lib/base/events.js +129 -0
- package/lib/base/model.js +995 -0
- package/lib/base/relation.js +69 -0
- package/lib/bookshelf.js +558 -0
- package/lib/collection.js +545 -0
- package/lib/constants.js +2 -0
- package/lib/eager.js +122 -0
- package/lib/errors.js +36 -0
- package/lib/extend.js +41 -0
- package/lib/helpers.js +212 -0
- package/lib/model.js +1568 -0
- package/lib/relation.js +947 -0
- package/lib/sync.js +244 -0
- package/package.json +111 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
// Base Collection
|
|
2
|
+
// ---------------
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const _ = require('lodash');
|
|
6
|
+
const inherits = require('util').inherits;
|
|
7
|
+
|
|
8
|
+
const Events = require('./events');
|
|
9
|
+
const Promise = require('bluebird');
|
|
10
|
+
const ModelBase = require('./model');
|
|
11
|
+
const extend = require('../extend');
|
|
12
|
+
|
|
13
|
+
// List of attributes attached directly from the constructor's options object.
|
|
14
|
+
//
|
|
15
|
+
// RE: 'relatedData'
|
|
16
|
+
// It's okay for two `Collection`s to share a `Relation` instance.
|
|
17
|
+
// `relatedData` does not mutate itself after declaration. This is only
|
|
18
|
+
// here because `clone` needs to duplicate this property. It should not
|
|
19
|
+
// be documented as a valid argument for consumer code.
|
|
20
|
+
//
|
|
21
|
+
// RE: 'attach', 'detach', 'updatePivot', 'withPivot', '_processPivot', '_processPlainPivot', '_processModelPivot'
|
|
22
|
+
// It's okay to whitelist also given method references to be copied when cloning
|
|
23
|
+
// a collection. These methods are present only when `relatedData` is present and
|
|
24
|
+
// its `type` is 'belongsToMany'. So it is safe to put them in the list and use them
|
|
25
|
+
// without any additional verification.
|
|
26
|
+
// These should not be documented as a valid arguments for consumer code.
|
|
27
|
+
const collectionProps = [
|
|
28
|
+
'model',
|
|
29
|
+
'comparator',
|
|
30
|
+
'relatedData',
|
|
31
|
+
// `belongsToMany` pivotal collection properties
|
|
32
|
+
'attach',
|
|
33
|
+
'detach',
|
|
34
|
+
'updatePivot',
|
|
35
|
+
'withPivot',
|
|
36
|
+
'_processPivot',
|
|
37
|
+
'_processPlainPivot',
|
|
38
|
+
'_processModelPivot'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @class CollectionBase
|
|
43
|
+
* @extends Events
|
|
44
|
+
* @inheritdoc
|
|
45
|
+
*/
|
|
46
|
+
function CollectionBase(models, options) {
|
|
47
|
+
if (options) _.extend(this, _.pick(options, collectionProps));
|
|
48
|
+
this._reset();
|
|
49
|
+
this.initialize.apply(this, arguments);
|
|
50
|
+
if (!_.isFunction(this.model)) {
|
|
51
|
+
throw new Error('A valid `model` constructor must be defined for all collections.');
|
|
52
|
+
}
|
|
53
|
+
if (models) this.reset(models, _.extend({silent: true}, options));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Registers an event listener.
|
|
58
|
+
*
|
|
59
|
+
* @method CollectionBase#on
|
|
60
|
+
* @example
|
|
61
|
+
* const ships = new bookshelf.Collection
|
|
62
|
+
* ships.on('fetched', function(collection) {
|
|
63
|
+
* // Do something after the data has been fetched from the database
|
|
64
|
+
* })
|
|
65
|
+
* @see Events#on
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @method CollectionBase#off
|
|
70
|
+
* @example
|
|
71
|
+
*
|
|
72
|
+
* ships.off('fetched') // Remove the 'fetched' event listener
|
|
73
|
+
*
|
|
74
|
+
* @see Events#off
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @method CollectionBase#trigger
|
|
79
|
+
* @example
|
|
80
|
+
*
|
|
81
|
+
* ships.trigger('fetched')
|
|
82
|
+
*
|
|
83
|
+
* @see Events#trigger
|
|
84
|
+
*/
|
|
85
|
+
inherits(CollectionBase, Events);
|
|
86
|
+
|
|
87
|
+
// Copied over from Backbone.
|
|
88
|
+
const setOptions = {add: true, remove: true, merge: true};
|
|
89
|
+
const addOptions = {add: true, remove: false};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @member {Number}
|
|
93
|
+
* @default 0
|
|
94
|
+
* @description
|
|
95
|
+
*
|
|
96
|
+
* This is the total number of models in the collection. Note that this may not represent how many
|
|
97
|
+
* models there are in total in the database.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
*
|
|
101
|
+
* var vanHalen = new bookshelf.Collection([eddie, alex, stone, roth]);
|
|
102
|
+
* console.log(vanHalen.length) // 4
|
|
103
|
+
*/
|
|
104
|
+
CollectionBase.prototype.length = 0;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @method CollectionBase#initialize
|
|
108
|
+
* @description
|
|
109
|
+
* Called by the {@link Collection Collection constructor} when creating a new instance.
|
|
110
|
+
* Override this function to add custom initialization, such as event listeners.
|
|
111
|
+
* Because plugins may override this method in subclasses, make sure to call
|
|
112
|
+
* your super (extended) class. e.g.
|
|
113
|
+
*
|
|
114
|
+
* initialize: function() {
|
|
115
|
+
* this.constructor.__super__.initialize.apply(this, arguments);
|
|
116
|
+
* // Your initialization code ...
|
|
117
|
+
* }
|
|
118
|
+
*
|
|
119
|
+
* @see Collection
|
|
120
|
+
*/
|
|
121
|
+
CollectionBase.prototype.initialize = function() {};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @method
|
|
125
|
+
* @private
|
|
126
|
+
* @description
|
|
127
|
+
* The `tableName` on the associated Model, used in relation building.
|
|
128
|
+
* @returns {string} The {@link Model#tableName tableName} of the associated model.
|
|
129
|
+
*/
|
|
130
|
+
CollectionBase.prototype.tableName = function() {
|
|
131
|
+
return _.result(this.model.prototype, 'tableName');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns the first model in the collection or `undefined` if the collection is empty.
|
|
136
|
+
*
|
|
137
|
+
* @return {Model|undefined} The first model or `undefined`.
|
|
138
|
+
*/
|
|
139
|
+
CollectionBase.prototype.first = function() {
|
|
140
|
+
return this.at(0);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns the last model in the collection or `undefined` if the collection is empty.
|
|
145
|
+
*
|
|
146
|
+
* @return {Model|undefined} The last model or `undefined`.
|
|
147
|
+
*/
|
|
148
|
+
CollectionBase.prototype.last = function() {
|
|
149
|
+
return this.slice(-1)[0];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @method
|
|
154
|
+
* @private
|
|
155
|
+
* @description
|
|
156
|
+
* The `idAttribute` on the associated Model, used in relation building.
|
|
157
|
+
* @returns {string} The {@link Model#idAttribute idAttribute} of the associated model.
|
|
158
|
+
*/
|
|
159
|
+
CollectionBase.prototype.idAttribute = function() {
|
|
160
|
+
return this.model.prototype.idAttribute;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @method
|
|
165
|
+
* @private
|
|
166
|
+
* @description
|
|
167
|
+
* When keying a collection by ID, ensure that it is safe to use as a key
|
|
168
|
+
* @param {any} id
|
|
169
|
+
* @return {string|number} The id safe for using as a key in a collection
|
|
170
|
+
*/
|
|
171
|
+
CollectionBase.prototype.idKey = function(id) {
|
|
172
|
+
return _.isBuffer(id) ? id.toString('hex') : id;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
CollectionBase.prototype.toString = function() {
|
|
176
|
+
return '[Object Collection]';
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @method
|
|
181
|
+
* @description
|
|
182
|
+
*
|
|
183
|
+
* Return a raw array of the collection's {@link Model#attributes
|
|
184
|
+
* attributes} for JSON stringification. If the {@link Model models} have any
|
|
185
|
+
* relations defined, this will also call {@link Model#toJSON toJSON} on
|
|
186
|
+
* each of the related objects, and include them on the object unless
|
|
187
|
+
* `{shallow: true}` is passed as an option.
|
|
188
|
+
*
|
|
189
|
+
* `serialize` is called internally by {@link Collection#toJSON toJSON}.
|
|
190
|
+
* Override this function if you want to customize its output.
|
|
191
|
+
*
|
|
192
|
+
* @param {Object=} options
|
|
193
|
+
* @param {Boolean} [options.shallow=false] Exclude relations.
|
|
194
|
+
* @param {Boolean} [options.omitPivot=false] Exclude pivot values.
|
|
195
|
+
* @param {Boolean} [options.omitNew=false] Exclude models that return true for isNew.
|
|
196
|
+
* @returns {Object} Serialized model as a plain object.
|
|
197
|
+
*/
|
|
198
|
+
CollectionBase.prototype.serialize = function(options) {
|
|
199
|
+
return this.invokeMap('toJSON', options).filter(_.negate(_.isNull));
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @method
|
|
204
|
+
* @description
|
|
205
|
+
*
|
|
206
|
+
* Called automatically by {@link
|
|
207
|
+
* https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior
|
|
208
|
+
* `JSON.stringify`}. To customize serialization, override {@link
|
|
209
|
+
* Collection#serialize serialize}.
|
|
210
|
+
*
|
|
211
|
+
* @param {options} Options passed to {@link Collection#serialize}.
|
|
212
|
+
*/
|
|
213
|
+
CollectionBase.prototype.toJSON = function(options) {
|
|
214
|
+
return this.serialize(options);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @method
|
|
219
|
+
* @description
|
|
220
|
+
*
|
|
221
|
+
* The set method performs a smart update of the collection with the passed
|
|
222
|
+
* model or list of models by following the following rules:
|
|
223
|
+
* - If a model in the list isn't yet in the collection it will be added
|
|
224
|
+
* - if the model is already in the collection its attributes will be merged
|
|
225
|
+
* - if the collection contains any models that aren't present in the list,
|
|
226
|
+
* they'll be removed.
|
|
227
|
+
*
|
|
228
|
+
* If you'd like to customize the behavior, you can do so with the `add`,
|
|
229
|
+
* `merge` and `remove` options.
|
|
230
|
+
*
|
|
231
|
+
* Since version 0.14.0 if both `remove` and `merge` options are set to
|
|
232
|
+
* `false`, then any duplicate models present will be added to the collection,
|
|
233
|
+
* otherwise they will either be removed or merged, according to the chosen
|
|
234
|
+
* option.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
*
|
|
238
|
+
* var vanHalen = new bookshelf.Collection([eddie, alex, stone, roth]);
|
|
239
|
+
* vanHalen.set([eddie, alex, stone, hagar]);
|
|
240
|
+
*
|
|
241
|
+
* @param {Object[]|Model[]|Object|Model} models One or more models or raw
|
|
242
|
+
* attribute objects.
|
|
243
|
+
* @param {Object=} options
|
|
244
|
+
* Options for controlling how models are added or removed.
|
|
245
|
+
* @param {Boolean=} options.add=true
|
|
246
|
+
* If set to `true` it will add any new models to the collection, otherwise
|
|
247
|
+
* any new models will be ignored.
|
|
248
|
+
* @param {Boolean=} options.merge=true
|
|
249
|
+
* If set to `true` it will merge the attributes of duplicate models with the
|
|
250
|
+
* attributes of existing models in the collection, otherwise duplicate
|
|
251
|
+
* models in the list will be ignored.
|
|
252
|
+
* @param {Boolean=} options.remove=true
|
|
253
|
+
* If set to `true` any models in the collection that are not in the list
|
|
254
|
+
* will be removed from the collection, otherwise they will be kept.
|
|
255
|
+
* @returns {Collection} Self, this method is chainable.
|
|
256
|
+
*/
|
|
257
|
+
CollectionBase.prototype.set = function(models, options) {
|
|
258
|
+
options = _.defaults({}, options, setOptions);
|
|
259
|
+
if (!Array.isArray(models)) models = models ? [models] : [];
|
|
260
|
+
if (options.parse) models = this.parse(models, options);
|
|
261
|
+
let i, l, id, model, attrs;
|
|
262
|
+
const at = options.at;
|
|
263
|
+
const targetModel = this.model;
|
|
264
|
+
const toAdd = [];
|
|
265
|
+
const toRemove = [];
|
|
266
|
+
const modelMap = {};
|
|
267
|
+
let order = options.add && options.remove ? [] : false;
|
|
268
|
+
|
|
269
|
+
// Turn bare objects into model references, and prevent invalid models
|
|
270
|
+
// from being added.
|
|
271
|
+
for (i = 0, l = models.length; i < l; i++) {
|
|
272
|
+
attrs = models[i];
|
|
273
|
+
if (attrs instanceof ModelBase) {
|
|
274
|
+
id = model = attrs;
|
|
275
|
+
} else {
|
|
276
|
+
id = attrs[targetModel.prototype.idAttribute];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If a duplicate is found, prevent it from being added and
|
|
280
|
+
// optionally merge it into the existing model.
|
|
281
|
+
const existing = this.get(id);
|
|
282
|
+
if (existing && (options.merge || options.remove)) {
|
|
283
|
+
if (options.remove) {
|
|
284
|
+
modelMap[existing.cid] = true;
|
|
285
|
+
}
|
|
286
|
+
if (options.merge) {
|
|
287
|
+
attrs = attrs === model ? model.attributes : attrs;
|
|
288
|
+
if (options.parse) attrs = existing.parse(attrs, options);
|
|
289
|
+
existing.set(attrs, options);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// This is a new model, push it to the `toAdd` list.
|
|
293
|
+
} else if (options.add) {
|
|
294
|
+
if (!(model = this._prepareModel(attrs, options))) continue;
|
|
295
|
+
toAdd.push(model);
|
|
296
|
+
this._byId[this.idKey(model.cid)] = model;
|
|
297
|
+
if (model.id != null) this._byId[this.idKey(model.id)] = model;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (order && !(existing && order.indexOf(existing) > -1)) order.push(existing || model);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Remove nonexistent models if appropriate.
|
|
304
|
+
if (options.remove) {
|
|
305
|
+
for (i = 0, l = this.length; i < l; ++i) {
|
|
306
|
+
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
|
|
307
|
+
}
|
|
308
|
+
if (toRemove.length) this.remove(toRemove, options);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// See if sorting is needed, update `length` and splice in new models.
|
|
312
|
+
if (toAdd.length || (order && order.length)) {
|
|
313
|
+
this.length += toAdd.length;
|
|
314
|
+
if (at != null) {
|
|
315
|
+
Array.prototype.splice.apply(this.models, [at, 0].concat(toAdd));
|
|
316
|
+
} else {
|
|
317
|
+
if (order) {
|
|
318
|
+
this.models.length = 0;
|
|
319
|
+
} else {
|
|
320
|
+
order = toAdd;
|
|
321
|
+
}
|
|
322
|
+
for (i = 0, l = order.length; i < l; ++i) {
|
|
323
|
+
this.models.push(order[i]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.silent) return this;
|
|
329
|
+
|
|
330
|
+
// Trigger `add` events.
|
|
331
|
+
for (i = 0, l = toAdd.length; i < l; i++) {
|
|
332
|
+
(model = toAdd[i]).trigger('add', model, this, options);
|
|
333
|
+
}
|
|
334
|
+
return this;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @method
|
|
339
|
+
* @private
|
|
340
|
+
* @description
|
|
341
|
+
* Prepare a model or hash of attributes to be added to this collection.
|
|
342
|
+
*/
|
|
343
|
+
CollectionBase.prototype._prepareModel = function(attrs, options) {
|
|
344
|
+
if (attrs instanceof ModelBase) return attrs;
|
|
345
|
+
return new this.model(attrs, options);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @method
|
|
350
|
+
* @private
|
|
351
|
+
* @description
|
|
352
|
+
* Run "Promise.map" over the models
|
|
353
|
+
*/
|
|
354
|
+
CollectionBase.prototype.mapThen = function(iterator, context) {
|
|
355
|
+
return Promise.bind(context)
|
|
356
|
+
.thenReturn(this.models)
|
|
357
|
+
.map(iterator);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @method
|
|
362
|
+
* @description
|
|
363
|
+
* Shortcut for calling `Promise.all` around a {@link Collection#invoke}, this
|
|
364
|
+
* will delegate to the collection's `invoke` method, resolving the promise with
|
|
365
|
+
* an array of responses all async (and sync) behavior has settled. Useful for
|
|
366
|
+
* bulk saving or deleting models:
|
|
367
|
+
*
|
|
368
|
+
* collection.invokeThen('save', null, options).then(function() {
|
|
369
|
+
* // ... all models in the collection have been saved
|
|
370
|
+
* });
|
|
371
|
+
*
|
|
372
|
+
* collection.invokeThen('destroy', options).then(function() {
|
|
373
|
+
* // ... all models in the collection have been destroyed
|
|
374
|
+
* });
|
|
375
|
+
*
|
|
376
|
+
* @param {string} method The {@link Model model} method to invoke.
|
|
377
|
+
* @param {...mixed} arguments Arguments to `method`.
|
|
378
|
+
* @returns {Promise<mixed[]>}
|
|
379
|
+
* Promise resolving to array of results from invocation.
|
|
380
|
+
*/
|
|
381
|
+
CollectionBase.prototype.invokeThen = function() {
|
|
382
|
+
return Promise.all(this.invokeMap.apply(this, arguments));
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* This iterator is used by the reduceThen method to ietrate over all models in the collection.
|
|
387
|
+
*
|
|
388
|
+
* @callback Collection~reduceThenIterator
|
|
389
|
+
* @param {mixed} acumulator
|
|
390
|
+
* @param {Model} model The current model being iterated over.
|
|
391
|
+
* @param {Number} index
|
|
392
|
+
* @param {Number} length Total number of models being iterated over.
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @method
|
|
397
|
+
* @description
|
|
398
|
+
* Iterate over all the models in the collection and reduce this array to a single value using the
|
|
399
|
+
* given iterator function.
|
|
400
|
+
* @see {@link http://bluebirdjs.com/docs/api/promise.reduce.html|Bluebird `Promise.reduce` reference}.
|
|
401
|
+
* @param {Collection~reduceThenIterator} iterator
|
|
402
|
+
* @param {mixed} initialValue
|
|
403
|
+
* @param {Object} context Bound to `this` in the `iterator` callback.
|
|
404
|
+
* @returns {Promise<mixed>}
|
|
405
|
+
* Promise resolving to the single result from the reduction.
|
|
406
|
+
*
|
|
407
|
+
*/
|
|
408
|
+
CollectionBase.prototype.reduceThen = function(iterator, initialValue, context) {
|
|
409
|
+
return Promise.bind(context)
|
|
410
|
+
.thenReturn(this.models)
|
|
411
|
+
.reduce(iterator, initialValue)
|
|
412
|
+
.bind();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
CollectionBase.prototype.fetch = function() {
|
|
416
|
+
return Promise.rejected('The fetch method has not been implemented');
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @method
|
|
421
|
+
* @description
|
|
422
|
+
*
|
|
423
|
+
* Add a {@link Model model}, or an array of models, to the collection. You may
|
|
424
|
+
* also pass raw attribute objects, which will be converted to proper models
|
|
425
|
+
* when being added to the collection.
|
|
426
|
+
*
|
|
427
|
+
* You can pass the `{at: index}` option to splice the model into the
|
|
428
|
+
* collection at the specified `index`.
|
|
429
|
+
*
|
|
430
|
+
* By default if you're adding models to the collection that are already
|
|
431
|
+
* present, they'll be ignored, unless you pass `{merge: true}`, in
|
|
432
|
+
* which case their {@link Model#attributes attributes} will be merged with the
|
|
433
|
+
* corresponding models.
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
*
|
|
437
|
+
* const ships = new bookshelf.Collection;
|
|
438
|
+
*
|
|
439
|
+
* ships.add([
|
|
440
|
+
* {name: "Flying Dutchman"},
|
|
441
|
+
* {name: "Black Pearl"}
|
|
442
|
+
* ]);
|
|
443
|
+
*
|
|
444
|
+
* @param {Object[]|Model[]|Object|Model} models
|
|
445
|
+
* One or more models or raw attribute objects.
|
|
446
|
+
* @param {Object=} options Options for controlling how models are added.
|
|
447
|
+
* @param {Boolean=} options.merge=false
|
|
448
|
+
* If set to `true` it will merge the attributes of duplicate models with the
|
|
449
|
+
* attributes of existing models in the collection.
|
|
450
|
+
* @param {Number=} options.at
|
|
451
|
+
* If set to a number equal to or greater than 0 it will splice the model
|
|
452
|
+
* into the collection at the specified index number.
|
|
453
|
+
* @returns {Collection} Self, this method is chainable.
|
|
454
|
+
*/
|
|
455
|
+
CollectionBase.prototype.add = function(models, options) {
|
|
456
|
+
return this.set(models, Object.assign({merge: false}, options, addOptions));
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Remove a {@link Model model}, or an array of models, from the collection. Note that this does not remove the affected
|
|
461
|
+
* models from the database. For that purpose you have to use the model's {@link Model#destroy destroy} method.
|
|
462
|
+
*
|
|
463
|
+
* If you wish to actually remove all the models in a collection from the database you can use this method:
|
|
464
|
+
*
|
|
465
|
+
* myCollection.invokeThen('destroy').then(() => {
|
|
466
|
+
* // models have been destroyed
|
|
467
|
+
* })
|
|
468
|
+
*
|
|
469
|
+
* @param {Model|Model[]} models The model, or models, to be removed.
|
|
470
|
+
* @param {Object} [options] Set of options for the operation.
|
|
471
|
+
* @param {Boolean} [options.silent] If set to `true` will not trigger a `remove` event on the removed model.
|
|
472
|
+
* @returns {Model|Model[]} The same value passed in the `models` argument.
|
|
473
|
+
*/
|
|
474
|
+
CollectionBase.prototype.remove = function(models, options) {
|
|
475
|
+
const singular = !Array.isArray(models);
|
|
476
|
+
models = singular ? [models] : _.clone(models);
|
|
477
|
+
options = options || {};
|
|
478
|
+
for (let i = 0; i < models.length; i++) {
|
|
479
|
+
const model = (models[i] = this.get(models[i]));
|
|
480
|
+
if (!model) continue;
|
|
481
|
+
delete this._byId[this.idKey(model.id)];
|
|
482
|
+
delete this._byId[model.cid];
|
|
483
|
+
const index = this.models.indexOf(model);
|
|
484
|
+
this.models.splice(index, 1);
|
|
485
|
+
this.length = this.length - 1;
|
|
486
|
+
if (!options.silent) {
|
|
487
|
+
options.index = index;
|
|
488
|
+
model.trigger('remove', model, this, options);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return singular ? models[0] : models;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @method
|
|
496
|
+
* @description
|
|
497
|
+
*
|
|
498
|
+
* Adding and removing models one at a time is all well and good, but sometimes
|
|
499
|
+
* you have so many models to change that you'd rather just update the
|
|
500
|
+
* collection in bulk. Use `reset` to replace a collection with a new list of
|
|
501
|
+
* models (or attribute hashes). Calling `collection.reset()` without passing
|
|
502
|
+
* any models as arguments will empty the entire collection.
|
|
503
|
+
*
|
|
504
|
+
* @param {Object[]|Model[]|Object|Model} models One or more models or raw
|
|
505
|
+
* attribute objects.
|
|
506
|
+
* @param {Object} options See {@link Collection#add add}.
|
|
507
|
+
* @returns {Model[]} Array of models.
|
|
508
|
+
*/
|
|
509
|
+
CollectionBase.prototype.reset = function(models, options) {
|
|
510
|
+
options = options || {};
|
|
511
|
+
options.previousModels = this.models;
|
|
512
|
+
this._reset();
|
|
513
|
+
models = this.set(models, Object.assign({silent: true}, options));
|
|
514
|
+
if (!options.silent) this.trigger('reset', this, options);
|
|
515
|
+
return models;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @method
|
|
520
|
+
* @description
|
|
521
|
+
* Add a model to the end of the collection.
|
|
522
|
+
* @param {Object[]|Model[]|Object|Model} model One or more models or raw
|
|
523
|
+
* attribute objects.
|
|
524
|
+
* @returns {Collection} Self, this method is chainable.
|
|
525
|
+
*/
|
|
526
|
+
CollectionBase.prototype.push = function(model, options) {
|
|
527
|
+
return this.add(model, _.extend({at: this.length}, options));
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @method
|
|
532
|
+
* @description
|
|
533
|
+
* Remove a model from the end of the collection.
|
|
534
|
+
*/
|
|
535
|
+
CollectionBase.prototype.pop = function(options) {
|
|
536
|
+
const model = this.at(this.length - 1);
|
|
537
|
+
this.remove(model, options);
|
|
538
|
+
return model;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* @method
|
|
543
|
+
* @description
|
|
544
|
+
* Add a model to the beginning of the collection.
|
|
545
|
+
*/
|
|
546
|
+
CollectionBase.prototype.unshift = function(model, options) {
|
|
547
|
+
return this.add(model, _.extend({at: 0}, options));
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @method
|
|
552
|
+
* @description
|
|
553
|
+
* Remove a model from the beginning of the collection.
|
|
554
|
+
*/
|
|
555
|
+
CollectionBase.prototype.shift = function(options) {
|
|
556
|
+
const model = this.at(0);
|
|
557
|
+
this.remove(model, options);
|
|
558
|
+
return model;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @method
|
|
563
|
+
* @description
|
|
564
|
+
* Slice out a sub-array of models from the collection.
|
|
565
|
+
*/
|
|
566
|
+
CollectionBase.prototype.slice = function() {
|
|
567
|
+
return Array.prototype.slice.apply(this.models, arguments);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @method
|
|
572
|
+
* @description
|
|
573
|
+
*
|
|
574
|
+
* Get a model from a collection, specified by an {@link Model#id id}, a {@link
|
|
575
|
+
* Model#cid cid}, or by passing in a {@link Model model}.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
*
|
|
579
|
+
* const book = library.get(110);
|
|
580
|
+
*
|
|
581
|
+
* @returns {Model} The model, or `undefined` if it is not in the collection.
|
|
582
|
+
*/
|
|
583
|
+
CollectionBase.prototype.get = function(obj) {
|
|
584
|
+
if (obj == null) return void 0;
|
|
585
|
+
return this._byId[this.idKey(obj.id)] || this._byId[obj.cid] || this._byId[this.idKey(obj)];
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @method
|
|
590
|
+
* @description
|
|
591
|
+
* Get a model from a collection, specified by index. Useful if your collection
|
|
592
|
+
* is sorted, and if your collection isn't sorted, `at` will still retrieve
|
|
593
|
+
* models in insertion order.
|
|
594
|
+
*/
|
|
595
|
+
CollectionBase.prototype.at = function(index) {
|
|
596
|
+
return this.models[index];
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* @method
|
|
601
|
+
* @private
|
|
602
|
+
* @description
|
|
603
|
+
* Force the collection to re-sort itself, based on a comporator defined on the model.
|
|
604
|
+
*/
|
|
605
|
+
CollectionBase.prototype.sort = function(options) {
|
|
606
|
+
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
|
|
607
|
+
options = options || {};
|
|
608
|
+
|
|
609
|
+
// Run sort based on type of `comparator`.
|
|
610
|
+
if (_.isString(this.comparator) || this.comparator.length === 1) {
|
|
611
|
+
this.models = this.sortBy(this.comparator, this);
|
|
612
|
+
} else {
|
|
613
|
+
this.models.sort(_.bind(this.comparator, this));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!options.silent) this.trigger('sort', this, options);
|
|
617
|
+
return this;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* @method
|
|
622
|
+
* @description
|
|
623
|
+
* Pluck an attribute from each model in the collection.
|
|
624
|
+
* @returns {mixed[]} An array of attribute values.
|
|
625
|
+
*/
|
|
626
|
+
CollectionBase.prototype.pluck = function(attr) {
|
|
627
|
+
return this.invokeMap('get', attr);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @method
|
|
632
|
+
* @description
|
|
633
|
+
* The `parse` method is called whenever a collection's data is returned in a
|
|
634
|
+
* {@link Collection#fetch fetch} call. The function is passed the raw
|
|
635
|
+
* database `response` array, and should return an array to be set on the
|
|
636
|
+
* collection. The default implementation is a no-op, simply passing through
|
|
637
|
+
* the JSON response.
|
|
638
|
+
*
|
|
639
|
+
* @param {Object[]} resp Raw database response array.
|
|
640
|
+
*/
|
|
641
|
+
CollectionBase.prototype.parse = function(resp) {
|
|
642
|
+
return resp;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* @method
|
|
647
|
+
* @description
|
|
648
|
+
* Create a new collection with an identical list of models as this one.
|
|
649
|
+
*/
|
|
650
|
+
CollectionBase.prototype.clone = function() {
|
|
651
|
+
// Iterate over the selected list of collection properties and invoke `clone` for
|
|
652
|
+
// each property that has a method for that porpose.
|
|
653
|
+
const clonedProps = _(this)
|
|
654
|
+
.pick(collectionProps)
|
|
655
|
+
.mapValues((val) => {
|
|
656
|
+
return val && typeof val.clone === 'function' ? val.clone() : val;
|
|
657
|
+
})
|
|
658
|
+
.value();
|
|
659
|
+
return new this.constructor(this.models, clonedProps);
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* @method
|
|
664
|
+
* @private
|
|
665
|
+
* @description
|
|
666
|
+
* Reset all internal state. Called when the collection is first initialized or reset.
|
|
667
|
+
*/
|
|
668
|
+
CollectionBase.prototype._reset = function() {
|
|
669
|
+
this.length = 0;
|
|
670
|
+
this.models = [];
|
|
671
|
+
this._byId = Object.create(null);
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// Make collection iterable in for of loops
|
|
675
|
+
CollectionBase.prototype[Symbol.iterator] = function*() {
|
|
676
|
+
yield* this.models;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* @method CollectionBase#forEach
|
|
681
|
+
* @see http://lodash.com/docs/#forEach
|
|
682
|
+
*/
|
|
683
|
+
/**
|
|
684
|
+
* @method CollectionBase#map
|
|
685
|
+
* @see http://lodash.com/docs/#map
|
|
686
|
+
*/
|
|
687
|
+
/**
|
|
688
|
+
* @method CollectionBase#reduce
|
|
689
|
+
* @see http://lodash.com/docs/#reduce
|
|
690
|
+
*/
|
|
691
|
+
/**
|
|
692
|
+
* @method CollectionBase#reduceRight
|
|
693
|
+
* @see http://lodash.com/docs/#reduceRight
|
|
694
|
+
*/
|
|
695
|
+
/**
|
|
696
|
+
* @method CollectionBase#find
|
|
697
|
+
* @see http://lodash.com/docs/#find
|
|
698
|
+
*/
|
|
699
|
+
/**
|
|
700
|
+
* @method CollectionBase#filter
|
|
701
|
+
* @see http://lodash.com/docs/#filter
|
|
702
|
+
*/
|
|
703
|
+
/**
|
|
704
|
+
* @method CollectionBase#reject
|
|
705
|
+
* @see http://lodash.com/docs/#reject
|
|
706
|
+
*/
|
|
707
|
+
/**
|
|
708
|
+
* @method CollectionBase#every
|
|
709
|
+
* @see http://lodash.com/docs/#every
|
|
710
|
+
*/
|
|
711
|
+
/**
|
|
712
|
+
* @method CollectionBase#some
|
|
713
|
+
* @see http://lodash.com/docs/#some
|
|
714
|
+
*/
|
|
715
|
+
/**
|
|
716
|
+
* @method CollectionBase#includes
|
|
717
|
+
* @see http://lodash.com/docs/#includes
|
|
718
|
+
*/
|
|
719
|
+
/**
|
|
720
|
+
* @method CollectionBase#invokeMap
|
|
721
|
+
* @see http://lodash.com/docs/#invokeMap
|
|
722
|
+
*/
|
|
723
|
+
/**
|
|
724
|
+
* @method CollectionBase#toArray
|
|
725
|
+
* @see http://lodash.com/docs/#toArray
|
|
726
|
+
*/
|
|
727
|
+
/**
|
|
728
|
+
* @method CollectionBase#isEmpty
|
|
729
|
+
* @see http://lodash.com/docs/#isEmpty
|
|
730
|
+
*/
|
|
731
|
+
// Lodash methods that we want to implement on the Collection.
|
|
732
|
+
// 90% of the core usefulness of Backbone Collections is actually implemented
|
|
733
|
+
// right here:
|
|
734
|
+
const methods = [
|
|
735
|
+
'forEach',
|
|
736
|
+
'map',
|
|
737
|
+
'reduce',
|
|
738
|
+
'reduceRight',
|
|
739
|
+
'find',
|
|
740
|
+
'filter',
|
|
741
|
+
'every',
|
|
742
|
+
'some',
|
|
743
|
+
'includes',
|
|
744
|
+
'invokeMap',
|
|
745
|
+
'toArray',
|
|
746
|
+
'isEmpty'
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
// Mix in each Lodash method as a proxy to `Collection#models`.
|
|
750
|
+
_.each(methods, function(method) {
|
|
751
|
+
CollectionBase.prototype[method] = function() {
|
|
752
|
+
return _[method].apply(_, [this.models].concat(Array.from(arguments)));
|
|
753
|
+
};
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* @method CollectionBase#groupBy
|
|
758
|
+
* @see http://lodash.com/docs/#groupBy
|
|
759
|
+
*/
|
|
760
|
+
// Underscore methods that we want to implement on the Collection.
|
|
761
|
+
/**
|
|
762
|
+
* @method CollectionBase#countBy
|
|
763
|
+
* @see http://lodash.com/docs/#countBy
|
|
764
|
+
*/
|
|
765
|
+
// Underscore methods that we want to implement on the Collection.
|
|
766
|
+
/**
|
|
767
|
+
* @method CollectionBase#sortBy
|
|
768
|
+
* @see http://lodash.com/docs/#sortBy
|
|
769
|
+
*/
|
|
770
|
+
// Lodash methods that take a property name as an argument.
|
|
771
|
+
const attributeMethods = ['groupBy', 'countBy', 'sortBy'];
|
|
772
|
+
|
|
773
|
+
// Use attributes instead of properties.
|
|
774
|
+
_.each(attributeMethods, function(method) {
|
|
775
|
+
CollectionBase.prototype[method] = function(value, context) {
|
|
776
|
+
const iterator = _.isFunction(value)
|
|
777
|
+
? value
|
|
778
|
+
: function(model) {
|
|
779
|
+
return model.get(value);
|
|
780
|
+
};
|
|
781
|
+
return _[method](this.models, _.bind(iterator, context));
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* @method Collection.extend
|
|
787
|
+
* @description
|
|
788
|
+
*
|
|
789
|
+
* To create a {@link Collection} class of your own, extend
|
|
790
|
+
* `Bookshelf.Collection`.
|
|
791
|
+
*
|
|
792
|
+
* @param {Object=} prototypeProperties
|
|
793
|
+
* Instance methods and properties to be attached to instances of the new
|
|
794
|
+
* class.
|
|
795
|
+
* @param {Object=} classProperties
|
|
796
|
+
* Class (ie. static) functions and properties to be attached to the
|
|
797
|
+
* constructor of the new class.
|
|
798
|
+
* @returns {Function} Constructor for new `Collection` subclass.
|
|
799
|
+
*/
|
|
800
|
+
CollectionBase.extend = extend;
|
|
801
|
+
|
|
802
|
+
module.exports = CollectionBase;
|