@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.
@@ -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;