backbone-relational-hal-rails 0.1.1

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,2043 @@
1
+ /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
2
+ /**
3
+ * Backbone-relational.js 0.8.7
4
+ * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
5
+ *
6
+ * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
7
+ * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
8
+ * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
9
+ *
10
+ * Example:
11
+ *
12
+ Zoo = Backbone.RelationalModel.extend( {
13
+ relations: [ {
14
+ type: Backbone.HasMany,
15
+ key: 'animals',
16
+ relatedModel: 'Animal',
17
+ reverseRelation: {
18
+ key: 'livesIn',
19
+ includeInJSON: 'id'
20
+ // 'relatedModel' is automatically set to 'Zoo'; the 'relationType' to 'HasOne'.
21
+ }
22
+ } ],
23
+
24
+ toString: function() {
25
+ return this.get( 'name' );
26
+ }
27
+ } );
28
+
29
+ Animal = Backbone.RelationalModel.extend( {
30
+ toString: function() {
31
+ return this.get( 'species' );
32
+ }
33
+ } );
34
+
35
+ // Creating the zoo will give it a collection with one animal in it: the monkey.
36
+ // The animal created after that has a relation `livesIn` that points to the zoo it's currently associated with.
37
+ // If you instantiate (or fetch) the zebra later, it will automatically be added.
38
+
39
+ var zoo = new Zoo( {
40
+ name: 'Artis',
41
+ animals: [ { id: 'monkey-1', species: 'Chimp' }, 'lion-1', 'zebra-1' ]
42
+ } );
43
+
44
+ var lion = new Animal( { id: 'lion-1', species: 'Lion' } ),
45
+ monkey = zoo.get( 'animals' ).first(),
46
+ sameZoo = lion.get( 'livesIn' );
47
+ */
48
+ ( function( root, factory ) {
49
+ // Set up Backbone-relational for the environment. Start with AMD.
50
+ if ( typeof define === 'function' && define.amd ) {
51
+ define( [ 'exports', 'backbone', 'underscore' ], factory );
52
+ }
53
+ // Next for Node.js or CommonJS.
54
+ else if ( typeof exports !== 'undefined' ) {
55
+ factory( exports, require( 'backbone' ), require( 'underscore' ) );
56
+ }
57
+ // Finally, as a browser global. Use `root` here as it references `window`.
58
+ else {
59
+ factory( root, root.Backbone, root._ );
60
+ }
61
+ } ( this, function( exports, Backbone, _ ) {
62
+ "use strict";
63
+
64
+ Backbone.Relational = {
65
+ showWarnings: true
66
+ };
67
+
68
+ /**
69
+ * Semaphore mixin; can be used as both binary and counting.
70
+ **/
71
+ Backbone.Semaphore = {
72
+ _permitsAvailable: null,
73
+ _permitsUsed: 0,
74
+
75
+ acquire: function() {
76
+ if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
77
+ throw new Error( 'Max permits acquired' );
78
+ }
79
+ else {
80
+ this._permitsUsed++;
81
+ }
82
+ },
83
+
84
+ release: function() {
85
+ if ( this._permitsUsed === 0 ) {
86
+ throw new Error( 'All permits released' );
87
+ }
88
+ else {
89
+ this._permitsUsed--;
90
+ }
91
+ },
92
+
93
+ isLocked: function() {
94
+ return this._permitsUsed > 0;
95
+ },
96
+
97
+ setAvailablePermits: function( amount ) {
98
+ if ( this._permitsUsed > amount ) {
99
+ throw new Error( 'Available permits cannot be less than used permits' );
100
+ }
101
+ this._permitsAvailable = amount;
102
+ }
103
+ };
104
+
105
+ /**
106
+ * A BlockingQueue that accumulates items while blocked (via 'block'),
107
+ * and processes them when unblocked (via 'unblock').
108
+ * Process can also be called manually (via 'process').
109
+ */
110
+ Backbone.BlockingQueue = function() {
111
+ this._queue = [];
112
+ };
113
+ _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, {
114
+ _queue: null,
115
+
116
+ add: function( func ) {
117
+ if ( this.isBlocked() ) {
118
+ this._queue.push( func );
119
+ }
120
+ else {
121
+ func();
122
+ }
123
+ },
124
+
125
+ // Some of the queued events may trigger other blocking events. By
126
+ // copying the queue here it allows queued events to process closer to
127
+ // the natural order.
128
+ //
129
+ // queue events [ 'A', 'B', 'C' ]
130
+ // A handler of 'B' triggers 'D' and 'E'
131
+ // By copying `this._queue` this executes:
132
+ // [ 'A', 'B', 'D', 'E', 'C' ]
133
+ // The same order the would have executed if they didn't have to be
134
+ // delayed and queued.
135
+ process: function() {
136
+ var queue = this._queue;
137
+ this._queue = [];
138
+ while ( queue && queue.length ) {
139
+ queue.shift()();
140
+ }
141
+ },
142
+
143
+ block: function() {
144
+ this.acquire();
145
+ },
146
+
147
+ unblock: function() {
148
+ this.release();
149
+ if ( !this.isBlocked() ) {
150
+ this.process();
151
+ }
152
+ },
153
+
154
+ isBlocked: function() {
155
+ return this.isLocked();
156
+ }
157
+ });
158
+ /**
159
+ * Global event queue. Accumulates external events ('add:<key>', 'remove:<key>' and 'change:<key>')
160
+ * until the top-level object is fully initialized (see 'Backbone.RelationalModel').
161
+ */
162
+ Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
163
+
164
+ /**
165
+ * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
166
+ * Handles lookup for relations.
167
+ */
168
+ Backbone.Store = function() {
169
+ this._collections = [];
170
+ this._reverseRelations = [];
171
+ this._orphanRelations = [];
172
+ this._subModels = [];
173
+ this._modelScopes = [ exports ];
174
+ };
175
+ _.extend( Backbone.Store.prototype, Backbone.Events, {
176
+ /**
177
+ * Create a new `Relation`.
178
+ * @param {Backbone.RelationalModel} [model]
179
+ * @param {Object} relation
180
+ * @param {Object} [options]
181
+ */
182
+ initializeRelation: function( model, relation, options ) {
183
+ var type = !_.isString( relation.type ) ? relation.type : Backbone[ relation.type ] || this.getObjectByName( relation.type );
184
+ if ( type && type.prototype instanceof Backbone.Relation ) {
185
+ new type( model, relation, options ); // Also pushes the new Relation into `model._relations`
186
+ }
187
+ else {
188
+ Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid relation type!', relation );
189
+ }
190
+ },
191
+
192
+ /**
193
+ * Add a scope for `getObjectByName` to look for model types by name.
194
+ * @param {Object} scope
195
+ */
196
+ addModelScope: function( scope ) {
197
+ this._modelScopes.push( scope );
198
+ },
199
+
200
+ /**
201
+ * Remove a scope.
202
+ * @param {Object} scope
203
+ */
204
+ removeModelScope: function( scope ) {
205
+ this._modelScopes = _.without( this._modelScopes, scope );
206
+ },
207
+
208
+ /**
209
+ * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel'
210
+ * for a model later in 'setupSuperModel'.
211
+ *
212
+ * @param {Backbone.RelationalModel} subModelTypes
213
+ * @param {Backbone.RelationalModel} superModelType
214
+ */
215
+ addSubModels: function( subModelTypes, superModelType ) {
216
+ this._subModels.push({
217
+ 'superModelType': superModelType,
218
+ 'subModels': subModelTypes
219
+ });
220
+ },
221
+
222
+ /**
223
+ * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's
224
+ * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'.
225
+ *
226
+ * @param {Backbone.RelationalModel} modelType
227
+ */
228
+ setupSuperModel: function( modelType ) {
229
+ _.find( this._subModels, function( subModelDef ) {
230
+ return _.filter( subModelDef.subModels || [], function( subModelTypeName, typeValue ) {
231
+ var subModelType = this.getObjectByName( subModelTypeName );
232
+
233
+ if ( modelType === subModelType ) {
234
+ // Set 'modelType' as a child of the found superModel
235
+ subModelDef.superModelType._subModels[ typeValue ] = modelType;
236
+
237
+ // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'.
238
+ modelType._superModel = subModelDef.superModelType;
239
+ modelType._subModelTypeValue = typeValue;
240
+ modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute;
241
+ return true;
242
+ }
243
+ }, this ).length;
244
+ }, this );
245
+ },
246
+
247
+ /**
248
+ * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
249
+ * existing instances of 'model' in the store as well.
250
+ * @param {Object} relation
251
+ * @param {Backbone.RelationalModel} relation.model
252
+ * @param {String} relation.type
253
+ * @param {String} relation.key
254
+ * @param {String|Object} relation.relatedModel
255
+ */
256
+ addReverseRelation: function( relation ) {
257
+ var exists = _.any( this._reverseRelations, function( rel ) {
258
+ return _.all( relation || [], function( val, key ) {
259
+ return val === rel[ key ];
260
+ });
261
+ });
262
+
263
+ if ( !exists && relation.model && relation.type ) {
264
+ this._reverseRelations.push( relation );
265
+ this._addRelation( relation.model, relation );
266
+ this.retroFitRelation( relation );
267
+ }
268
+ },
269
+
270
+ /**
271
+ * Deposit a `relation` for which the `relatedModel` can't be resolved at the moment.
272
+ *
273
+ * @param {Object} relation
274
+ */
275
+ addOrphanRelation: function( relation ) {
276
+ var exists = _.any( this._orphanRelations, function( rel ) {
277
+ return _.all( relation || [], function( val, key ) {
278
+ return val === rel[ key ];
279
+ });
280
+ });
281
+
282
+ if ( !exists && relation.model && relation.type ) {
283
+ this._orphanRelations.push( relation );
284
+ }
285
+ },
286
+
287
+ /**
288
+ * Try to initialize any `_orphanRelation`s
289
+ */
290
+ processOrphanRelations: function() {
291
+ // Make sure to operate on a copy since we're removing while iterating
292
+ _.each( this._orphanRelations.slice( 0 ), function( rel ) {
293
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
294
+ if ( relatedModel ) {
295
+ this.initializeRelation( null, rel );
296
+ this._orphanRelations = _.without( this._orphanRelations, rel );
297
+ }
298
+ }, this );
299
+ },
300
+
301
+ /**
302
+ *
303
+ * @param {Backbone.RelationalModel.constructor} type
304
+ * @param {Object} relation
305
+ * @private
306
+ */
307
+ _addRelation: function( type, relation ) {
308
+ if ( !type.prototype.relations ) {
309
+ type.prototype.relations = [];
310
+ }
311
+ type.prototype.relations.push( relation );
312
+
313
+ _.each( type._subModels || [], function( subModel ) {
314
+ this._addRelation( subModel, relation );
315
+ }, this );
316
+ },
317
+
318
+ /**
319
+ * Add a 'relation' to all existing instances of 'relation.model' in the store
320
+ * @param {Object} relation
321
+ */
322
+ retroFitRelation: function( relation ) {
323
+ var coll = this.getCollection( relation.model, false );
324
+ coll && coll.each( function( model ) {
325
+ if ( !( model instanceof relation.model ) ) {
326
+ return;
327
+ }
328
+
329
+ new relation.type( model, relation );
330
+ }, this );
331
+ },
332
+
333
+ /**
334
+ * Find the Store's collection for a certain type of model.
335
+ * @param {Backbone.RelationalModel} type
336
+ * @param {Boolean} [create=true] Should a collection be created if none is found?
337
+ * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
338
+ */
339
+ getCollection: function( type, create ) {
340
+ if ( type instanceof Backbone.RelationalModel ) {
341
+ type = type.constructor;
342
+ }
343
+
344
+ var rootModel = type;
345
+ while ( rootModel._superModel ) {
346
+ rootModel = rootModel._superModel;
347
+ }
348
+
349
+ var coll = _.find( this._collections, function(item) {
350
+ return item.model === rootModel;
351
+ });
352
+
353
+ if ( !coll && create !== false ) {
354
+ coll = this._createCollection( rootModel );
355
+ }
356
+
357
+ return coll;
358
+ },
359
+
360
+ /**
361
+ * Find a model type on one of the modelScopes by name. Names are split on dots.
362
+ * @param {String} name
363
+ * @return {Object}
364
+ */
365
+ getObjectByName: function( name ) {
366
+ var parts = name.split( '.' ),
367
+ type = null;
368
+
369
+ _.find( this._modelScopes, function( scope ) {
370
+ type = _.reduce( parts || [], function( memo, val ) {
371
+ return memo ? memo[ val ] : undefined;
372
+ }, scope );
373
+
374
+ if ( type && type !== scope ) {
375
+ return true;
376
+ }
377
+ }, this );
378
+
379
+ return type;
380
+ },
381
+
382
+ _createCollection: function( type ) {
383
+ var coll;
384
+
385
+ // If 'type' is an instance, take its constructor
386
+ if ( type instanceof Backbone.RelationalModel ) {
387
+ type = type.constructor;
388
+ }
389
+
390
+ // Type should inherit from Backbone.RelationalModel.
391
+ if ( type.prototype instanceof Backbone.RelationalModel ) {
392
+ coll = new Backbone.Collection();
393
+ coll.model = type;
394
+
395
+ this._collections.push( coll );
396
+ }
397
+
398
+ return coll;
399
+ },
400
+
401
+ /**
402
+ * Find the attribute that is to be used as the `id` on a given object
403
+ * @param type
404
+ * @param {String|Number|Object|Backbone.RelationalModel} item
405
+ * @return {String|Number}
406
+ */
407
+ resolveIdForItem: function( type, item ) {
408
+ var id = _.isString( item ) || _.isNumber( item ) ? item : null;
409
+
410
+ if ( id === null ) {
411
+ if ( item instanceof Backbone.RelationalModel ) {
412
+ id = item.id;
413
+ }
414
+ else if ( _.isObject( item ) ) {
415
+ id = item[ type.prototype.idAttribute ];
416
+ }
417
+ }
418
+
419
+ // Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179')
420
+ if ( !id && id !== 0 ) {
421
+ id = null;
422
+ }
423
+
424
+ return id;
425
+ },
426
+
427
+ /**
428
+ * Find a specific model of a certain `type` in the store
429
+ * @param type
430
+ * @param {String|Number|Object|Backbone.RelationalModel} item
431
+ */
432
+ find: function( type, item ) {
433
+ var id = this.resolveIdForItem( type, item ),
434
+ coll = this.getCollection( type );
435
+
436
+ // Because the found object could be of any of the type's superModel
437
+ // types, only return it if it's actually of the type asked for.
438
+ if ( coll ) {
439
+ var obj = coll.get( id );
440
+
441
+ if ( obj instanceof type ) {
442
+ return obj;
443
+ }
444
+ }
445
+
446
+ return null;
447
+ },
448
+
449
+ /**
450
+ * Add a 'model' to its appropriate collection. Retain the original contents of 'model.collection'.
451
+ * @param {Backbone.RelationalModel} model
452
+ */
453
+ register: function( model ) {
454
+ var coll = this.getCollection( model );
455
+
456
+ if ( coll ) {
457
+ var modelColl = model.collection;
458
+ coll.add( model );
459
+ model.collection = modelColl;
460
+ }
461
+ },
462
+
463
+ /**
464
+ * Check if the given model may use the given `id`
465
+ * @param model
466
+ * @param [id]
467
+ */
468
+ checkId: function( model, id ) {
469
+ var coll = this.getCollection( model ),
470
+ duplicate = coll && coll.get( id );
471
+
472
+ if ( duplicate && model !== duplicate ) {
473
+ if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
474
+ console.warn( 'Duplicate id! Old RelationalModel=%o, new RelationalModel=%o', duplicate, model );
475
+ }
476
+
477
+ throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" );
478
+ }
479
+ },
480
+
481
+ /**
482
+ * Explicitly update a model's id in its store collection
483
+ * @param {Backbone.RelationalModel} model
484
+ */
485
+ update: function( model ) {
486
+ var coll = this.getCollection( model );
487
+
488
+ // Register a model if it isn't yet (which happens if it was created without an id).
489
+ if ( !coll.contains( model ) ) {
490
+ this.register( model );
491
+ }
492
+
493
+ // This triggers updating the lookup indices kept in a collection
494
+ coll._onModelEvent( 'change:' + model.idAttribute, model, coll );
495
+
496
+ // Trigger an event on model so related models (having the model's new id in their keyContents) can add it.
497
+ model.trigger( 'relational:change:id', model, coll );
498
+ },
499
+
500
+ /**
501
+ * Unregister from the store: a specific model, a collection, or a model type.
502
+ * @param {Backbone.RelationalModel|Backbone.RelationalModel.constructor|Backbone.Collection} type
503
+ */
504
+ unregister: function( type ) {
505
+ var coll,
506
+ models;
507
+
508
+ if ( type instanceof Backbone.Model ) {
509
+ coll = this.getCollection( type );
510
+ models = [ type ];
511
+ }
512
+ else if ( type instanceof Backbone.Collection ) {
513
+ coll = this.getCollection( type.model );
514
+ models = _.clone( type.models );
515
+ }
516
+ else {
517
+ coll = this.getCollection( type );
518
+ models = _.clone( coll.models );
519
+ }
520
+
521
+ _.each( models, function( model ) {
522
+ this.stopListening( model );
523
+ _.invoke( model.getRelations(), 'stopListening' );
524
+ }, this );
525
+
526
+
527
+ // If we've unregistered an entire store collection, reset the collection (which is much faster).
528
+ // Otherwise, remove each model one by one.
529
+ if ( _.contains( this._collections, type ) ) {
530
+ coll.reset( [] );
531
+ }
532
+ else {
533
+ _.each( models, function( model ) {
534
+ if ( coll.get( model ) ) {
535
+ coll.remove( model );
536
+ }
537
+ else {
538
+ coll.trigger( 'relational:remove', model, coll );
539
+ }
540
+ }, this );
541
+ }
542
+ },
543
+
544
+ /**
545
+ * Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to
546
+ * re-initialize these on models would lead to a large amount of warnings.
547
+ */
548
+ reset: function() {
549
+ this.stopListening();
550
+
551
+ // Unregister each collection to remove event listeners
552
+ _.each( this._collections, function( coll ) {
553
+ this.unregister( coll );
554
+ }, this );
555
+
556
+ this._collections = [];
557
+ this._subModels = [];
558
+ this._modelScopes = [ exports ];
559
+ }
560
+ });
561
+ Backbone.Relational.store = new Backbone.Store();
562
+
563
+ /**
564
+ * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:<key>' events
565
+ * are used to regulate addition and removal of models from relations.
566
+ *
567
+ * @param {Backbone.RelationalModel} [instance] Model that this relation is created for. If no model is supplied,
568
+ * Relation just tries to instantiate it's `reverseRelation` if specified, and bails out after that.
569
+ * @param {Object} options
570
+ * @param {string} options.key
571
+ * @param {Backbone.RelationalModel.constructor} options.relatedModel
572
+ * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
573
+ * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store.
574
+ * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
575
+ * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
576
+ * {Backbone.Relation|String} type ('HasOne' or 'HasMany').
577
+ * @param {Object} opts
578
+ */
579
+ Backbone.Relation = function( instance, options, opts ) {
580
+ this.instance = instance;
581
+ // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
582
+ options = _.isObject( options ) ? options : {};
583
+ this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
584
+ this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
585
+
586
+ this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
587
+ Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
588
+
589
+ this.key = this.options.key;
590
+ this.keySource = this.options.keySource || this.key;
591
+ this.keyDestination = this.options.keyDestination || this.keySource || this.key;
592
+
593
+ this.model = this.options.model || this.instance.constructor;
594
+
595
+ this.relatedModel = this.options.relatedModel;
596
+
597
+ if ( _.isFunction( this.relatedModel ) && !( this.relatedModel.prototype instanceof Backbone.RelationalModel ) ) {
598
+ this.relatedModel = _.result( this, 'relatedModel' );
599
+ }
600
+ if ( _.isString( this.relatedModel ) ) {
601
+ this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
602
+ }
603
+
604
+ if ( !this.checkPreconditions() ) {
605
+ return;
606
+ }
607
+
608
+ // Add the reverse relation on 'relatedModel' to the store's reverseRelations
609
+ if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
610
+ Backbone.Relational.store.addReverseRelation( _.defaults( {
611
+ isAutoRelation: true,
612
+ model: this.relatedModel,
613
+ relatedModel: this.model,
614
+ reverseRelation: this.options // current relation is the 'reverseRelation' for its own reverseRelation
615
+ },
616
+ this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
617
+ ) );
618
+ }
619
+
620
+ if ( instance ) {
621
+ var contentKey = this.keySource;
622
+ if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) {
623
+ contentKey = this.key;
624
+ }
625
+
626
+ this.setKeyContents( this.instance.get( contentKey ) );
627
+ this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel );
628
+
629
+ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
630
+ if ( this.keySource !== this.key ) {
631
+ delete this.instance.attributes[ this.keySource ];
632
+ }
633
+
634
+ // Add this Relation to instance._relations
635
+ this.instance._relations[ this.key ] = this;
636
+
637
+ this.initialize( opts );
638
+
639
+ if ( this.options.autoFetch ) {
640
+ this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} );
641
+ }
642
+
643
+ // When 'relatedModel' are created or destroyed, check if it affects this relation.
644
+ this.listenTo( this.instance, 'destroy', this.destroy )
645
+ .listenTo( this.relatedCollection, 'relational:add relational:change:id', this.tryAddRelated )
646
+ .listenTo( this.relatedCollection, 'relational:remove', this.removeRelated )
647
+ }
648
+ };
649
+ // Fix inheritance :\
650
+ Backbone.Relation.extend = Backbone.Model.extend;
651
+ // Set up all inheritable **Backbone.Relation** properties and methods.
652
+ _.extend( Backbone.Relation.prototype, Backbone.Events, Backbone.Semaphore, {
653
+ options: {
654
+ createModels: true,
655
+ includeInJSON: true,
656
+ isAutoRelation: false,
657
+ autoFetch: false,
658
+ parse: false
659
+ },
660
+
661
+ instance: null,
662
+ key: null,
663
+ keyContents: null,
664
+ relatedModel: null,
665
+ relatedCollection: null,
666
+ reverseRelation: null,
667
+ related: null,
668
+
669
+ /**
670
+ * Check several pre-conditions.
671
+ * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
672
+ */
673
+ checkPreconditions: function() {
674
+ var i = this.instance,
675
+ k = this.key,
676
+ m = this.model,
677
+ rm = this.relatedModel,
678
+ warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
679
+
680
+ if ( !m || !k || !rm ) {
681
+ warn && console.warn( 'Relation=%o: missing model, key or relatedModel (%o, %o, %o).', this, m, k, rm );
682
+ return false;
683
+ }
684
+ // Check if the type in 'model' inherits from Backbone.RelationalModel
685
+ if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
686
+ warn && console.warn( 'Relation=%o: model does not inherit from Backbone.RelationalModel (%o).', this, i );
687
+ return false;
688
+ }
689
+ // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
690
+ if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
691
+ warn && console.warn( 'Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).', this, rm );
692
+ return false;
693
+ }
694
+ // Check if this is not a HasMany, and the reverse relation is HasMany as well
695
+ if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
696
+ warn && console.warn( 'Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.', this );
697
+ return false;
698
+ }
699
+ // Check if we're not attempting to create a relationship on a `key` that's already used.
700
+ if ( i && _.keys( i._relations ).length ) {
701
+ var existing = _.find( i._relations, function( rel ) {
702
+ return rel.key === k;
703
+ }, this );
704
+
705
+ if ( existing ) {
706
+ warn && console.warn( 'Cannot create relation=%o on %o for model=%o: already taken by relation=%o.',
707
+ this, k, i, existing );
708
+ return false;
709
+ }
710
+ }
711
+
712
+ return true;
713
+ },
714
+
715
+ /**
716
+ * Set the related model(s) for this relation
717
+ * @param {Backbone.Model|Backbone.Collection} related
718
+ */
719
+ setRelated: function( related ) {
720
+ this.related = related;
721
+ this.instance.attributes[ this.key ] = related;
722
+ },
723
+
724
+ /**
725
+ * Determine if a relation (on a different RelationalModel) is the reverse
726
+ * relation of the current one.
727
+ * @param {Backbone.Relation} relation
728
+ * @return {Boolean}
729
+ */
730
+ _isReverseRelation: function( relation ) {
731
+ return relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
732
+ this.key === relation.reverseRelation.key;
733
+ },
734
+
735
+ /**
736
+ * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
737
+ * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
738
+ * If not specified, 'this.related' is used.
739
+ * @return {Backbone.Relation[]}
740
+ */
741
+ getReverseRelations: function( model ) {
742
+ var reverseRelations = [];
743
+ // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
744
+ var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
745
+ _.each( models || [], function( related ) {
746
+ _.each( related.getRelations() || [], function( relation ) {
747
+ if ( this._isReverseRelation( relation ) ) {
748
+ reverseRelations.push( relation );
749
+ }
750
+ }, this );
751
+ }, this );
752
+
753
+ return reverseRelations;
754
+ },
755
+
756
+ /**
757
+ * When `this.instance` is destroyed, cleanup our relations.
758
+ * Get reverse relation, call removeRelated on each.
759
+ */
760
+ destroy: function() {
761
+ this.stopListening();
762
+
763
+ if ( this instanceof Backbone.HasOne ) {
764
+ this.setRelated( null );
765
+ }
766
+ else if ( this instanceof Backbone.HasMany ) {
767
+ this.setRelated( this._prepareCollection() );
768
+ }
769
+
770
+ _.each( this.getReverseRelations(), function( relation ) {
771
+ relation.removeRelated( this.instance );
772
+ }, this );
773
+ }
774
+ });
775
+
776
+ Backbone.HasOne = Backbone.Relation.extend({
777
+ options: {
778
+ reverseRelation: { type: 'HasMany' }
779
+ },
780
+
781
+ initialize: function( opts ) {
782
+ this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
783
+
784
+ var related = this.findRelated( opts );
785
+ this.setRelated( related );
786
+
787
+ // Notify new 'related' object of the new relation.
788
+ _.each( this.getReverseRelations(), function( relation ) {
789
+ relation.addRelated( this.instance, opts );
790
+ }, this );
791
+ },
792
+
793
+ /**
794
+ * Find related Models.
795
+ * @param {Object} [options]
796
+ * @return {Backbone.Model}
797
+ */
798
+ findRelated: function( options ) {
799
+ var related = null;
800
+
801
+ options = _.defaults( { parse: this.options.parse }, options );
802
+
803
+ if ( this.keyContents instanceof this.relatedModel ) {
804
+ related = this.keyContents;
805
+ }
806
+ else if ( this.keyContents || this.keyContents === 0 ) { // since 0 can be a valid `id` as well
807
+ var opts = _.defaults( { create: this.options.createModels }, options );
808
+ related = this.relatedModel.findOrCreate( this.keyContents, opts );
809
+ }
810
+
811
+ // Nullify `keyId` if we have a related model; in case it was already part of the relation
812
+ if ( related ) {
813
+ this.keyId = null;
814
+ }
815
+
816
+ return related;
817
+ },
818
+
819
+ /**
820
+ * Normalize and reduce `keyContents` to an `id`, for easier comparison
821
+ * @param {String|Number|Backbone.Model} keyContents
822
+ */
823
+ setKeyContents: function( keyContents ) {
824
+ this.keyContents = keyContents;
825
+ this.keyId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, this.keyContents );
826
+ },
827
+
828
+ /**
829
+ * Event handler for `change:<key>`.
830
+ * If the key is changed, notify old & new reverse relations and initialize the new relation.
831
+ */
832
+ onChange: function( model, attr, options ) {
833
+ // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
834
+ if ( this.isLocked() ) {
835
+ return;
836
+ }
837
+ this.acquire();
838
+ options = options ? _.clone( options ) : {};
839
+
840
+ // 'options.__related' is set by 'addRelated'/'removeRelated'. If it is set, the change
841
+ // is the result of a call from a relation. If it's not, the change is the result of
842
+ // a 'set' call on this.instance.
843
+ var changed = _.isUndefined( options.__related ),
844
+ oldRelated = changed ? this.related : options.__related;
845
+
846
+ if ( changed ) {
847
+ this.setKeyContents( attr );
848
+ var related = this.findRelated( options );
849
+ this.setRelated( related );
850
+ }
851
+
852
+ // Notify old 'related' object of the terminated relation
853
+ if ( oldRelated && this.related !== oldRelated ) {
854
+ _.each( this.getReverseRelations( oldRelated ), function( relation ) {
855
+ relation.removeRelated( this.instance, null, options );
856
+ }, this );
857
+ }
858
+
859
+ // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
860
+ // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
861
+ // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
862
+ _.each( this.getReverseRelations(), function( relation ) {
863
+ relation.addRelated( this.instance, options );
864
+ }, this );
865
+
866
+ // Fire the 'change:<key>' event if 'related' was updated
867
+ if ( !options.silent && this.related !== oldRelated ) {
868
+ var dit = this;
869
+ this.changed = true;
870
+ Backbone.Relational.eventQueue.add( function() {
871
+ dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
872
+ dit.changed = false;
873
+ });
874
+ }
875
+ this.release();
876
+ },
877
+
878
+ /**
879
+ * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
880
+ */
881
+ tryAddRelated: function( model, coll, options ) {
882
+ if ( ( this.keyId || this.keyId === 0 ) && model.id === this.keyId ) { // since 0 can be a valid `id` as well
883
+ this.addRelated( model, options );
884
+ this.keyId = null;
885
+ }
886
+ },
887
+
888
+ addRelated: function( model, options ) {
889
+ // Allow 'model' to set up its relations before proceeding.
890
+ // (which can result in a call to 'addRelated' from a relation of 'model')
891
+ var dit = this;
892
+ model.queue( function() {
893
+ if ( model !== dit.related ) {
894
+ var oldRelated = dit.related || null;
895
+ dit.setRelated( model );
896
+ dit.onChange( dit.instance, model, _.defaults( { __related: oldRelated }, options ) );
897
+ }
898
+ });
899
+ },
900
+
901
+ removeRelated: function( model, coll, options ) {
902
+ if ( !this.related ) {
903
+ return;
904
+ }
905
+
906
+ if ( model === this.related ) {
907
+ var oldRelated = this.related || null;
908
+ this.setRelated( null );
909
+ this.onChange( this.instance, model, _.defaults( { __related: oldRelated }, options ) );
910
+ }
911
+ }
912
+ });
913
+
914
+ Backbone.HasMany = Backbone.Relation.extend({
915
+ collectionType: null,
916
+
917
+ options: {
918
+ reverseRelation: { type: 'HasOne' },
919
+ collectionType: Backbone.Collection,
920
+ collectionKey: true,
921
+ collectionOptions: {}
922
+ },
923
+
924
+ initialize: function( opts ) {
925
+ this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
926
+
927
+ // Handle a custom 'collectionType'
928
+ this.collectionType = this.options.collectionType;
929
+ if ( _.isFunction( this.collectionType ) && this.collectionType !== Backbone.Collection && !( this.collectionType.prototype instanceof Backbone.Collection ) ) {
930
+ this.collectionType = _.result( this, 'collectionType' );
931
+ }
932
+ if ( _.isString( this.collectionType ) ) {
933
+ this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
934
+ }
935
+ if ( this.collectionType !== Backbone.Collection && !( this.collectionType.prototype instanceof Backbone.Collection ) ) {
936
+ throw new Error( '`collectionType` must inherit from Backbone.Collection' );
937
+ }
938
+
939
+ var related = this.findRelated( opts );
940
+ this.setRelated( related );
941
+ },
942
+
943
+ /**
944
+ * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
945
+ * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
946
+ * @param {Backbone.Collection} [collection]
947
+ * @return {Backbone.Collection}
948
+ */
949
+ _prepareCollection: function( collection ) {
950
+ if ( this.related ) {
951
+ this.stopListening( this.related );
952
+ }
953
+
954
+ if ( !collection || !( collection instanceof Backbone.Collection ) ) {
955
+ var options = _.isFunction( this.options.collectionOptions ) ?
956
+ this.options.collectionOptions( this.instance ) : this.options.collectionOptions;
957
+
958
+ collection = new this.collectionType( null, options );
959
+ }
960
+
961
+ collection.model = this.relatedModel;
962
+
963
+ if ( this.options.collectionKey ) {
964
+ var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
965
+
966
+ if ( collection[ key ] && collection[ key ] !== this.instance ) {
967
+ if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
968
+ console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
969
+ }
970
+ }
971
+ else if ( key ) {
972
+ collection[ key ] = this.instance;
973
+ }
974
+ }
975
+
976
+ this.listenTo( collection, 'relational:add', this.handleAddition )
977
+ .listenTo( collection, 'relational:remove', this.handleRemoval )
978
+ .listenTo( collection, 'relational:reset', this.handleReset );
979
+
980
+ return collection;
981
+ },
982
+
983
+ /**
984
+ * Find related Models.
985
+ * @param {Object} [options]
986
+ * @return {Backbone.Collection}
987
+ */
988
+ findRelated: function( options ) {
989
+ var related = null;
990
+
991
+ options = _.defaults( { parse: this.options.parse }, options );
992
+
993
+ // Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection
994
+ if ( this.keyContents instanceof Backbone.Collection ) {
995
+ this._prepareCollection( this.keyContents );
996
+ related = this.keyContents;
997
+ }
998
+ // Otherwise, 'this.keyContents' should be an array of related object ids.
999
+ // Re-use the current 'this.related' if it is a Backbone.Collection; otherwise, create a new collection.
1000
+ else {
1001
+ var toAdd = [];
1002
+
1003
+ _.each( this.keyContents, function( attributes ) {
1004
+ if ( attributes instanceof this.relatedModel ) {
1005
+ var model = attributes;
1006
+ }
1007
+ else {
1008
+ // If `merge` is true, update models here, instead of during update.
1009
+ model = this.relatedModel.findOrCreate( attributes,
1010
+ _.extend( { merge: true }, options, { create: this.options.createModels } )
1011
+ );
1012
+ }
1013
+
1014
+ model && toAdd.push( model );
1015
+ }, this );
1016
+
1017
+ if ( this.related instanceof Backbone.Collection ) {
1018
+ related = this.related;
1019
+ }
1020
+ else {
1021
+ related = this._prepareCollection();
1022
+ }
1023
+
1024
+ // By now, both `merge` and `parse` will already have been executed for models if they were specified.
1025
+ // Disable them to prevent additional calls.
1026
+ related.set( toAdd, _.defaults( { merge: false, parse: false }, options ) );
1027
+ }
1028
+
1029
+ // Remove entries from `keyIds` that were already part of the relation (and are thus 'unchanged')
1030
+ this.keyIds = _.difference( this.keyIds, _.pluck( related.models, 'id' ) );
1031
+
1032
+ return related;
1033
+ },
1034
+
1035
+ /**
1036
+ * Normalize and reduce `keyContents` to a list of `ids`, for easier comparison
1037
+ * @param {String|Number|String[]|Number[]|Backbone.Collection} keyContents
1038
+ */
1039
+ setKeyContents: function( keyContents ) {
1040
+ this.keyContents = keyContents instanceof Backbone.Collection ? keyContents : null;
1041
+ this.keyIds = [];
1042
+
1043
+ if ( !this.keyContents && ( keyContents || keyContents === 0 ) ) { // since 0 can be a valid `id` as well
1044
+ // Handle cases the an API/user supplies just an Object/id instead of an Array
1045
+ this.keyContents = _.isArray( keyContents ) ? keyContents : [ keyContents ];
1046
+
1047
+ _.each( this.keyContents, function( item ) {
1048
+ var itemId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
1049
+ if ( itemId || itemId === 0 ) {
1050
+ this.keyIds.push( itemId );
1051
+ }
1052
+ }, this );
1053
+ }
1054
+ },
1055
+
1056
+ /**
1057
+ * Event handler for `change:<key>`.
1058
+ * If the contents of the key are changed, notify old & new reverse relations and initialize the new relation.
1059
+ */
1060
+ onChange: function( model, attr, options ) {
1061
+ options = options ? _.clone( options ) : {};
1062
+ this.setKeyContents( attr );
1063
+ this.changed = false;
1064
+
1065
+ var related = this.findRelated( options );
1066
+ this.setRelated( related );
1067
+
1068
+ if ( !options.silent ) {
1069
+ var dit = this;
1070
+ Backbone.Relational.eventQueue.add( function() {
1071
+ // The `changed` flag can be set in `handleAddition` or `handleRemoval`
1072
+ if ( dit.changed ) {
1073
+ dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
1074
+ dit.changed = false;
1075
+ }
1076
+ });
1077
+ }
1078
+ },
1079
+
1080
+ /**
1081
+ * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
1082
+ * (should be 'HasOne', must set 'this.instance' as their related).
1083
+ */
1084
+ handleAddition: function( model, coll, options ) {
1085
+ //console.debug('handleAddition called; args=%o', arguments);
1086
+ options = options ? _.clone( options ) : {};
1087
+ this.changed = true;
1088
+
1089
+ _.each( this.getReverseRelations( model ), function( relation ) {
1090
+ relation.addRelated( this.instance, options );
1091
+ }, this );
1092
+
1093
+ // Only trigger 'add' once the newly added model is initialized (so, has its relations set up)
1094
+ var dit = this;
1095
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
1096
+ dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
1097
+ });
1098
+ },
1099
+
1100
+ /**
1101
+ * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.
1102
+ * (should be 'HasOne', which should be nullified)
1103
+ */
1104
+ handleRemoval: function( model, coll, options ) {
1105
+ //console.debug('handleRemoval called; args=%o', arguments);
1106
+ options = options ? _.clone( options ) : {};
1107
+ this.changed = true;
1108
+
1109
+ _.each( this.getReverseRelations( model ), function( relation ) {
1110
+ relation.removeRelated( this.instance, null, options );
1111
+ }, this );
1112
+
1113
+ var dit = this;
1114
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
1115
+ dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
1116
+ });
1117
+ },
1118
+
1119
+ handleReset: function( coll, options ) {
1120
+ var dit = this;
1121
+ options = options ? _.clone( options ) : {};
1122
+ !options.silent && Backbone.Relational.eventQueue.add( function() {
1123
+ dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
1124
+ });
1125
+ },
1126
+
1127
+ tryAddRelated: function( model, coll, options ) {
1128
+ var item = _.contains( this.keyIds, model.id );
1129
+
1130
+ if ( item ) {
1131
+ this.addRelated( model, options );
1132
+ this.keyIds = _.without( this.keyIds, model.id );
1133
+ }
1134
+ },
1135
+
1136
+ addRelated: function( model, options ) {
1137
+ // Allow 'model' to set up its relations before proceeding.
1138
+ // (which can result in a call to 'addRelated' from a relation of 'model')
1139
+ var dit = this;
1140
+ model.queue( function() {
1141
+ if ( dit.related && !dit.related.get( model ) ) {
1142
+ dit.related.add( model, _.defaults( { parse: false }, options ) );
1143
+ }
1144
+ });
1145
+ },
1146
+
1147
+ removeRelated: function( model, coll, options ) {
1148
+ if ( this.related.get( model ) ) {
1149
+ this.related.remove( model, options );
1150
+ }
1151
+ }
1152
+ });
1153
+
1154
+ /**
1155
+ * A type of Backbone.Model that also maintains relations to other models and collections.
1156
+ * New events when compared to the original:
1157
+ * - 'add:<key>' (model, related collection, options)
1158
+ * - 'remove:<key>' (model, related collection, options)
1159
+ * - 'change:<key>' (model, related model or collection, options)
1160
+ */
1161
+ Backbone.RelationalModel = Backbone.Model.extend({
1162
+ relations: null, // Relation descriptions on the prototype
1163
+ _relations: null, // Relation instances
1164
+ _isInitialized: false,
1165
+ _deferProcessing: false,
1166
+ _queue: null,
1167
+ _attributeChangeFired: false, // Keeps track of `change` event firing under some conditions (like nested `set`s)
1168
+
1169
+ subModelTypeAttribute: 'type',
1170
+ subModelTypes: null,
1171
+
1172
+ constructor: function( attributes, options ) {
1173
+ // Nasty hack, for cases like 'model.get( <HasMany key> ).add( item )'.
1174
+ // Defer 'processQueue', so that when 'Relation.createModels' is used we trigger 'HasMany'
1175
+ // collection events only after the model is really fully set up.
1176
+ // Example: event for "p.on( 'add:jobs' )" -> "p.get('jobs').add( { company: c.id, person: p.id } )".
1177
+ if ( options && options.collection ) {
1178
+ var dit = this,
1179
+ collection = this.collection = options.collection;
1180
+
1181
+ // Prevent `collection` from cascading down to nested models; they shouldn't go into this `if` clause.
1182
+ delete options.collection;
1183
+
1184
+ this._deferProcessing = true;
1185
+
1186
+ var processQueue = function( model ) {
1187
+ if ( model === dit ) {
1188
+ dit._deferProcessing = false;
1189
+ dit.processQueue();
1190
+ collection.off( 'relational:add', processQueue );
1191
+ }
1192
+ };
1193
+ collection.on( 'relational:add', processQueue );
1194
+
1195
+ // So we do process the queue eventually, regardless of whether this model actually gets added to 'options.collection'.
1196
+ _.defer( function() {
1197
+ processQueue( dit );
1198
+ });
1199
+ }
1200
+
1201
+ Backbone.Relational.store.processOrphanRelations();
1202
+ Backbone.Relational.store.listenTo( this, 'relational:unregister', Backbone.Relational.store.unregister );
1203
+
1204
+ this._queue = new Backbone.BlockingQueue();
1205
+ this._queue.block();
1206
+ Backbone.Relational.eventQueue.block();
1207
+
1208
+ try {
1209
+ Backbone.Model.apply( this, arguments );
1210
+ }
1211
+ finally {
1212
+ // Try to run the global queue holding external events
1213
+ Backbone.Relational.eventQueue.unblock();
1214
+ }
1215
+ },
1216
+
1217
+ /**
1218
+ * Override 'trigger' to queue 'change' and 'change:*' events
1219
+ */
1220
+ trigger: function( eventName ) {
1221
+ if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) {
1222
+ var dit = this,
1223
+ args = arguments;
1224
+
1225
+ if ( !Backbone.Relational.eventQueue.isLocked() ) {
1226
+ // If we're not in a more complicated nested scenario, fire the change event right away
1227
+ Backbone.Model.prototype.trigger.apply( dit, args );
1228
+ }
1229
+ else {
1230
+ Backbone.Relational.eventQueue.add( function() {
1231
+ // Determine if the `change` event is still valid, now that all relations are populated
1232
+ var changed = true;
1233
+ if ( eventName === 'change' ) {
1234
+ // `hasChanged` may have gotten reset by nested calls to `set`.
1235
+ changed = dit.hasChanged() || dit._attributeChangeFired;
1236
+ dit._attributeChangeFired = false;
1237
+ }
1238
+ else {
1239
+ var attr = eventName.slice( 7 ),
1240
+ rel = dit.getRelation( attr );
1241
+
1242
+ if ( rel ) {
1243
+ // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`.
1244
+ // These take precedence over `change:attr` events triggered by `Model.set`.
1245
+ // The relation sets a fourth attribute to `true`. If this attribute is present,
1246
+ // continue triggering this event; otherwise, it's from `Model.set` and should be stopped.
1247
+ changed = ( args[ 4 ] === true );
1248
+
1249
+ // If this event was triggered by a relation, set the right value in `this.changed`
1250
+ // (a Collection or Model instead of raw data).
1251
+ if ( changed ) {
1252
+ dit.changed[ attr ] = args[ 2 ];
1253
+ }
1254
+ // Otherwise, this event is from `Model.set`. If the relation doesn't report a change,
1255
+ // remove attr from `dit.changed` so `hasChanged` doesn't take it into account.
1256
+ else if ( !rel.changed ) {
1257
+ delete dit.changed[ attr ];
1258
+ }
1259
+ }
1260
+ else if ( changed ) {
1261
+ dit._attributeChangeFired = true;
1262
+ }
1263
+ }
1264
+
1265
+ changed && Backbone.Model.prototype.trigger.apply( dit, args );
1266
+ });
1267
+ }
1268
+ }
1269
+ else if ( eventName === 'destroy' ) {
1270
+ Backbone.Model.prototype.trigger.apply( this, arguments );
1271
+ Backbone.Relational.store.unregister( this );
1272
+ }
1273
+ else {
1274
+ Backbone.Model.prototype.trigger.apply( this, arguments );
1275
+ }
1276
+
1277
+ return this;
1278
+ },
1279
+
1280
+ /**
1281
+ * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.
1282
+ * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor).
1283
+ */
1284
+ initializeRelations: function( options ) {
1285
+ this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
1286
+ this._relations = {};
1287
+
1288
+ _.each( this.relations || [], function( rel ) {
1289
+ Backbone.Relational.store.initializeRelation( this, rel, options );
1290
+ }, this );
1291
+
1292
+ this._isInitialized = true;
1293
+ this.release();
1294
+ this.processQueue();
1295
+ },
1296
+
1297
+ /**
1298
+ * When new values are set, notify this model's relations (also if options.silent is set).
1299
+ * (called from `set`; Relation.setRelated locks this model before calling 'set' on it to prevent loops)
1300
+ * @param {Object} [changedAttrs]
1301
+ * @param {Object} [options]
1302
+ */
1303
+ updateRelations: function( changedAttrs, options ) {
1304
+ if ( this._isInitialized && !this.isLocked() ) {
1305
+ _.each( this._relations, function( rel ) {
1306
+ if ( !changedAttrs || ( rel.keySource in changedAttrs || rel.key in changedAttrs ) ) {
1307
+ // Fetch data in `rel.keySource` if data got set in there, or `rel.key` otherwise
1308
+ var value = this.attributes[ rel.keySource ] || this.attributes[ rel.key ],
1309
+ attr = changedAttrs && ( changedAttrs[ rel.keySource ] || changedAttrs[ rel.key ] );
1310
+
1311
+ // Update a relation if its value differs from this model's attributes, or it's been explicitly nullified.
1312
+ // Which can also happen before the originally intended related model has been found (`val` is null).
1313
+ if ( rel.related !== value || ( value === null && attr === null ) ) {
1314
+ this.trigger( 'relational:change:' + rel.key, this, value, options || {} );
1315
+ }
1316
+ }
1317
+
1318
+ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
1319
+ if ( rel.keySource !== rel.key ) {
1320
+ delete this.attributes[ rel.keySource ];
1321
+ }
1322
+ }, this );
1323
+ }
1324
+ },
1325
+
1326
+ /**
1327
+ * Either add to the queue (if we're not initialized yet), or execute right away.
1328
+ */
1329
+ queue: function( func ) {
1330
+ this._queue.add( func );
1331
+ },
1332
+
1333
+ /**
1334
+ * Process _queue
1335
+ */
1336
+ processQueue: function() {
1337
+ if ( this._isInitialized && !this._deferProcessing && this._queue.isBlocked() ) {
1338
+ this._queue.unblock();
1339
+ }
1340
+ },
1341
+
1342
+ /**
1343
+ * Get a specific relation.
1344
+ * @param {string} key The relation key to look for.
1345
+ * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
1346
+ */
1347
+ getRelation: function( key ) {
1348
+ return this._relations[ key ];
1349
+ },
1350
+
1351
+ /**
1352
+ * Get all of the created relations.
1353
+ * @return {Backbone.Relation[]}
1354
+ */
1355
+ getRelations: function() {
1356
+ return _.values( this._relations );
1357
+ },
1358
+
1359
+ /**
1360
+ * Retrieve related objects.
1361
+ * @param {string} key The relation key to fetch models for.
1362
+ * @param {Object} [options] Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
1363
+ * @param {Boolean} [refresh=false] Fetch existing models from the server as well (in order to update them).
1364
+ * @return {jQuery.when[]} An array of request objects
1365
+ */
1366
+ fetchRelated: function( key, options, refresh ) {
1367
+ // Set default `options` for fetch
1368
+ options = _.extend( { update: true, remove: false }, options );
1369
+
1370
+ var models,
1371
+ setUrl,
1372
+ requests = [],
1373
+ rel = this.getRelation( key ),
1374
+ idsToFetch = rel && ( ( rel.keyIds && rel.keyIds.slice( 0 ) ) || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) );
1375
+
1376
+ // On `refresh`, add the ids for current models in the relation to `idsToFetch`
1377
+ if ( refresh ) {
1378
+ models = rel.related instanceof Backbone.Collection ? rel.related.models : [ rel.related ];
1379
+ _.each( models, function( model ) {
1380
+ if ( model.id || model.id === 0 ) {
1381
+ idsToFetch.push( model.id );
1382
+ }
1383
+ });
1384
+ }
1385
+
1386
+ if ( idsToFetch && idsToFetch.length ) {
1387
+ // Find (or create) a model for each one that is to be fetched
1388
+ var created = [];
1389
+ models = _.map( idsToFetch, function( id ) {
1390
+ var model = rel.relatedModel.findModel( id );
1391
+
1392
+ if ( !model ) {
1393
+ var attrs = {};
1394
+ attrs[ rel.relatedModel.prototype.idAttribute ] = id;
1395
+ model = rel.relatedModel.findOrCreate( attrs, options );
1396
+ created.push( model );
1397
+ }
1398
+
1399
+ return model;
1400
+ }, this );
1401
+
1402
+ // Try if the 'collection' can provide a url to fetch a set of models in one request.
1403
+ if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
1404
+ setUrl = rel.related.url( models );
1405
+ }
1406
+
1407
+ // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls.
1408
+ // To make sure it can, test if the url we got by supplying a list of models to fetch is different from
1409
+ // the one supplied for the default fetch action (without args to 'url').
1410
+ if ( setUrl && setUrl !== rel.related.url() ) {
1411
+ var opts = _.defaults(
1412
+ {
1413
+ error: function() {
1414
+ var args = arguments;
1415
+ _.each( created, function( model ) {
1416
+ model.trigger( 'destroy', model, model.collection, options );
1417
+ options.error && options.error.apply( model, args );
1418
+ });
1419
+ },
1420
+ url: setUrl
1421
+ },
1422
+ options
1423
+ );
1424
+
1425
+ requests = [ rel.related.fetch( opts ) ];
1426
+ }
1427
+ else {
1428
+ requests = _.map( models, function( model ) {
1429
+ var opts = _.defaults(
1430
+ {
1431
+ error: function() {
1432
+ if ( _.contains( created, model ) ) {
1433
+ model.trigger( 'destroy', model, model.collection, options );
1434
+ options.error && options.error.apply( model, arguments );
1435
+ }
1436
+ }
1437
+ },
1438
+ options
1439
+ );
1440
+ return model.fetch( opts );
1441
+ }, this );
1442
+ }
1443
+ }
1444
+
1445
+ return requests;
1446
+ },
1447
+
1448
+ get: function( attr ) {
1449
+ var originalResult = Backbone.Model.prototype.get.call( this, attr );
1450
+
1451
+ // Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr`
1452
+ if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) {
1453
+ return originalResult;
1454
+ }
1455
+
1456
+ // Go through all splits and return the final result
1457
+ var splits = attr.split( '.' );
1458
+ var result = _.reduce(splits, function( model, split ) {
1459
+ if ( _.isNull(model) || _.isUndefined( model ) ) {
1460
+ // Return undefined if the path cannot be expanded
1461
+ return undefined;
1462
+ }
1463
+ else if ( model instanceof Backbone.Model ) {
1464
+ return Backbone.Model.prototype.get.call( model, split );
1465
+ }
1466
+ else if ( model instanceof Backbone.Collection ) {
1467
+ return Backbone.Collection.prototype.at.call( model, split )
1468
+ }
1469
+ else {
1470
+ throw new Error( 'Attribute must be an instanceof Backbone.Model or Backbone.Collection. Is: ' + model + ', currentSplit: ' + split );
1471
+ }
1472
+ }, this );
1473
+
1474
+ if ( originalResult !== undefined && result !== undefined ) {
1475
+ throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result );
1476
+ }
1477
+
1478
+ return originalResult || result;
1479
+ },
1480
+
1481
+ set: function( key, value, options ) {
1482
+ Backbone.Relational.eventQueue.block();
1483
+
1484
+ // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
1485
+ var attributes;
1486
+ if ( _.isObject( key ) || key == null ) {
1487
+ attributes = key;
1488
+ options = value;
1489
+ }
1490
+ else {
1491
+ attributes = {};
1492
+ attributes[ key ] = value;
1493
+ }
1494
+
1495
+ try {
1496
+ var id = this.id,
1497
+ newId = attributes && this.idAttribute in attributes && attributes[ this.idAttribute ];
1498
+
1499
+ // Check if we're not setting a duplicate id before actually calling `set`.
1500
+ Backbone.Relational.store.checkId( this, newId );
1501
+
1502
+ var result = Backbone.Model.prototype.set.apply( this, arguments );
1503
+
1504
+ // Ideal place to set up relations, if this is the first time we're here for this model
1505
+ if ( !this._isInitialized && !this.isLocked() ) {
1506
+ this.constructor.initializeModelHierarchy();
1507
+
1508
+ // Only register models that have an id. A model will be registered when/if it gets an id later on.
1509
+ if ( newId || newId === 0 ) {
1510
+ Backbone.Relational.store.register( this );
1511
+ }
1512
+
1513
+ this.initializeRelations( options );
1514
+ }
1515
+ // The store should know about an `id` update asap
1516
+ else if ( newId && newId !== id ) {
1517
+ Backbone.Relational.store.update( this );
1518
+ }
1519
+
1520
+ if ( attributes ) {
1521
+ this.updateRelations( attributes, options );
1522
+ }
1523
+ }
1524
+ finally {
1525
+ // Try to run the global queue holding external events
1526
+ Backbone.Relational.eventQueue.unblock();
1527
+ }
1528
+
1529
+ return result;
1530
+ },
1531
+
1532
+ clone: function() {
1533
+ var attributes = _.clone( this.attributes );
1534
+ if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) {
1535
+ attributes[ this.idAttribute ] = null;
1536
+ }
1537
+
1538
+ _.each( this.getRelations(), function( rel ) {
1539
+ delete attributes[ rel.key ];
1540
+ });
1541
+
1542
+ return new this.constructor( attributes );
1543
+ },
1544
+
1545
+ /**
1546
+ * Convert relations to JSON, omits them when required
1547
+ */
1548
+ toJSON: function( options ) {
1549
+ // If this Model has already been fully serialized in this branch once, return to avoid loops
1550
+ if ( this.isLocked() ) {
1551
+ return this.id;
1552
+ }
1553
+
1554
+ this.acquire();
1555
+ var json = Backbone.Model.prototype.toJSON.call( this, options );
1556
+
1557
+ if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) {
1558
+ json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
1559
+ }
1560
+
1561
+ _.each( this._relations, function( rel ) {
1562
+ var related = json[ rel.key ],
1563
+ includeInJSON = rel.options.includeInJSON,
1564
+ value = null;
1565
+
1566
+ if ( includeInJSON === true ) {
1567
+ if ( related && _.isFunction( related.toJSON ) ) {
1568
+ value = related.toJSON( options );
1569
+ }
1570
+ }
1571
+ else if ( _.isString( includeInJSON ) ) {
1572
+ if ( related instanceof Backbone.Collection ) {
1573
+ value = related.pluck( includeInJSON );
1574
+ }
1575
+ else if ( related instanceof Backbone.Model ) {
1576
+ value = related.get( includeInJSON );
1577
+ }
1578
+
1579
+ // Add ids for 'unfound' models if includeInJSON is equal to (only) the relatedModel's `idAttribute`
1580
+ if ( includeInJSON === rel.relatedModel.prototype.idAttribute ) {
1581
+ if ( rel instanceof Backbone.HasMany ) {
1582
+ value = value.concat( rel.keyIds );
1583
+ }
1584
+ else if ( rel instanceof Backbone.HasOne ) {
1585
+ value = value || rel.keyId;
1586
+
1587
+ if ( !value && !_.isObject( rel.keyContents ) ) {
1588
+ value = rel.keyContents || null;
1589
+ }
1590
+ }
1591
+ }
1592
+ }
1593
+ else if ( _.isArray( includeInJSON ) ) {
1594
+ if ( related instanceof Backbone.Collection ) {
1595
+ value = [];
1596
+ related.each( function( model ) {
1597
+ var curJson = {};
1598
+ _.each( includeInJSON, function( key ) {
1599
+ curJson[ key ] = model.get( key );
1600
+ });
1601
+ value.push( curJson );
1602
+ });
1603
+ }
1604
+ else if ( related instanceof Backbone.Model ) {
1605
+ value = {};
1606
+ _.each( includeInJSON, function( key ) {
1607
+ value[ key ] = related.get( key );
1608
+ });
1609
+ }
1610
+ }
1611
+ else {
1612
+ delete json[ rel.key ];
1613
+ }
1614
+
1615
+ if ( includeInJSON ) {
1616
+ json[ rel.keyDestination ] = value;
1617
+ }
1618
+
1619
+ if ( rel.keyDestination !== rel.key ) {
1620
+ delete json[ rel.key ];
1621
+ }
1622
+ });
1623
+
1624
+ this.release();
1625
+ return json;
1626
+ }
1627
+ },
1628
+ {
1629
+ /**
1630
+ *
1631
+ * @param superModel
1632
+ * @returns {Backbone.RelationalModel.constructor}
1633
+ */
1634
+ setup: function( superModel ) {
1635
+ // We don't want to share a relations array with a parent, as this will cause problems with reverse
1636
+ // relations. Since `relations` may also be a property or function, only use slice if we have an array.
1637
+ this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 );
1638
+
1639
+ this._subModels = {};
1640
+ this._superModel = null;
1641
+
1642
+ // If this model has 'subModelTypes' itself, remember them in the store
1643
+ if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) {
1644
+ Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this );
1645
+ }
1646
+ // The 'subModelTypes' property should not be inherited, so reset it.
1647
+ else {
1648
+ this.prototype.subModelTypes = null;
1649
+ }
1650
+
1651
+ // Initialize all reverseRelations that belong to this new model.
1652
+ _.each( this.prototype.relations || [], function( rel ) {
1653
+ if ( !rel.model ) {
1654
+ rel.model = this;
1655
+ }
1656
+
1657
+ if ( rel.reverseRelation && rel.model === this ) {
1658
+ var preInitialize = true;
1659
+ if ( _.isString( rel.relatedModel ) ) {
1660
+ /**
1661
+ * The related model might not be defined for two reasons
1662
+ * 1. it is related to itself
1663
+ * 2. it never gets defined, e.g. a typo
1664
+ * 3. the model hasn't been defined yet, but will be later
1665
+ * In neither of these cases do we need to pre-initialize reverse relations.
1666
+ * However, for 3. (which is, to us, indistinguishable from 2.), we do need to attempt
1667
+ * setting up this relation again later, in case the related model is defined later.
1668
+ */
1669
+ var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
1670
+ preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
1671
+ }
1672
+
1673
+ if ( preInitialize ) {
1674
+ Backbone.Relational.store.initializeRelation( null, rel );
1675
+ }
1676
+ else if ( _.isString( rel.relatedModel ) ) {
1677
+ Backbone.Relational.store.addOrphanRelation( rel );
1678
+ }
1679
+ }
1680
+ }, this );
1681
+
1682
+ return this;
1683
+ },
1684
+
1685
+ /**
1686
+ * Create a 'Backbone.Model' instance based on 'attributes'.
1687
+ * @param {Object} attributes
1688
+ * @param {Object} [options]
1689
+ * @return {Backbone.Model}
1690
+ */
1691
+ build: function( attributes, options ) {
1692
+ // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
1693
+ this.initializeModelHierarchy();
1694
+
1695
+ // Determine what type of (sub)model should be built if applicable.
1696
+ var model = this._findSubModelType( this, attributes ) || this;
1697
+
1698
+ return new model( attributes, options );
1699
+ },
1700
+
1701
+ /**
1702
+ * Determines what type of (sub)model should be built if applicable.
1703
+ * Looks up the proper subModelType in 'this._subModels', recursing into
1704
+ * types until a match is found. Returns the applicable 'Backbone.Model'
1705
+ * or null if no match is found.
1706
+ * @param {Backbone.Model} type
1707
+ * @param {Object} attributes
1708
+ * @return {Backbone.Model}
1709
+ */
1710
+ _findSubModelType: function( type, attributes ) {
1711
+ if ( type._subModels && type.prototype.subModelTypeAttribute in attributes ) {
1712
+ var subModelTypeAttribute = attributes[ type.prototype.subModelTypeAttribute ];
1713
+ var subModelType = type._subModels[ subModelTypeAttribute ];
1714
+ if ( subModelType ) {
1715
+ return subModelType;
1716
+ }
1717
+ else {
1718
+ // Recurse into subModelTypes to find a match
1719
+ for ( subModelTypeAttribute in type._subModels ) {
1720
+ subModelType = this._findSubModelType( type._subModels[ subModelTypeAttribute ], attributes );
1721
+ if ( subModelType ) {
1722
+ return subModelType;
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ return null;
1728
+ },
1729
+
1730
+ /**
1731
+ *
1732
+ */
1733
+ initializeModelHierarchy: function() {
1734
+ // Inherit any relations that have been defined in the parent model.
1735
+ this.inheritRelations();
1736
+
1737
+ // If we came here through 'build' for a model that has 'subModelTypes' then try to initialize the ones that
1738
+ // haven't been resolved yet.
1739
+ if ( this.prototype.subModelTypes ) {
1740
+ var resolvedSubModels = _.keys( this._subModels );
1741
+ var unresolvedSubModels = _.omit( this.prototype.subModelTypes, resolvedSubModels );
1742
+ _.each( unresolvedSubModels, function( subModelTypeName ) {
1743
+ var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
1744
+ subModelType && subModelType.initializeModelHierarchy();
1745
+ });
1746
+ }
1747
+ },
1748
+
1749
+ inheritRelations: function() {
1750
+ // Bail out if we've been here before.
1751
+ if (!_.isUndefined( this._superModel ) && !_.isNull( this._superModel )) {
1752
+ return;
1753
+ }
1754
+ // Try to initialize the _superModel.
1755
+ Backbone.Relational.store.setupSuperModel( this );
1756
+
1757
+ // If a superModel has been found, copy relations from the _superModel if they haven't been inherited automatically
1758
+ // (due to a redefinition of 'relations').
1759
+ if ( this._superModel ) {
1760
+ // The _superModel needs a chance to initialize its own inherited relations before we attempt to inherit relations
1761
+ // from the _superModel. You don't want to call 'initializeModelHierarchy' because that could cause sub-models of
1762
+ // this class to inherit their relations before this class has had chance to inherit it's relations.
1763
+ this._superModel.inheritRelations();
1764
+ if ( this._superModel.prototype.relations ) {
1765
+ // Find relations that exist on the '_superModel', but not yet on this model.
1766
+ var inheritedRelations = _.filter( this._superModel.prototype.relations || [], function( superRel ) {
1767
+ return !_.any( this.prototype.relations || [], function( rel ) {
1768
+ return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key;
1769
+ }, this );
1770
+ }, this );
1771
+
1772
+ this.prototype.relations = inheritedRelations.concat( this.prototype.relations );
1773
+ }
1774
+ }
1775
+ // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail the
1776
+ // isUndefined/isNull check next time.
1777
+ else {
1778
+ this._superModel = false;
1779
+ }
1780
+ },
1781
+
1782
+ /**
1783
+ * Find an instance of `this` type in 'Backbone.Relational.store'.
1784
+ * A new model is created if no matching model is found, `attributes` is an object, and `options.create` is true.
1785
+ * - If `attributes` is a string or a number, `findOrCreate` will query the `store` and return a model if found.
1786
+ * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1787
+ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1788
+ * @param {Object} [options]
1789
+ * @param {Boolean} [options.create=true]
1790
+ * @param {Boolean} [options.merge=true]
1791
+ * @param {Boolean} [options.parse=false]
1792
+ * @return {Backbone.RelationalModel}
1793
+ */
1794
+ findOrCreate: function( attributes, options ) {
1795
+ options || ( options = {} );
1796
+ var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ?
1797
+ this.prototype.parse( _.clone( attributes ) ) : attributes;
1798
+
1799
+ // If specified, use a custom `find` function to match up existing models to the given attributes.
1800
+ // Otherwise, try to find an instance of 'this' model type in the store
1801
+ var model = this.findModel( parsedAttributes );
1802
+
1803
+ // If we found an instance, update it with the data in 'item' (unless 'options.merge' is false).
1804
+ // If not, create an instance (unless 'options.create' is false).
1805
+ if ( _.isObject( attributes ) ) {
1806
+ if ( model && options.merge !== false ) {
1807
+ // Make sure `options.collection` and `options.url` doesn't cascade to nested models
1808
+ delete options.collection;
1809
+ delete options.url;
1810
+
1811
+ model.set( parsedAttributes, options );
1812
+ }
1813
+ else if ( !model && options.create !== false ) {
1814
+ model = this.build( parsedAttributes, _.defaults( { parse: false }, options ) );
1815
+ }
1816
+ }
1817
+
1818
+ return model;
1819
+ },
1820
+
1821
+ /**
1822
+ * Find an instance of `this` type in 'Backbone.Relational.store'.
1823
+ * - If `attributes` is a string or a number, `find` will query the `store` and return a model if found.
1824
+ * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
1825
+ * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
1826
+ * @param {Object} [options]
1827
+ * @param {Boolean} [options.merge=true]
1828
+ * @param {Boolean} [options.parse=false]
1829
+ * @return {Backbone.RelationalModel}
1830
+ */
1831
+ find: function( attributes, options ) {
1832
+ options || ( options = {} );
1833
+ options.create = false;
1834
+ return this.findOrCreate( attributes, options );
1835
+ },
1836
+
1837
+ /**
1838
+ * A hook to override the matching when updating (or creating) a model.
1839
+ * The default implementation is to look up the model by id in the store.
1840
+ * @param {Object} attributes
1841
+ * @returns {Backbone.RelationalModel}
1842
+ */
1843
+ findModel: function( attributes ) {
1844
+ return Backbone.Relational.store.find( this, attributes );
1845
+ }
1846
+ });
1847
+ _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
1848
+
1849
+ /**
1850
+ * Override Backbone.Collection._prepareModel, so objects will be built using the correct type
1851
+ * if the collection.model has subModels.
1852
+ * Attempts to find a model for `attrs` in Backbone.store through `findOrCreate`
1853
+ * (which sets the new properties on it if found), or instantiates a new model.
1854
+ */
1855
+ Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
1856
+ Backbone.Collection.prototype._prepareModel = function ( attrs, options ) {
1857
+ var model;
1858
+
1859
+ if ( attrs instanceof Backbone.Model ) {
1860
+ if ( !attrs.collection ) {
1861
+ attrs.collection = this;
1862
+ }
1863
+ model = attrs;
1864
+ }
1865
+ else {
1866
+ options = options ? _.clone( options ) : {};
1867
+ options.collection = this;
1868
+
1869
+ if ( typeof this.model.findOrCreate !== 'undefined' ) {
1870
+ model = this.model.findOrCreate( attrs, options );
1871
+ }
1872
+ else {
1873
+ model = new this.model( attrs, options );
1874
+ }
1875
+
1876
+ if ( model && model.validationError ) {
1877
+ this.trigger( 'invalid', this, attrs, options );
1878
+ model = false;
1879
+ }
1880
+ }
1881
+
1882
+ return model;
1883
+ };
1884
+
1885
+
1886
+ /**
1887
+ * Override Backbone.Collection.set, so we'll create objects from attributes where required,
1888
+ * and update the existing models. Also, trigger 'relational:add'.
1889
+ */
1890
+ var set = Backbone.Collection.prototype.__set = Backbone.Collection.prototype.set;
1891
+ Backbone.Collection.prototype.set = function( models, options ) {
1892
+ // Short-circuit if this Collection doesn't hold RelationalModels
1893
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1894
+ return set.apply( this, arguments );
1895
+ }
1896
+
1897
+ if ( options && options.parse ) {
1898
+ models = this.parse( models, options );
1899
+ }
1900
+
1901
+ var singular = !_.isArray( models ),
1902
+ newModels = [],
1903
+ toAdd = [];
1904
+
1905
+ models = singular ? ( models ? [ models ] : [] ) : _.clone( models );
1906
+
1907
+ //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
1908
+ _.each( models, function( model ) {
1909
+ if ( !( model instanceof Backbone.Model ) ) {
1910
+ model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
1911
+ }
1912
+
1913
+ if ( model ) {
1914
+ toAdd.push( model );
1915
+
1916
+ if ( !( this.get( model ) || this.get( model.cid ) ) ) {
1917
+ newModels.push( model );
1918
+ }
1919
+ // If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`),
1920
+ // we may get here before `_onModelEvent` has had the chance to update `_byId`.
1921
+ else if ( model.id != null ) {
1922
+ this._byId[ model.id ] = model;
1923
+ }
1924
+ }
1925
+ }, this );
1926
+
1927
+ // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
1928
+ // If `parse` was specified, the collection and contained models have been parsed now.
1929
+ toAdd = singular ? ( toAdd.length ? toAdd[ 0 ] : null ) : toAdd;
1930
+ var result = set.call( this, toAdd, _.defaults( { parse: false }, options ) );
1931
+
1932
+ _.each( newModels, function( model ) {
1933
+ // Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection.
1934
+ if ( this.get( model ) || this.get( model.cid ) ) {
1935
+ this.trigger( 'relational:add', model, this, options );
1936
+ }
1937
+ }, this );
1938
+
1939
+ return result;
1940
+ };
1941
+
1942
+ /**
1943
+ * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
1944
+ */
1945
+ var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
1946
+ Backbone.Collection.prototype.remove = function( models, options ) {
1947
+ // Short-circuit if this Collection doesn't hold RelationalModels
1948
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
1949
+ return remove.apply( this, arguments );
1950
+ }
1951
+
1952
+ var singular = !_.isArray( models ),
1953
+ toRemove = [];
1954
+
1955
+ models = singular ? ( models ? [ models ] : [] ) : _.clone( models );
1956
+ options || ( options = {} );
1957
+
1958
+ //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
1959
+ _.each( models, function( model ) {
1960
+ model = this.get( model ) || ( model && this.get( model.cid ) );
1961
+ model && toRemove.push( model );
1962
+ }, this );
1963
+
1964
+ var result = remove.call( this, singular ? ( toRemove.length ? toRemove[ 0 ] : null ) : toRemove, options );
1965
+
1966
+ _.each( toRemove, function( model ) {
1967
+ this.trigger('relational:remove', model, this, options);
1968
+ }, this );
1969
+
1970
+ return result;
1971
+ };
1972
+
1973
+ /**
1974
+ * Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
1975
+ */
1976
+ var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
1977
+ Backbone.Collection.prototype.reset = function( models, options ) {
1978
+ options = _.extend( { merge: true }, options );
1979
+ var result = reset.call( this, models, options );
1980
+
1981
+ if ( this.model.prototype instanceof Backbone.RelationalModel ) {
1982
+ this.trigger( 'relational:reset', this, options );
1983
+ }
1984
+
1985
+ return result;
1986
+ };
1987
+
1988
+ /**
1989
+ * Override 'Backbone.Collection.sort' to trigger 'relational:reset'.
1990
+ */
1991
+ var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
1992
+ Backbone.Collection.prototype.sort = function( options ) {
1993
+ var result = sort.call( this, options );
1994
+
1995
+ if ( this.model.prototype instanceof Backbone.RelationalModel ) {
1996
+ this.trigger( 'relational:reset', this, options );
1997
+ }
1998
+
1999
+ return result;
2000
+ };
2001
+
2002
+ /**
2003
+ * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
2004
+ * are ready.
2005
+ */
2006
+ var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
2007
+ Backbone.Collection.prototype.trigger = function( eventName ) {
2008
+ // Short-circuit if this Collection doesn't hold RelationalModels
2009
+ if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
2010
+ return trigger.apply( this, arguments );
2011
+ }
2012
+
2013
+ if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' || eventName === 'sort' ) {
2014
+ var dit = this,
2015
+ args = arguments;
2016
+
2017
+ if ( _.isObject( args[ 3 ] ) ) {
2018
+ args = _.toArray( args );
2019
+ // the fourth argument is the option object.
2020
+ // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
2021
+ args[ 3 ] = _.clone( args[ 3 ] );
2022
+ }
2023
+
2024
+ Backbone.Relational.eventQueue.add( function() {
2025
+ trigger.apply( dit, args );
2026
+ });
2027
+ }
2028
+ else {
2029
+ trigger.apply( this, arguments );
2030
+ }
2031
+
2032
+ return this;
2033
+ };
2034
+
2035
+ // Override .extend() to automatically call .setup()
2036
+ Backbone.RelationalModel.extend = function( protoProps, classProps ) {
2037
+ var child = Backbone.Model.extend.apply( this, arguments );
2038
+
2039
+ child.setup( this );
2040
+
2041
+ return child;
2042
+ };
2043
+ }));