ember-data-source 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7937 @@
1
+ // Last commit: 73ad70c (2013-02-23 22:52:20 -0800)
2
+
3
+
4
+ (function() {
5
+ window.DS = Ember.Namespace.create({
6
+ // this one goes to 11
7
+ CURRENT_API_REVISION: 11
8
+ });
9
+
10
+ })();
11
+
12
+
13
+
14
+ (function() {
15
+ var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred
16
+ Evented = Ember.Evented, // ember-runtime/mixins/evented
17
+ run = Ember.run, // ember-metal/run-loop
18
+ get = Ember.get; // ember-metal/accessors
19
+
20
+ var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, {
21
+ init: function() {
22
+ this._super.apply(this, arguments);
23
+ this.one('didLoad', function() {
24
+ run(this, 'resolve', this);
25
+ });
26
+
27
+ if (get(this, 'isLoaded')) {
28
+ this.trigger('didLoad');
29
+ }
30
+ }
31
+ });
32
+
33
+ DS.LoadPromise = LoadPromise;
34
+
35
+ })();
36
+
37
+
38
+
39
+ (function() {
40
+ var get = Ember.get, set = Ember.set;
41
+
42
+ var LoadPromise = DS.LoadPromise; // system/mixins/load_promise
43
+
44
+ /**
45
+ A record array is an array that contains records of a certain type. The record
46
+ array materializes records as needed when they are retrieved for the first
47
+ time. You should not create record arrays yourself. Instead, an instance of
48
+ DS.RecordArray or its subclasses will be returned by your application's store
49
+ in response to queries.
50
+ */
51
+
52
+ DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, {
53
+ /**
54
+ The model type contained by this record array.
55
+
56
+ @type DS.Model
57
+ */
58
+ type: null,
59
+
60
+ // The array of client ids backing the record array. When a
61
+ // record is requested from the record array, the record
62
+ // for the client id at the same index is materialized, if
63
+ // necessary, by the store.
64
+ content: null,
65
+
66
+ isLoaded: false,
67
+ isUpdating: false,
68
+
69
+ // The store that created this record array.
70
+ store: null,
71
+
72
+ objectAtContent: function(index) {
73
+ var content = get(this, 'content'),
74
+ reference = content.objectAt(index),
75
+ store = get(this, 'store');
76
+
77
+ if (reference) {
78
+ return store.recordForReference(reference);
79
+ }
80
+ },
81
+
82
+ materializedObjectAt: function(index) {
83
+ var reference = get(this, 'content').objectAt(index);
84
+ if (!reference) { return; }
85
+
86
+ if (get(this, 'store').recordIsMaterialized(reference)) {
87
+ return this.objectAt(index);
88
+ }
89
+ },
90
+
91
+ update: function() {
92
+ if (get(this, 'isUpdating')) { return; }
93
+
94
+ var store = get(this, 'store'),
95
+ type = get(this, 'type');
96
+
97
+ store.fetchAll(type, this);
98
+ },
99
+
100
+ addReference: function(reference) {
101
+ get(this, 'content').addObject(reference);
102
+ },
103
+
104
+ removeReference: function(reference) {
105
+ get(this, 'content').removeObject(reference);
106
+ }
107
+ });
108
+
109
+ })();
110
+
111
+
112
+
113
+ (function() {
114
+ var get = Ember.get;
115
+
116
+ DS.FilteredRecordArray = DS.RecordArray.extend({
117
+ filterFunction: null,
118
+ isLoaded: true,
119
+
120
+ replace: function() {
121
+ var type = get(this, 'type').toString();
122
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
123
+ },
124
+
125
+ updateFilter: Ember.observer(function() {
126
+ var store = get(this, 'store');
127
+ store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
128
+ }, 'filterFunction')
129
+ });
130
+
131
+ })();
132
+
133
+
134
+
135
+ (function() {
136
+ var get = Ember.get, set = Ember.set;
137
+
138
+ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
139
+ query: null,
140
+
141
+ replace: function() {
142
+ var type = get(this, 'type').toString();
143
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
144
+ },
145
+
146
+ load: function(references) {
147
+ var store = get(this, 'store'), type = get(this, 'type');
148
+
149
+ this.beginPropertyChanges();
150
+ set(this, 'content', Ember.A(references));
151
+ set(this, 'isLoaded', true);
152
+ this.endPropertyChanges();
153
+
154
+ var self = this;
155
+ // TODO: does triggering didLoad event should be the last action of the runLoop?
156
+ Ember.run.once(function() {
157
+ self.trigger('didLoad');
158
+ });
159
+ }
160
+ });
161
+
162
+ })();
163
+
164
+
165
+
166
+ (function() {
167
+ var get = Ember.get, set = Ember.set;
168
+
169
+ /**
170
+ A ManyArray is a RecordArray that represents the contents of a has-many
171
+ relationship.
172
+
173
+ The ManyArray is instantiated lazily the first time the relationship is
174
+ requested.
175
+
176
+ ### Inverses
177
+
178
+ Often, the relationships in Ember Data applications will have
179
+ an inverse. For example, imagine the following models are
180
+ defined:
181
+
182
+ App.Post = DS.Model.extend({
183
+ comments: DS.hasMany('App.Comment')
184
+ });
185
+
186
+ App.Comment = DS.Model.extend({
187
+ post: DS.belongsTo('App.Post')
188
+ });
189
+
190
+ If you created a new instance of `App.Post` and added
191
+ a `App.Comment` record to its `comments` has-many
192
+ relationship, you would expect the comment's `post`
193
+ property to be set to the post that contained
194
+ the has-many.
195
+
196
+ We call the record to which a relationship belongs the
197
+ relationship's _owner_.
198
+ */
199
+ DS.ManyArray = DS.RecordArray.extend({
200
+ init: function() {
201
+ this._super.apply(this, arguments);
202
+ this._changesToSync = Ember.OrderedSet.create();
203
+ },
204
+
205
+ /**
206
+ @private
207
+
208
+ The record to which this relationship belongs.
209
+
210
+ @property {DS.Model}
211
+ */
212
+ owner: null,
213
+
214
+ // LOADING STATE
215
+
216
+ isLoaded: false,
217
+
218
+ loadingRecordsCount: function(count) {
219
+ this.loadingRecordsCount = count;
220
+ },
221
+
222
+ loadedRecord: function() {
223
+ this.loadingRecordsCount--;
224
+ if (this.loadingRecordsCount === 0) {
225
+ set(this, 'isLoaded', true);
226
+ this.trigger('didLoad');
227
+ }
228
+ },
229
+
230
+ fetch: function() {
231
+ var references = get(this, 'content'),
232
+ store = get(this, 'store'),
233
+ type = get(this, 'type'),
234
+ owner = get(this, 'owner');
235
+
236
+ store.fetchUnloadedReferences(type, references, owner);
237
+ },
238
+
239
+ // Overrides Ember.Array's replace method to implement
240
+ replaceContent: function(index, removed, added) {
241
+ // Map the array of record objects into an array of client ids.
242
+ added = added.map(function(record) {
243
+ Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type') === record.constructor));
244
+ return get(record, '_reference');
245
+ }, this);
246
+
247
+ this._super(index, removed, added);
248
+ },
249
+
250
+ arrangedContentDidChange: function() {
251
+ this.fetch();
252
+ },
253
+
254
+ arrayContentWillChange: function(index, removed, added) {
255
+ var owner = get(this, 'owner'),
256
+ name = get(this, 'name');
257
+
258
+ if (!owner._suspendedRelationships) {
259
+ // This code is the first half of code that continues inside
260
+ // of arrayContentDidChange. It gets or creates a change from
261
+ // the child object, adds the current owner as the old
262
+ // parent if this is the first time the object was removed
263
+ // from a ManyArray, and sets `newParent` to null.
264
+ //
265
+ // Later, if the object is added to another ManyArray,
266
+ // the `arrayContentDidChange` will set `newParent` on
267
+ // the change.
268
+ for (var i=index; i<index+removed; i++) {
269
+ var reference = get(this, 'content').objectAt(i);
270
+
271
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, get(this, 'store'), {
272
+ parentType: owner.constructor,
273
+ changeType: "remove",
274
+ kind: "hasMany",
275
+ key: name
276
+ });
277
+
278
+ this._changesToSync.add(change);
279
+ }
280
+ }
281
+
282
+ return this._super.apply(this, arguments);
283
+ },
284
+
285
+ arrayContentDidChange: function(index, removed, added) {
286
+ this._super.apply(this, arguments);
287
+
288
+ var owner = get(this, 'owner'),
289
+ name = get(this, 'name'),
290
+ store = get(this, 'store');
291
+
292
+ if (!owner._suspendedRelationships) {
293
+ // This code is the second half of code that started in
294
+ // `arrayContentWillChange`. It gets or creates a change
295
+ // from the child object, and adds the current owner as
296
+ // the new parent.
297
+ for (var i=index; i<index+added; i++) {
298
+ var reference = get(this, 'content').objectAt(i);
299
+
300
+ var change = DS.RelationshipChange.createChange(owner.get('_reference'), reference, store, {
301
+ parentType: owner.constructor,
302
+ changeType: "add",
303
+ kind:"hasMany",
304
+ key: name
305
+ });
306
+ change.hasManyName = name;
307
+
308
+ this._changesToSync.add(change);
309
+ }
310
+
311
+ // We wait until the array has finished being
312
+ // mutated before syncing the OneToManyChanges created
313
+ // in arrayContentWillChange, so that the array
314
+ // membership test in the sync() logic operates
315
+ // on the final results.
316
+ this._changesToSync.forEach(function(change) {
317
+ change.sync();
318
+ });
319
+ DS.OneToManyChange.ensureSameTransaction(this._changesToSync, store);
320
+ this._changesToSync.clear();
321
+ }
322
+ },
323
+
324
+ // Create a child record within the owner
325
+ createRecord: function(hash, transaction) {
326
+ var owner = get(this, 'owner'),
327
+ store = get(owner, 'store'),
328
+ type = get(this, 'type'),
329
+ record;
330
+
331
+ transaction = transaction || get(owner, 'transaction');
332
+
333
+ record = store.createRecord.call(store, type, hash, transaction);
334
+ this.pushObject(record);
335
+
336
+ return record;
337
+ }
338
+
339
+ });
340
+
341
+ })();
342
+
343
+
344
+
345
+ (function() {
346
+
347
+ })();
348
+
349
+
350
+
351
+ (function() {
352
+ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt,
353
+ removeObject = Ember.EnumerableUtils.removeObject, forEach = Ember.EnumerableUtils.forEach;
354
+
355
+ /**
356
+ A transaction allows you to collect multiple records into a unit of work
357
+ that can be committed or rolled back as a group.
358
+
359
+ For example, if a record has local modifications that have not yet
360
+ been saved, calling `commit()` on its transaction will cause those
361
+ modifications to be sent to the adapter to be saved. Calling
362
+ `rollback()` on its transaction would cause all of the modifications to
363
+ be discarded and the record to return to the last known state before
364
+ changes were made.
365
+
366
+ If a newly created record's transaction is rolled back, it will
367
+ immediately transition to the deleted state.
368
+
369
+ If you do not explicitly create a transaction, a record is assigned to
370
+ an implicit transaction called the default transaction. In these cases,
371
+ you can treat your application's instance of `DS.Store` as a transaction
372
+ and call the `commit()` and `rollback()` methods on the store itself.
373
+
374
+ Once a record has been successfully committed or rolled back, it will
375
+ be moved back to the implicit transaction. Because it will now be in
376
+ a clean state, it can be moved to a new transaction if you wish.
377
+
378
+ ### Creating a Transaction
379
+
380
+ To create a new transaction, call the `transaction()` method of your
381
+ application's `DS.Store` instance:
382
+
383
+ var transaction = App.store.transaction();
384
+
385
+ This will return a new instance of `DS.Transaction` with no records
386
+ yet assigned to it.
387
+
388
+ ### Adding Existing Records
389
+
390
+ Add records to a transaction using the `add()` method:
391
+
392
+ record = App.store.find(App.Person, 1);
393
+ transaction.add(record);
394
+
395
+ Note that only records whose `isDirty` flag is `false` may be added
396
+ to a transaction. Once modifications to a record have been made
397
+ (its `isDirty` flag is `true`), it is not longer able to be added to
398
+ a transaction.
399
+
400
+ ### Creating New Records
401
+
402
+ Because newly created records are dirty from the time they are created,
403
+ and because dirty records can not be added to a transaction, you must
404
+ use the `createRecord()` method to assign new records to a transaction.
405
+
406
+ For example, instead of this:
407
+
408
+ var transaction = store.transaction();
409
+ var person = App.Person.createRecord({ name: "Steve" });
410
+
411
+ // won't work because person is dirty
412
+ transaction.add(person);
413
+
414
+ Call `createRecord()` on the transaction directly:
415
+
416
+ var transaction = store.transaction();
417
+ transaction.createRecord(App.Person, { name: "Steve" });
418
+
419
+ ### Asynchronous Commits
420
+
421
+ Typically, all of the records in a transaction will be committed
422
+ together. However, new records that have a dependency on other new
423
+ records need to wait for their parent record to be saved and assigned an
424
+ ID. In that case, the child record will continue to live in the
425
+ transaction until its parent is saved, at which time the transaction will
426
+ attempt to commit again.
427
+
428
+ For this reason, you should not re-use transactions once you have committed
429
+ them. Always make a new transaction and move the desired records to it before
430
+ calling commit.
431
+ */
432
+
433
+ var arrayDefault = function() { return []; };
434
+
435
+ DS.Transaction = Ember.Object.extend({
436
+ /**
437
+ @private
438
+
439
+ Creates the bucket data structure used to segregate records by
440
+ type.
441
+ */
442
+ init: function() {
443
+ set(this, 'buckets', {
444
+ clean: Ember.OrderedSet.create(),
445
+ created: Ember.OrderedSet.create(),
446
+ updated: Ember.OrderedSet.create(),
447
+ deleted: Ember.OrderedSet.create(),
448
+ inflight: Ember.OrderedSet.create()
449
+ });
450
+
451
+ set(this, 'relationships', Ember.OrderedSet.create());
452
+ },
453
+
454
+ /**
455
+ Creates a new record of the given type and assigns it to the transaction
456
+ on which the method was called.
457
+
458
+ This is useful as only clean records can be added to a transaction and
459
+ new records created using other methods immediately become dirty.
460
+
461
+ @param {DS.Model} type the model type to create
462
+ @param {Object} hash the data hash to assign the new record
463
+ */
464
+ createRecord: function(type, hash) {
465
+ var store = get(this, 'store');
466
+
467
+ return store.createRecord(type, hash, this);
468
+ },
469
+
470
+ isEqualOrDefault: function(other) {
471
+ if (this === other || other === get(this, 'store.defaultTransaction')) {
472
+ return true;
473
+ }
474
+ },
475
+
476
+ isDefault: Ember.computed(function() {
477
+ return this === get(this, 'store.defaultTransaction');
478
+ }),
479
+
480
+ /**
481
+ Adds an existing record to this transaction. Only records without
482
+ modficiations (i.e., records whose `isDirty` property is `false`)
483
+ can be added to a transaction.
484
+
485
+ @param {DS.Model} record the record to add to the transaction
486
+ */
487
+ add: function(record) {
488
+ Ember.assert("You must pass a record into transaction.add()", record instanceof DS.Model);
489
+
490
+ var recordTransaction = get(record, 'transaction'),
491
+ defaultTransaction = get(this, 'store.defaultTransaction');
492
+
493
+ // Make `add` idempotent
494
+ if (recordTransaction === this) { return; }
495
+
496
+ // XXX it should be possible to move a dirty transaction from the default transaction
497
+
498
+ // we could probably make this work if someone has a valid use case. Do you?
499
+ Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
500
+
501
+ Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
502
+
503
+ this.adoptRecord(record);
504
+ },
505
+
506
+ relationshipBecameDirty: function(relationship) {
507
+ get(this, 'relationships').add(relationship);
508
+ },
509
+
510
+ relationshipBecameClean: function(relationship) {
511
+ get(this, 'relationships').remove(relationship);
512
+ },
513
+
514
+ /**
515
+ Commits the transaction, which causes all of the modified records that
516
+ belong to the transaction to be sent to the adapter to be saved.
517
+
518
+ Once you call `commit()` on a transaction, you should not re-use it.
519
+
520
+ When a record is saved, it will be removed from this transaction and
521
+ moved back to the store's default transaction.
522
+ */
523
+ commit: function() {
524
+ var store = get(this, 'store');
525
+ var adapter = get(store, '_adapter');
526
+ var defaultTransaction = get(store, 'defaultTransaction');
527
+
528
+ var iterate = function(records) {
529
+ var set = records.copy();
530
+ set.forEach(function (record) {
531
+ record.send('willCommit');
532
+ });
533
+ return set;
534
+ };
535
+
536
+ var relationships = get(this, 'relationships');
537
+
538
+ var commitDetails = {
539
+ created: iterate(this.bucketForType('created')),
540
+ updated: iterate(this.bucketForType('updated')),
541
+ deleted: iterate(this.bucketForType('deleted')),
542
+ relationships: relationships
543
+ };
544
+
545
+ if (this === defaultTransaction) {
546
+ set(store, 'defaultTransaction', store.transaction());
547
+ }
548
+
549
+ this.removeCleanRecords();
550
+
551
+ if (!commitDetails.created.isEmpty() || !commitDetails.updated.isEmpty() || !commitDetails.deleted.isEmpty() || !relationships.isEmpty()) {
552
+ if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
553
+ else { throw fmt("Adapter is either null or does not implement `commit` method", this); }
554
+ }
555
+
556
+ // Once we've committed the transaction, there is no need to
557
+ // keep the OneToManyChanges around. Destroy them so they
558
+ // can be garbage collected.
559
+ relationships.forEach(function(relationship) {
560
+ relationship.destroy();
561
+ });
562
+ },
563
+
564
+ /**
565
+ Rolling back a transaction resets the records that belong to
566
+ that transaction.
567
+
568
+ Updated records have their properties reset to the last known
569
+ value from the persistence layer. Deleted records are reverted
570
+ to a clean, non-deleted state. Newly created records immediately
571
+ become deleted, and are not sent to the adapter to be persisted.
572
+
573
+ After the transaction is rolled back, any records that belong
574
+ to it will return to the store's default transaction, and the
575
+ current transaction should not be used again.
576
+ */
577
+ rollback: function() {
578
+ // Loop through all of the records in each of the dirty states
579
+ // and initiate a rollback on them. As a side effect of telling
580
+ // the record to roll back, it should also move itself out of
581
+ // the dirty bucket and into the clean bucket.
582
+ ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) {
583
+ var records = this.bucketForType(bucketType);
584
+ forEach(records, function(record) {
585
+ record.send('rollback');
586
+ });
587
+ records.clear();
588
+ }, this);
589
+
590
+ // Now that all records in the transaction are guaranteed to be
591
+ // clean, migrate them all to the store's default transaction.
592
+ this.removeCleanRecords();
593
+ },
594
+
595
+ /**
596
+ @private
597
+
598
+ Removes a record from this transaction and back to the store's
599
+ default transaction.
600
+
601
+ Note: This method is private for now, but should probably be exposed
602
+ in the future once we have stricter error checking (for example, in the
603
+ case of the record being dirty).
604
+
605
+ @param {DS.Model} record
606
+ */
607
+ remove: function(record) {
608
+ var defaultTransaction = get(this, 'store.defaultTransaction');
609
+ defaultTransaction.adoptRecord(record);
610
+ },
611
+
612
+ /**
613
+ @private
614
+
615
+ Removes all of the records in the transaction's clean bucket.
616
+ */
617
+ removeCleanRecords: function() {
618
+ var clean = this.bucketForType('clean');
619
+ clean.forEach(function(record) {
620
+ this.remove(record);
621
+ }, this);
622
+ clean.clear();
623
+ },
624
+
625
+ /**
626
+ @private
627
+
628
+ Returns the bucket for the given bucket type. For example, you might call
629
+ `this.bucketForType('updated')` to get the `Ember.Map` that contains all
630
+ of the records that have changes pending.
631
+
632
+ @param {String} bucketType the type of bucket
633
+ @returns Ember.Map
634
+ */
635
+ bucketForType: function(bucketType) {
636
+ var buckets = get(this, 'buckets');
637
+
638
+ return get(buckets, bucketType);
639
+ },
640
+
641
+ /**
642
+ @private
643
+
644
+ This method moves a record into a different transaction without the normal
645
+ checks that ensure that the user is not doing something weird, like moving
646
+ a dirty record into a new transaction.
647
+
648
+ It is designed for internal use, such as when we are moving a clean record
649
+ into a new transaction when the transaction is committed.
650
+
651
+ This method must not be called unless the record is clean.
652
+
653
+ @param {DS.Model} record
654
+ */
655
+ adoptRecord: function(record) {
656
+ var oldTransaction = get(record, 'transaction');
657
+
658
+ if (oldTransaction) {
659
+ oldTransaction.removeFromBucket('clean', record);
660
+ }
661
+
662
+ this.addToBucket('clean', record);
663
+ set(record, 'transaction', this);
664
+ },
665
+
666
+ /**
667
+ @private
668
+
669
+ Adds a record to the named bucket.
670
+
671
+ @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
672
+ */
673
+ addToBucket: function(bucketType, record) {
674
+ this.bucketForType(bucketType).add(record);
675
+ },
676
+
677
+ /**
678
+ @private
679
+
680
+ Removes a record from the named bucket.
681
+
682
+ @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
683
+ */
684
+ removeFromBucket: function(bucketType, record) {
685
+ this.bucketForType(bucketType).remove(record);
686
+ },
687
+
688
+ /**
689
+ @private
690
+
691
+ Called by a record's state manager to indicate that the record has entered
692
+ a dirty state. The record will be moved from the `clean` bucket and into
693
+ the appropriate dirty bucket.
694
+
695
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
696
+ */
697
+ recordBecameDirty: function(bucketType, record) {
698
+ this.removeFromBucket('clean', record);
699
+ this.addToBucket(bucketType, record);
700
+ },
701
+
702
+ /**
703
+ @private
704
+
705
+ Called by a record's state manager to indicate that the record has entered
706
+ inflight state. The record will be moved from its current dirty bucket and into
707
+ the `inflight` bucket.
708
+
709
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
710
+ */
711
+ recordBecameInFlight: function(kind, record) {
712
+ this.removeFromBucket(kind, record);
713
+ this.addToBucket('inflight', record);
714
+ },
715
+
716
+ recordIsMoving: function(kind, record) {
717
+ this.removeFromBucket(kind, record);
718
+ this.addToBucket('clean', record);
719
+ },
720
+
721
+ /**
722
+ @private
723
+
724
+ Called by a record's state manager to indicate that the record has entered
725
+ a clean state. The record will be moved from its current dirty or inflight bucket and into
726
+ the `clean` bucket.
727
+
728
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
729
+ */
730
+ recordBecameClean: function(kind, record) {
731
+ this.removeFromBucket(kind, record);
732
+ this.remove(record);
733
+ }
734
+ });
735
+
736
+ })();
737
+
738
+
739
+
740
+ (function() {
741
+ var classify = Ember.String.classify, get = Ember.get;
742
+
743
+ /**
744
+ @private
745
+
746
+ The Mappable mixin is designed for classes that would like to
747
+ behave as a map for configuration purposes.
748
+
749
+ For example, the DS.Adapter class can behave like a map, with
750
+ more semantic API, via the `map` API:
751
+
752
+ DS.Adapter.map('App.Person', { firstName: { keyName: 'FIRST' } });
753
+
754
+ Class configuration via a map-like API has a few common requirements
755
+ that differentiate it from the standard Ember.Map implementation.
756
+
757
+ First, values often are provided as strings that should be normalized
758
+ into classes the first time the configuration options are used.
759
+
760
+ Second, the values configured on parent classes should also be taken
761
+ into account.
762
+
763
+ Finally, setting the value of a key sometimes should merge with the
764
+ previous value, rather than replacing it.
765
+
766
+ This mixin provides a instance method, `createInstanceMapFor`, that
767
+ will reify all of the configuration options set on an instance's
768
+ constructor and provide it for the instance to use.
769
+
770
+ Classes can implement certain hooks that allow them to customize
771
+ the requirements listed above:
772
+
773
+ * `resolveMapConflict` - called when a value is set for an existing
774
+ value
775
+ * `transformMapKey` - allows a key name (for example, a global path
776
+ to a class) to be normalized
777
+ * `transformMapValue` - allows a value (for example, a class that
778
+ should be instantiated) to be normalized
779
+
780
+ Classes that implement this mixin should also implement a class
781
+ method built using the `generateMapFunctionFor` method:
782
+
783
+ DS.Adapter.reopenClass({
784
+ map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) {
785
+ var existingValue = map.get(key);
786
+
787
+ for (var prop in newValue) {
788
+ if (!newValue.hasOwnProperty(prop)) { continue; }
789
+ existingValue[prop] = newValue[prop];
790
+ }
791
+ })
792
+ });
793
+
794
+ The function passed to `generateMapFunctionFor` is invoked every time a
795
+ new value is added to the map.
796
+ **/
797
+
798
+ var resolveMapConflict = function(oldValue, newValue, mappingsKey) {
799
+ return oldValue;
800
+ };
801
+
802
+ var transformMapKey = function(key, value) {
803
+ return key;
804
+ };
805
+
806
+ var transformMapValue = function(key, value) {
807
+ return value;
808
+ };
809
+
810
+ DS._Mappable = Ember.Mixin.create({
811
+ createInstanceMapFor: function(mapName) {
812
+ var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true);
813
+
814
+ instanceMeta.values = instanceMeta.values || {};
815
+
816
+ if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; }
817
+
818
+ var instanceMap = instanceMeta.values[mapName] = new Ember.Map();
819
+
820
+ var klass = this.constructor;
821
+
822
+ while (klass && klass !== DS.Store) {
823
+ this._copyMap(mapName, klass, instanceMap);
824
+ klass = klass.superclass;
825
+ }
826
+
827
+ instanceMeta.values[mapName] = instanceMap;
828
+ return instanceMap;
829
+ },
830
+
831
+ _copyMap: function(mapName, klass, instanceMap) {
832
+ var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true);
833
+
834
+ var classMap = classMeta[mapName];
835
+ if (classMap) {
836
+ classMap.forEach(eachMap, this);
837
+ }
838
+
839
+ function eachMap(key, value) {
840
+ var transformedKey = (klass.transformMapKey || transformMapKey)(key, value);
841
+ var transformedValue = (klass.transformMapValue || transformMapValue)(key, value);
842
+
843
+ var oldValue = instanceMap.get(transformedKey);
844
+ var newValue = transformedValue;
845
+
846
+ if (oldValue) {
847
+ newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue, mapName);
848
+ }
849
+
850
+ instanceMap.set(transformedKey, newValue);
851
+ }
852
+ }
853
+
854
+
855
+ });
856
+
857
+ DS._Mappable.generateMapFunctionFor = function(mapName, transform) {
858
+ return function(key, value) {
859
+ var meta = Ember.metaPath(this, ['DS.Mappable'], true);
860
+ var map = meta[mapName] || Ember.MapWithDefault.create({
861
+ defaultValue: function() { return {}; }
862
+ });
863
+
864
+ transform.call(this, key, value, map);
865
+
866
+ meta[mapName] = map;
867
+ };
868
+ };
869
+
870
+ })();
871
+
872
+
873
+
874
+ (function() {
875
+ /*globals Ember*/
876
+ /*jshint eqnull:true*/
877
+ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt, once = Ember.run.once;
878
+ var forEach = Ember.EnumerableUtils.forEach;
879
+ // These values are used in the data cache when clientIds are
880
+ // needed but the underlying data has not yet been loaded by
881
+ // the server.
882
+ var UNLOADED = 'unloaded';
883
+ var LOADING = 'loading';
884
+ var MATERIALIZED = { materialized: true };
885
+ var CREATED = { created: true };
886
+
887
+ // Implementors Note:
888
+ //
889
+ // The variables in this file are consistently named according to the following
890
+ // scheme:
891
+ //
892
+ // * +id+ means an identifier managed by an external source, provided inside
893
+ // the data provided by that source.
894
+ // * +clientId+ means a transient numerical identifier generated at runtime by
895
+ // the data store. It is important primarily because newly created objects may
896
+ // not yet have an externally generated id.
897
+ // * +type+ means a subclass of DS.Model.
898
+
899
+ // Used by the store to normalize IDs entering the store. Despite the fact
900
+ // that developers may provide IDs as numbers (e.g., `store.find(Person, 1)`),
901
+ // it is important that internally we use strings, since IDs may be serialized
902
+ // and lose type information. For example, Ember's router may put a record's
903
+ // ID into the URL, and if we later try to deserialize that URL and find the
904
+ // corresponding record, we will not know if it is a string or a number.
905
+ var coerceId = function(id) {
906
+ return id == null ? null : id+'';
907
+ };
908
+
909
+ var map = Ember.EnumerableUtils.map;
910
+
911
+ /**
912
+ The store contains all of the data for records loaded from the server.
913
+ It is also responsible for creating instances of DS.Model that wraps
914
+ the individual data for a record, so that they can be bound to in your
915
+ Handlebars templates.
916
+
917
+ Create a new store like this:
918
+
919
+ MyApp.store = DS.Store.create();
920
+
921
+ You can retrieve DS.Model instances from the store in several ways. To retrieve
922
+ a record for a specific id, use the `find()` method:
923
+
924
+ var record = MyApp.store.find(MyApp.Contact, 123);
925
+
926
+ By default, the store will talk to your backend using a standard REST mechanism.
927
+ You can customize how the store talks to your backend by specifying a custom adapter:
928
+
929
+ MyApp.store = DS.Store.create({
930
+ adapter: 'MyApp.CustomAdapter'
931
+ });
932
+
933
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
934
+ documentation.
935
+ */
936
+ DS.Store = Ember.Object.extend(DS._Mappable, {
937
+
938
+ /**
939
+ Many methods can be invoked without specifying which store should be used.
940
+ In those cases, the first store created will be used as the default. If
941
+ an application has multiple stores, it should specify which store to use
942
+ when performing actions, such as finding records by id.
943
+
944
+ The init method registers this store as the default if none is specified.
945
+ */
946
+ init: function() {
947
+ // Enforce API revisioning. See BREAKING_CHANGES.md for more.
948
+ var revision = get(this, 'revision');
949
+
950
+ if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
951
+ throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION);
952
+ }
953
+
954
+ if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
955
+ set(DS, 'defaultStore', this);
956
+ }
957
+
958
+ // internal bookkeeping; not observable
959
+ this.typeMaps = {};
960
+ this.recordCache = [];
961
+ this.clientIdToId = {};
962
+ this.clientIdToType = {};
963
+ this.clientIdToData = {};
964
+ this.clientIdToPrematerializedData = {};
965
+ this.recordArraysByClientId = {};
966
+ this.relationshipChanges = {};
967
+ this.recordReferences = {};
968
+
969
+ // Internally, we maintain a map of all unloaded IDs requested by
970
+ // a ManyArray. As the adapter loads data into the store, the
971
+ // store notifies any interested ManyArrays. When the ManyArray's
972
+ // total number of loading records drops to zero, it becomes
973
+ // `isLoaded` and fires a `didLoad` event.
974
+ this.loadingRecordArrays = {};
975
+
976
+ set(this, 'defaultTransaction', this.transaction());
977
+ },
978
+
979
+ /**
980
+ Returns a new transaction scoped to this store. This delegates
981
+ responsibility for invoking the adapter's commit mechanism to
982
+ a transaction.
983
+
984
+ Transaction are responsible for tracking changes to records
985
+ added to them, and supporting `commit` and `rollback`
986
+ functionality. Committing a transaction invokes the store's
987
+ adapter, while rolling back a transaction reverses all
988
+ changes made to records added to the transaction.
989
+
990
+ A store has an implicit (default) transaction, which tracks changes
991
+ made to records not explicitly added to a transaction.
992
+
993
+ @see {DS.Transaction}
994
+ @returns DS.Transaction
995
+ */
996
+ transaction: function() {
997
+ return DS.Transaction.create({ store: this });
998
+ },
999
+
1000
+ ensureSameTransaction: function(records){
1001
+ var transactions = Ember.A();
1002
+ forEach( records, function(record){
1003
+ if (record){ transactions.pushObject(get(record, 'transaction')); }
1004
+ });
1005
+
1006
+ var transaction = transactions.reduce(function(prev, t) {
1007
+ if (!get(t, 'isDefault')) {
1008
+ if (prev === null) { return t; }
1009
+ Ember.assert("All records in a changed relationship must be in the same transaction. You tried to change the relationship between records when one is in " + t + " and the other is in " + prev, t === prev);
1010
+ }
1011
+
1012
+ return prev;
1013
+ }, null);
1014
+
1015
+ if (transaction) {
1016
+ forEach( records, function(record){
1017
+ if (record){ transaction.add(record); }
1018
+ });
1019
+ } else {
1020
+ transaction = transactions.objectAt(0);
1021
+ }
1022
+ return transaction;
1023
+
1024
+ },
1025
+ /**
1026
+ @private
1027
+
1028
+ Instructs the store to materialize the data for a given record.
1029
+
1030
+ To materialize a record, the store first retrieves the opaque data that was
1031
+ passed to either `load()` or `loadMany()`. Then, the data and the record
1032
+ are passed to the adapter's `materialize()` method, which allows the adapter
1033
+ to translate arbitrary data structures from the adapter into the normalized
1034
+ form the record expects.
1035
+
1036
+ The adapter's `materialize()` method will invoke `materializeAttribute()`,
1037
+ `materializeHasMany()` and `materializeBelongsTo()` on the record to
1038
+ populate it with normalized values.
1039
+
1040
+ @param {DS.Model} record
1041
+ */
1042
+ materializeData: function(record) {
1043
+ var clientId = get(record, 'clientId'),
1044
+ cidToData = this.clientIdToData,
1045
+ adapter = this.adapterForType(record.constructor),
1046
+ data = cidToData[clientId];
1047
+
1048
+ cidToData[clientId] = MATERIALIZED;
1049
+
1050
+ var prematerialized = this.clientIdToPrematerializedData[clientId];
1051
+
1052
+ // Ensures the record's data structures are setup
1053
+ // before being populated by the adapter.
1054
+ record.setupData();
1055
+
1056
+ if (data !== CREATED) {
1057
+ // Instructs the adapter to extract information from the
1058
+ // opaque data and materialize the record's attributes and
1059
+ // relationships.
1060
+ adapter.materialize(record, data, prematerialized);
1061
+ }
1062
+ },
1063
+
1064
+ /**
1065
+ @private
1066
+
1067
+ Returns true if there is already a record for this clientId.
1068
+
1069
+ This is used to determine whether cleanup is required, so that
1070
+ "changes" to unmaterialized records do not trigger mass
1071
+ materialization.
1072
+
1073
+ For example, if a parent record in a relationship with a large
1074
+ number of children is deleted, we want to avoid materializing
1075
+ those children.
1076
+
1077
+ @param {Object} reference
1078
+ @return {Boolean}
1079
+ */
1080
+ recordIsMaterialized: function(reference) {
1081
+ return !!this.recordCache[reference.clientId];
1082
+ },
1083
+
1084
+ /**
1085
+ The adapter to use to communicate to a backend server or other persistence layer.
1086
+
1087
+ This can be specified as an instance, a class, or a property path that specifies
1088
+ where the adapter can be located.
1089
+
1090
+ @property {DS.Adapter|String}
1091
+ */
1092
+ adapter: 'DS.RESTAdapter',
1093
+
1094
+ /**
1095
+ @private
1096
+
1097
+ Returns a JSON representation of the record using the adapter's
1098
+ serialization strategy. This method exists primarily to enable
1099
+ a record, which has access to its store (but not the store's
1100
+ adapter) to provide a `serialize()` convenience.
1101
+
1102
+ The available options are:
1103
+
1104
+ * `includeId`: `true` if the record's ID should be included in
1105
+ the JSON representation
1106
+
1107
+ @param {DS.Model} record the record to serialize
1108
+ @param {Object} options an options hash
1109
+ */
1110
+ serialize: function(record, options) {
1111
+ return this.adapterForType(record.constructor).serialize(record, options);
1112
+ },
1113
+
1114
+ /**
1115
+ @private
1116
+
1117
+ This property returns the adapter, after resolving a possible
1118
+ property path.
1119
+
1120
+ If the supplied `adapter` was a class, or a String property
1121
+ path resolved to a class, this property will instantiate the
1122
+ class.
1123
+
1124
+ This property is cacheable, so the same instance of a specified
1125
+ adapter class should be used for the lifetime of the store.
1126
+
1127
+ @returns DS.Adapter
1128
+ */
1129
+ _adapter: Ember.computed(function() {
1130
+ var adapter = get(this, 'adapter');
1131
+ if (typeof adapter === 'string') {
1132
+ adapter = get(this, adapter, false) || get(Ember.lookup, adapter);
1133
+ }
1134
+
1135
+ if (DS.Adapter.detect(adapter)) {
1136
+ adapter = adapter.create();
1137
+ }
1138
+
1139
+ return adapter;
1140
+ }).property('adapter'),
1141
+
1142
+ /**
1143
+ @private
1144
+
1145
+ A monotonically increasing number to be used to uniquely identify
1146
+ data and records.
1147
+
1148
+ It starts at 1 so other parts of the code can test for truthiness
1149
+ when provided a `clientId` instead of having to explicitly test
1150
+ for undefined.
1151
+ */
1152
+ clientIdCounter: 1,
1153
+
1154
+ // .....................
1155
+ // . CREATE NEW RECORD .
1156
+ // .....................
1157
+
1158
+ /**
1159
+ Create a new record in the current store. The properties passed
1160
+ to this method are set on the newly created record.
1161
+
1162
+ Note: The third `transaction` property is for internal use only.
1163
+ If you want to create a record inside of a given transaction,
1164
+ use `transaction.createRecord()` instead of `store.createRecord()`.
1165
+
1166
+ @param {subclass of DS.Model} type
1167
+ @param {Object} properties a hash of properties to set on the
1168
+ newly created record.
1169
+ @returns DS.Model
1170
+ */
1171
+ createRecord: function(type, properties, transaction) {
1172
+ properties = properties || {};
1173
+
1174
+ // Create a new instance of the model `type` and put it
1175
+ // into the specified `transaction`. If no transaction is
1176
+ // specified, the default transaction will be used.
1177
+ var record = type._create({
1178
+ store: this
1179
+ });
1180
+
1181
+ transaction = transaction || get(this, 'defaultTransaction');
1182
+
1183
+ // adoptRecord is an internal API that allows records to move
1184
+ // into a transaction without assertions designed for app
1185
+ // code. It is used here to ensure that regardless of new
1186
+ // restrictions on the use of the public `transaction.add()`
1187
+ // API, we will always be able to insert new records into
1188
+ // their transaction.
1189
+ transaction.adoptRecord(record);
1190
+
1191
+ // `id` is a special property that may not be a `DS.attr`
1192
+ var id = properties.id;
1193
+
1194
+ // If the passed properties do not include a primary key,
1195
+ // give the adapter an opportunity to generate one. Typically,
1196
+ // client-side ID generators will use something like uuid.js
1197
+ // to avoid conflicts.
1198
+ var adapter;
1199
+ if (Ember.isNone(id)) {
1200
+ adapter = this.adapterForType(type);
1201
+ if (adapter && adapter.generateIdForRecord) {
1202
+ id = coerceId(adapter.generateIdForRecord(this, record));
1203
+ properties.id = id;
1204
+ }
1205
+ }
1206
+
1207
+ id = coerceId(id);
1208
+
1209
+ // Create a new `clientId` and associate it with the
1210
+ // specified (or generated) `id`. Since we don't have
1211
+ // any data for the server yet (by definition), store
1212
+ // the sentinel value CREATED as the data for this
1213
+ // clientId. If we see this value later, we will skip
1214
+ // materialization.
1215
+ var clientId = this.pushData(CREATED, id, type);
1216
+
1217
+ // Now that we have a clientId, attach it to the record we
1218
+ // just created.
1219
+ set(record, 'clientId', clientId);
1220
+
1221
+ // Move the record out of its initial `empty` state into
1222
+ // the `loaded` state.
1223
+ record.loadedData();
1224
+
1225
+ // Make sure the data is set up so the record doesn't
1226
+ // try to materialize its nonexistent data.
1227
+ record.setupData();
1228
+
1229
+ // Store the record we just created in the record cache for
1230
+ // this clientId.
1231
+ this.recordCache[clientId] = record;
1232
+
1233
+ // Set the properties specified on the record.
1234
+ record.setProperties(properties);
1235
+
1236
+ // Resolve record promise
1237
+ Ember.run(record, 'resolve', record);
1238
+
1239
+ return record;
1240
+ },
1241
+
1242
+ // .................
1243
+ // . DELETE RECORD .
1244
+ // .................
1245
+
1246
+ /**
1247
+ For symmetry, a record can be deleted via the store.
1248
+
1249
+ @param {DS.Model} record
1250
+ */
1251
+ deleteRecord: function(record) {
1252
+ record.deleteRecord();
1253
+ },
1254
+
1255
+ /**
1256
+ For symmetry, a record can be unloaded via the store.
1257
+
1258
+ @param {DS.Model} record
1259
+ */
1260
+ unloadRecord: function(record) {
1261
+ record.unloadRecord();
1262
+ },
1263
+
1264
+ // ................
1265
+ // . FIND RECORDS .
1266
+ // ................
1267
+
1268
+ /**
1269
+ This is the main entry point into finding records. The first parameter to
1270
+ this method is always a subclass of `DS.Model`.
1271
+
1272
+ You can use the `find` method on a subclass of `DS.Model` directly if your
1273
+ application only has one store. For example, instead of
1274
+ `store.find(App.Person, 1)`, you could say `App.Person.find(1)`.
1275
+
1276
+ ---
1277
+
1278
+ To find a record by ID, pass the `id` as the second parameter:
1279
+
1280
+ store.find(App.Person, 1);
1281
+ App.Person.find(1);
1282
+
1283
+ If the record with that `id` had not previously been loaded, the store will
1284
+ return an empty record immediately and ask the adapter to find the data by
1285
+ calling the adapter's `find` method.
1286
+
1287
+ The `find` method will always return the same object for a given type and
1288
+ `id`. To check whether the adapter has populated a record, you can check
1289
+ its `isLoaded` property.
1290
+
1291
+ ---
1292
+
1293
+ To find all records for a type, call `find` with no additional parameters:
1294
+
1295
+ store.find(App.Person);
1296
+ App.Person.find();
1297
+
1298
+ This will return a `RecordArray` representing all known records for the
1299
+ given type and kick off a request to the adapter's `findAll` method to load
1300
+ any additional records for the type.
1301
+
1302
+ The `RecordArray` returned by `find()` is live. If any more records for the
1303
+ type are added at a later time through any mechanism, it will automatically
1304
+ update to reflect the change.
1305
+
1306
+ ---
1307
+
1308
+ To find a record by a query, call `find` with a hash as the second
1309
+ parameter:
1310
+
1311
+ store.find(App.Person, { page: 1 });
1312
+ App.Person.find({ page: 1 });
1313
+
1314
+ This will return a `RecordArray` immediately, but it will always be an
1315
+ empty `RecordArray` at first. It will call the adapter's `findQuery`
1316
+ method, which will populate the `RecordArray` once the server has returned
1317
+ results.
1318
+
1319
+ You can check whether a query results `RecordArray` has loaded by checking
1320
+ its `isLoaded` property.
1321
+ */
1322
+ find: function(type, id) {
1323
+ if (id === undefined) {
1324
+ return this.findAll(type);
1325
+ }
1326
+
1327
+ // We are passed a query instead of an id.
1328
+ if (Ember.typeOf(id) === 'object') {
1329
+ return this.findQuery(type, id);
1330
+ }
1331
+
1332
+ return this.findById(type, coerceId(id));
1333
+ },
1334
+
1335
+ /**
1336
+ @private
1337
+
1338
+ This method returns a record for a given type and id combination.
1339
+
1340
+ If the store has never seen this combination of type and id before, it
1341
+ creates a new `clientId` with the LOADING sentinel and asks the adapter to
1342
+ load the data.
1343
+
1344
+ If the store has seen the combination, this method delegates to
1345
+ `getByReference`.
1346
+ */
1347
+ findById: function(type, id) {
1348
+ var clientId = this.typeMapFor(type).idToCid[id];
1349
+
1350
+ if (clientId) {
1351
+ return this.findByClientId(type, clientId);
1352
+ }
1353
+
1354
+ clientId = this.pushData(LOADING, id, type);
1355
+
1356
+ // create a new instance of the model type in the
1357
+ // 'isLoading' state
1358
+ var record = this.materializeRecord(type, clientId, id);
1359
+
1360
+ // let the adapter set the data, possibly async
1361
+ var adapter = this.adapterForType(type);
1362
+ if (adapter && adapter.find) { adapter.find(this, type, id); }
1363
+ else { throw "Adapter is either null or does not implement `find` method"; }
1364
+
1365
+ return record;
1366
+ },
1367
+
1368
+ reloadRecord: function(record) {
1369
+ var type = record.constructor,
1370
+ adapter = this.adapterForType(type),
1371
+ id = get(record, 'id');
1372
+
1373
+ Ember.assert("You cannot update a record without an ID", id);
1374
+ Ember.assert("You tried to update a record but you have no adapter (for " + type + ")", adapter);
1375
+ Ember.assert("You tried to update a record but your adapter does not implement `find`", adapter.find);
1376
+
1377
+ adapter.find(this, type, id);
1378
+ },
1379
+
1380
+ /**
1381
+ @private
1382
+
1383
+ This method returns a record for a given clientId.
1384
+
1385
+ If there is no record object yet for the clientId, this method materializes
1386
+ a new record object. This allows adapters to eagerly load large amounts of
1387
+ data into the store, and avoid incurring the cost to create the objects
1388
+ until they are requested.
1389
+
1390
+ Several parts of Ember Data call this method:
1391
+
1392
+ * findById, if a clientId already exists for a given type and
1393
+ id combination
1394
+ * OneToManyChange, which is backed by clientIds, when getChild,
1395
+ getOldParent or getNewParent are called
1396
+ * RecordArray, which is backed by clientIds, when an object at
1397
+ a particular index is looked up
1398
+
1399
+ In short, it's a convenient way to get a record for a known
1400
+ clientId, materializing it if necessary.
1401
+
1402
+ @param {Class} type
1403
+ @param {Number|String} clientId
1404
+ */
1405
+ findByClientId: function(type, clientId) {
1406
+ var cidToData, record, id;
1407
+
1408
+ record = this.recordCache[clientId];
1409
+
1410
+ if (!record) {
1411
+ // create a new instance of the model type in the
1412
+ // 'isLoading' state
1413
+ id = this.clientIdToId[clientId];
1414
+ record = this.materializeRecord(type, clientId, id);
1415
+
1416
+ cidToData = this.clientIdToData;
1417
+
1418
+ if (typeof cidToData[clientId] === 'object') {
1419
+ record.loadedData();
1420
+ }
1421
+ }
1422
+
1423
+ return record;
1424
+ },
1425
+
1426
+ /**
1427
+ @private
1428
+
1429
+ Given a type and array of `clientId`s, determines which of those
1430
+ `clientId`s has not yet been loaded.
1431
+
1432
+ In preparation for loading, this method also marks any unloaded
1433
+ `clientId`s as loading.
1434
+ */
1435
+ neededReferences: function(type, references) {
1436
+ var neededReferences = [],
1437
+ cidToData = this.clientIdToData,
1438
+ reference;
1439
+
1440
+ for (var i=0, l=references.length; i<l; i++) {
1441
+ reference = references[i];
1442
+
1443
+ if (cidToData[reference.clientId] === UNLOADED) {
1444
+ neededReferences.push(reference);
1445
+ cidToData[reference.clientId] = LOADING;
1446
+ }
1447
+ }
1448
+
1449
+ return neededReferences;
1450
+ },
1451
+
1452
+ /**
1453
+ @private
1454
+
1455
+ This method is the entry point that relationships use to update
1456
+ themselves when their underlying data changes.
1457
+
1458
+ First, it determines which of its `clientId`s are still unloaded,
1459
+ then converts the needed `clientId`s to IDs and invokes `findMany`
1460
+ on the adapter.
1461
+ */
1462
+ fetchUnloadedReferences: function(type, references, owner) {
1463
+ var neededReferences = this.neededReferences(type, references);
1464
+ this.fetchMany(type, neededReferences, owner);
1465
+ },
1466
+
1467
+ /**
1468
+ @private
1469
+
1470
+ This method takes a type and list of `clientId`s, converts the
1471
+ `clientId`s into IDs, and then invokes the adapter's `findMany`
1472
+ method.
1473
+
1474
+ It is used both by a brand new relationship (via the `findMany`
1475
+ method) or when the data underlying an existing relationship
1476
+ changes (via the `fetchUnloadedReferences` method).
1477
+ */
1478
+ fetchMany: function(type, references, owner) {
1479
+ if (!references.length) { return; }
1480
+
1481
+ var ids = map(references, function(reference) {
1482
+ return reference.id;
1483
+ });
1484
+
1485
+ var adapter = this.adapterForType(type);
1486
+ if (adapter && adapter.findMany) { adapter.findMany(this, type, ids, owner); }
1487
+ else { throw "Adapter is either null or does not implement `findMany` method"; }
1488
+ },
1489
+
1490
+ referenceForId: function(type, id) {
1491
+ var clientId = this.clientIdForId(type, id);
1492
+ return this.referenceForClientId(clientId);
1493
+ },
1494
+
1495
+ referenceForClientId: function(clientId) {
1496
+ var references = this.recordReferences;
1497
+
1498
+ if (references[clientId]) {
1499
+ return references[clientId];
1500
+ }
1501
+
1502
+ var type = this.clientIdToType[clientId];
1503
+
1504
+ return references[clientId] = {
1505
+ id: this.idForClientId(clientId),
1506
+ clientId: clientId,
1507
+ type: type
1508
+ };
1509
+ },
1510
+
1511
+ recordForReference: function(reference) {
1512
+ return this.findByClientId(reference.type, reference.clientId);
1513
+ },
1514
+
1515
+ /**
1516
+ @private
1517
+
1518
+ `findMany` is the entry point that relationships use to generate a
1519
+ new `ManyArray` for the list of IDs specified by the server for
1520
+ the relationship.
1521
+
1522
+ Its responsibilities are:
1523
+
1524
+ * convert the IDs into clientIds
1525
+ * determine which of the clientIds still need to be loaded
1526
+ * create a new ManyArray whose content is *all* of the clientIds
1527
+ * notify the ManyArray of the number of its elements that are
1528
+ already loaded
1529
+ * insert the unloaded clientIds into the `loadingRecordArrays`
1530
+ bookkeeping structure, which will allow the `ManyArray` to know
1531
+ when all of its loading elements are loaded from the server.
1532
+ * ask the adapter to load the unloaded elements, by invoking
1533
+ findMany with the still-unloaded IDs.
1534
+ */
1535
+ findMany: function(type, ids, record, relationship) {
1536
+ // 1. Convert ids to client ids
1537
+ // 2. Determine which of the client ids need to be loaded
1538
+ // 3. Create a new ManyArray whose content is ALL of the clientIds
1539
+ // 4. Decrement the ManyArray's counter by the number of loaded clientIds
1540
+ // 5. Put the ManyArray into our bookkeeping data structure, keyed on
1541
+ // the needed clientIds
1542
+ // 6. Ask the adapter to load the records for the unloaded clientIds (but
1543
+ // convert them back to ids)
1544
+
1545
+ if (!Ember.isArray(ids)) {
1546
+ var adapter = this.adapterForType(type);
1547
+ if (adapter && adapter.findHasMany) { adapter.findHasMany(this, record, relationship, ids); }
1548
+ else { throw fmt("Adapter is either null or does not implement `findHasMany` method", this); }
1549
+
1550
+ return this.createManyArray(type, Ember.A());
1551
+ }
1552
+
1553
+ // Coerce server IDs into Record Reference
1554
+ var references = map(ids, function(reference) {
1555
+ if (typeof reference !== 'object' && reference !== null) {
1556
+ return this.referenceForId(type, reference);
1557
+ }
1558
+
1559
+ return reference;
1560
+ }, this);
1561
+
1562
+ var neededReferences = this.neededReferences(type, references),
1563
+ manyArray = this.createManyArray(type, Ember.A(references)),
1564
+ loadingRecordArrays = this.loadingRecordArrays,
1565
+ reference, clientId, i, l;
1566
+
1567
+ // Start the decrementing counter on the ManyArray at the number of
1568
+ // records we need to load from the adapter
1569
+ manyArray.loadingRecordsCount(neededReferences.length);
1570
+
1571
+ if (neededReferences.length) {
1572
+ for (i=0, l=neededReferences.length; i<l; i++) {
1573
+ reference = neededReferences[i];
1574
+ clientId = reference.clientId;
1575
+
1576
+ // keep track of the record arrays that a given loading record
1577
+ // is part of. This way, if the same record is in multiple
1578
+ // ManyArrays, all of their loading records counters will be
1579
+ // decremented when the adapter provides the data.
1580
+ if (loadingRecordArrays[clientId]) {
1581
+ loadingRecordArrays[clientId].push(manyArray);
1582
+ } else {
1583
+ this.loadingRecordArrays[clientId] = [ manyArray ];
1584
+ }
1585
+ }
1586
+
1587
+ this.fetchMany(type, neededReferences, record);
1588
+ } else {
1589
+ // all requested records are available
1590
+ manyArray.set('isLoaded', true);
1591
+
1592
+ Ember.run.once(function() {
1593
+ manyArray.trigger('didLoad');
1594
+ });
1595
+ }
1596
+
1597
+ return manyArray;
1598
+ },
1599
+
1600
+ /**
1601
+ @private
1602
+
1603
+ This method delegates a query to the adapter. This is the one place where
1604
+ adapter-level semantics are exposed to the application.
1605
+
1606
+ Exposing queries this way seems preferable to creating an abstract query
1607
+ language for all server-side queries, and then require all adapters to
1608
+ implement them.
1609
+
1610
+ @param {Class} type
1611
+ @param {Object} query an opaque query to be used by the adapter
1612
+ @return {DS.AdapterPopulatedRecordArray}
1613
+ */
1614
+ findQuery: function(type, query) {
1615
+ var array = DS.AdapterPopulatedRecordArray.create({ type: type, query: query, content: Ember.A([]), store: this });
1616
+ var adapter = this.adapterForType(type);
1617
+ if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
1618
+ else { throw "Adapter is either null or does not implement `findQuery` method"; }
1619
+ return array;
1620
+ },
1621
+
1622
+ /**
1623
+ @private
1624
+
1625
+ This method returns an array of all records adapter can find.
1626
+ It triggers the adapter's `findAll` method to give it an opportunity to populate
1627
+ the array with records of that type.
1628
+
1629
+ @param {Class} type
1630
+ @return {DS.AdapterPopulatedRecordArray}
1631
+ */
1632
+ findAll: function(type) {
1633
+ var array = this.all(type);
1634
+ this.fetchAll(type, array);
1635
+ return array;
1636
+ },
1637
+
1638
+ /**
1639
+ @private
1640
+ */
1641
+ fetchAll: function(type, array) {
1642
+ var sinceToken = this.typeMapFor(type).sinceToken,
1643
+ adapter = this.adapterForType(type);
1644
+
1645
+ set(array, 'isUpdating', true);
1646
+
1647
+ if (adapter && adapter.findAll) { adapter.findAll(this, type, sinceToken); }
1648
+ else { throw "Adapter is either null or does not implement `findAll` method"; }
1649
+ },
1650
+
1651
+ /**
1652
+ */
1653
+ sinceForType: function(type, sinceToken) {
1654
+ this.typeMapFor(type).sinceToken = sinceToken;
1655
+ },
1656
+
1657
+ /**
1658
+ */
1659
+ didUpdateAll: function(type) {
1660
+ var findAllCache = this.typeMapFor(type).findAllCache;
1661
+ set(findAllCache, 'isUpdating', false);
1662
+ },
1663
+
1664
+ /**
1665
+ This method returns a filtered array that contains all of the known records
1666
+ for a given type.
1667
+
1668
+ Note that because it's just a filter, it will have any locally
1669
+ created records of the type.
1670
+
1671
+ Also note that multiple calls to `all` for a given type will always
1672
+ return the same RecordArray.
1673
+
1674
+ @param {Class} type
1675
+ @return {DS.RecordArray}
1676
+ */
1677
+ all: function(type) {
1678
+ var typeMap = this.typeMapFor(type),
1679
+ findAllCache = typeMap.findAllCache;
1680
+
1681
+ if (findAllCache) { return findAllCache; }
1682
+
1683
+ var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this, isLoaded: true });
1684
+ this.registerRecordArray(array, type);
1685
+
1686
+ typeMap.findAllCache = array;
1687
+ return array;
1688
+ },
1689
+
1690
+ /**
1691
+ Takes a type and filter function, and returns a live RecordArray that
1692
+ remains up to date as new records are loaded into the store or created
1693
+ locally.
1694
+
1695
+ The callback function takes a materialized record, and returns true
1696
+ if the record should be included in the filter and false if it should
1697
+ not.
1698
+
1699
+ The filter function is called once on all records for the type when
1700
+ it is created, and then once on each newly loaded or created record.
1701
+
1702
+ If any of a record's properties change, or if it changes state, the
1703
+ filter function will be invoked again to determine whether it should
1704
+ still be in the array.
1705
+
1706
+ Note that the existence of a filter on a type will trigger immediate
1707
+ materialization of all loaded data for a given type, so you might
1708
+ not want to use filters for a type if you are loading many records
1709
+ into the store, many of which are not active at any given time.
1710
+
1711
+ In this scenario, you might want to consider filtering the raw
1712
+ data before loading it into the store.
1713
+
1714
+ @param {Class} type
1715
+ @param {Function} filter
1716
+
1717
+ @return {DS.FilteredRecordArray}
1718
+ */
1719
+ filter: function(type, query, filter) {
1720
+ // allow an optional server query
1721
+ if (arguments.length === 3) {
1722
+ this.findQuery(type, query);
1723
+ } else if (arguments.length === 2) {
1724
+ filter = query;
1725
+ }
1726
+
1727
+ var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
1728
+
1729
+ this.registerRecordArray(array, type, filter);
1730
+
1731
+ return array;
1732
+ },
1733
+
1734
+ /**
1735
+ This method returns if a certain record is already loaded
1736
+ in the store. Use this function to know beforehand if a find()
1737
+ will result in a request or that it will be a cache hit.
1738
+
1739
+ @param {Class} type
1740
+ @param {string} id
1741
+ @return {boolean}
1742
+ */
1743
+ recordIsLoaded: function(type, id) {
1744
+ return !Ember.isNone(this.typeMapFor(type).idToCid[id]);
1745
+ },
1746
+
1747
+ // ............
1748
+ // . UPDATING .
1749
+ // ............
1750
+
1751
+ /**
1752
+ @private
1753
+
1754
+ If the adapter updates attributes or acknowledges creation
1755
+ or deletion, the record will notify the store to update its
1756
+ membership in any filters.
1757
+
1758
+ To avoid thrashing, this method is invoked only once per
1759
+ run loop per record.
1760
+
1761
+ @param {Class} type
1762
+ @param {Number|String} clientId
1763
+ @param {DS.Model} record
1764
+ */
1765
+ dataWasUpdated: function(type, reference, record) {
1766
+ // Because data updates are invoked at the end of the run loop,
1767
+ // it is possible that a record might be deleted after its data
1768
+ // has been modified and this method was scheduled to be called.
1769
+ //
1770
+ // If that's the case, the record would have already been removed
1771
+ // from all record arrays; calling updateRecordArrays would just
1772
+ // add it back. If the record is deleted, just bail. It shouldn't
1773
+ // give us any more trouble after this.
1774
+
1775
+ if (get(record, 'isDeleted')) { return; }
1776
+
1777
+ var cidToData = this.clientIdToData,
1778
+ clientId = reference.clientId,
1779
+ data = cidToData[clientId];
1780
+
1781
+ if (typeof data === "object") {
1782
+ this.updateRecordArrays(type, clientId);
1783
+ }
1784
+ },
1785
+
1786
+ // ..............
1787
+ // . PERSISTING .
1788
+ // ..............
1789
+
1790
+ /**
1791
+ This method delegates committing to the store's implicit
1792
+ transaction.
1793
+
1794
+ Calling this method is essentially a request to persist
1795
+ any changes to records that were not explicitly added to
1796
+ a transaction.
1797
+ */
1798
+ commit: function() {
1799
+ get(this, 'defaultTransaction').commit();
1800
+ },
1801
+
1802
+ /**
1803
+ Adapters should call this method if they would like to acknowledge
1804
+ that all changes related to a record (other than relationship
1805
+ changes) have persisted.
1806
+
1807
+ Because relationship changes affect multiple records, the adapter
1808
+ is responsible for acknowledging the change to the relationship
1809
+ directly (using `store.didUpdateRelationship`) when all aspects
1810
+ of the relationship change have persisted.
1811
+
1812
+ It can be called for created, deleted or updated records.
1813
+
1814
+ If the adapter supplies new data, that data will become the new
1815
+ canonical data for the record. That will result in blowing away
1816
+ all local changes and rematerializing the record with the new
1817
+ data (the "sledgehammer" approach).
1818
+
1819
+ Alternatively, if the adapter does not supply new data, the record
1820
+ will collapse all local changes into its saved data. Subsequent
1821
+ rollbacks of the record will roll back to this point.
1822
+
1823
+ If an adapter is acknowledging receipt of a newly created record
1824
+ that did not generate an id in the client, it *must* either
1825
+ provide data or explicitly invoke `store.didReceiveId` with
1826
+ the server-provided id.
1827
+
1828
+ Note that an adapter may not supply new data when acknowledging
1829
+ a deleted record.
1830
+
1831
+ @see DS.Store#didUpdateRelationship
1832
+
1833
+ @param {DS.Model} record the in-flight record
1834
+ @param {Object} data optional data (see above)
1835
+ */
1836
+ didSaveRecord: function(record, data) {
1837
+ record.adapterDidCommit();
1838
+
1839
+ if (data) {
1840
+ this.updateId(record, data);
1841
+ this.updateRecordData(record, data);
1842
+ } else {
1843
+ this.didUpdateAttributes(record);
1844
+ }
1845
+ },
1846
+
1847
+ /**
1848
+ For convenience, if an adapter is performing a bulk commit, it can also
1849
+ acknowledge all of the records at once.
1850
+
1851
+ If the adapter supplies an array of data, they must be in the same order as
1852
+ the array of records passed in as the first parameter.
1853
+
1854
+ @param {#forEach} list a list of records whose changes the
1855
+ adapter is acknowledging. You can pass any object that
1856
+ has an ES5-like `forEach` method, including the
1857
+ `OrderedSet` objects passed into the adapter at commit
1858
+ time.
1859
+ @param {Array[Object]} dataList an Array of data. This
1860
+ parameter must be an integer-indexed Array-like.
1861
+ */
1862
+ didSaveRecords: function(list, dataList) {
1863
+ var i = 0;
1864
+ list.forEach(function(record) {
1865
+ this.didSaveRecord(record, dataList && dataList[i++]);
1866
+ }, this);
1867
+ },
1868
+
1869
+ /**
1870
+ This method allows the adapter to specify that a record
1871
+ could not be saved because it had backend-supplied validation
1872
+ errors.
1873
+
1874
+ The errors object must have keys that correspond to the
1875
+ attribute names. Once each of the specified attributes have
1876
+ changed, the record will automatically move out of the
1877
+ invalid state and be ready to commit again.
1878
+
1879
+ TODO: We should probably automate the process of converting
1880
+ server names to attribute names using the existing serializer
1881
+ infrastructure.
1882
+
1883
+ @param {DS.Model} record
1884
+ @param {Object} errors
1885
+ */
1886
+ recordWasInvalid: function(record, errors) {
1887
+ record.adapterDidInvalidate(errors);
1888
+ },
1889
+
1890
+ /**
1891
+ This method allows the adapter to specify that a record
1892
+ could not be saved because the server returned an unhandled
1893
+ error.
1894
+
1895
+ @param {DS.Model} record
1896
+ */
1897
+ recordWasError: function(record) {
1898
+ record.adapterDidError();
1899
+ },
1900
+
1901
+ /**
1902
+ This is a lower-level API than `didSaveRecord` that allows an
1903
+ adapter to acknowledge the persistence of a single attribute.
1904
+
1905
+ This is useful if an adapter needs to make multiple asynchronous
1906
+ calls to fully persist a record. The record will keep track of
1907
+ which attributes and relationships are still outstanding and
1908
+ automatically move into the `saved` state once the adapter has
1909
+ acknowledged everything.
1910
+
1911
+ If a value is provided, it clobbers the locally specified value.
1912
+ Otherwise, the local value becomes the record's last known
1913
+ saved value (which is used when rolling back a record).
1914
+
1915
+ Note that the specified attributeName is the normalized name
1916
+ specified in the definition of the `DS.Model`, not a key in
1917
+ the server-provided data.
1918
+
1919
+ Also note that the adapter is responsible for performing any
1920
+ transformations on the value using the serializer API.
1921
+
1922
+ @param {DS.Model} record
1923
+ @param {String} attributeName
1924
+ @param {Object} value
1925
+ */
1926
+ didUpdateAttribute: function(record, attributeName, value) {
1927
+ record.adapterDidUpdateAttribute(attributeName, value);
1928
+ },
1929
+
1930
+ /**
1931
+ This method allows an adapter to acknowledge persistence
1932
+ of all attributes of a record but not relationships or
1933
+ other factors.
1934
+
1935
+ It loops through the record's defined attributes and
1936
+ notifies the record that they are all acknowledged.
1937
+
1938
+ This method does not take optional values, because
1939
+ the adapter is unlikely to have a hash of normalized
1940
+ keys and transformed values, and instead of building
1941
+ one up, it should just call `didUpdateAttribute` as
1942
+ needed.
1943
+
1944
+ This method is intended as a middle-ground between
1945
+ `didSaveRecord`, which acknowledges all changes to
1946
+ a record, and `didUpdateAttribute`, which allows an
1947
+ adapter fine-grained control over updates.
1948
+
1949
+ @param {DS.Model} record
1950
+ */
1951
+ didUpdateAttributes: function(record) {
1952
+ record.eachAttribute(function(attributeName) {
1953
+ this.didUpdateAttribute(record, attributeName);
1954
+ }, this);
1955
+ },
1956
+
1957
+ /**
1958
+ This allows an adapter to acknowledge that it has saved all
1959
+ necessary aspects of a relationship change.
1960
+
1961
+ This is separated from acknowledging the record itself
1962
+ (via `didSaveRecord`) because a relationship change can
1963
+ involve as many as three separate records. Records should
1964
+ only move out of the in-flight state once the server has
1965
+ acknowledged all of their relationships, and this differs
1966
+ based upon the adapter's semantics.
1967
+
1968
+ There are three basic scenarios by which an adapter can
1969
+ save a relationship.
1970
+
1971
+ ### Foreign Key
1972
+
1973
+ An adapter can save all relationship changes by updating
1974
+ a foreign key on the child record. If it does this, it
1975
+ should acknowledge the changes when the child record is
1976
+ saved.
1977
+
1978
+ record.eachRelationship(function(name, meta) {
1979
+ if (meta.kind === 'belongsTo') {
1980
+ store.didUpdateRelationship(record, name);
1981
+ }
1982
+ });
1983
+
1984
+ store.didSaveRecord(record, data);
1985
+
1986
+ ### Embedded in Parent
1987
+
1988
+ An adapter can save one-to-many relationships by embedding
1989
+ IDs (or records) in the parent object. In this case, the
1990
+ relationship is not considered acknowledged until both the
1991
+ old parent and new parent have acknowledged the change.
1992
+
1993
+ In this case, the adapter should keep track of the old
1994
+ parent and new parent, and acknowledge the relationship
1995
+ change once both have acknowledged. If one of the two
1996
+ sides does not exist (e.g. the new parent does not exist
1997
+ because of nulling out the belongs-to relationship),
1998
+ the adapter should acknowledge the relationship once
1999
+ the other side has acknowledged.
2000
+
2001
+ ### Separate Entity
2002
+
2003
+ An adapter can save relationships as separate entities
2004
+ on the server. In this case, they should acknowledge
2005
+ the relationship as saved once the server has
2006
+ acknowledged the entity.
2007
+
2008
+ @see DS.Store#didSaveRecord
2009
+
2010
+ @param {DS.Model} record
2011
+ @param {DS.Model} relationshipName
2012
+ */
2013
+ didUpdateRelationship: function(record, relationshipName) {
2014
+ var relationship = this.relationshipChangeFor(get(record, 'clientId'), relationshipName);
2015
+ //TODO(Igor)
2016
+ if (relationship) { relationship.adapterDidUpdate(); }
2017
+ },
2018
+
2019
+ /**
2020
+ This allows an adapter to acknowledge all relationship changes
2021
+ for a given record.
2022
+
2023
+ Like `didUpdateAttributes`, this is intended as a middle ground
2024
+ between `didSaveRecord` and fine-grained control via the
2025
+ `didUpdateRelationship` API.
2026
+ */
2027
+ didUpdateRelationships: function(record) {
2028
+ var changes = this.relationshipChangesFor(get(record, '_reference'));
2029
+
2030
+ for (var name in changes) {
2031
+ if (!changes.hasOwnProperty(name)) { continue; }
2032
+ changes[name].adapterDidUpdate();
2033
+ }
2034
+ },
2035
+
2036
+ /**
2037
+ When acknowledging the creation of a locally created record,
2038
+ adapters must supply an id (if they did not implement
2039
+ `generateIdForRecord` to generate an id locally).
2040
+
2041
+ If an adapter does not use `didSaveRecord` and supply a hash
2042
+ (for example, if it needs to make multiple HTTP requests to
2043
+ create and then update the record), it will need to invoke
2044
+ `didReceiveId` with the backend-supplied id.
2045
+
2046
+ When not using `didSaveRecord`, an adapter will need to
2047
+ invoke:
2048
+
2049
+ * didReceiveId (unless the id was generated locally)
2050
+ * didCreateRecord
2051
+ * didUpdateAttribute(s)
2052
+ * didUpdateRelationship(s)
2053
+
2054
+ @param {DS.Model} record
2055
+ @param {Number|String} id
2056
+ */
2057
+ didReceiveId: function(record, id) {
2058
+ var typeMap = this.typeMapFor(record.constructor),
2059
+ clientId = get(record, 'clientId'),
2060
+ oldId = get(record, 'id');
2061
+
2062
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === undefined || id === oldId);
2063
+
2064
+ typeMap.idToCid[id] = clientId;
2065
+ this.clientIdToId[clientId] = id;
2066
+ },
2067
+
2068
+ /**
2069
+ @private
2070
+
2071
+ This method re-indexes the data by its clientId in the store
2072
+ and then notifies the record that it should rematerialize
2073
+ itself.
2074
+
2075
+ @param {DS.Model} record
2076
+ @param {Object} data
2077
+ */
2078
+ updateRecordData: function(record, data) {
2079
+ var clientId = get(record, 'clientId'),
2080
+ cidToData = this.clientIdToData;
2081
+
2082
+ cidToData[clientId] = data;
2083
+
2084
+ record.didChangeData();
2085
+ },
2086
+
2087
+ /**
2088
+ @private
2089
+
2090
+ If an adapter invokes `didSaveRecord` with data, this method
2091
+ extracts the id from the supplied data (using the adapter's
2092
+ `extractId()` method) and indexes the clientId with that id.
2093
+
2094
+ @param {DS.Model} record
2095
+ @param {Object} data
2096
+ */
2097
+ updateId: function(record, data) {
2098
+ var typeMap = this.typeMapFor(record.constructor),
2099
+ clientId = get(record, 'clientId'),
2100
+ oldId = get(record, 'id'),
2101
+ type = record.constructor,
2102
+ id = this.preprocessData(type, data);
2103
+
2104
+ Ember.assert("An adapter cannot assign a new id to a record that already has an id. " + record + " had id: " + oldId + " and you tried to update it with " + id + ". This likely happened because your server returned data in response to a find or update that had a different id than the one you sent.", oldId === null || id === oldId);
2105
+
2106
+ typeMap.idToCid[id] = clientId;
2107
+ this.clientIdToId[clientId] = id;
2108
+ this.referenceForClientId(clientId).id = id;
2109
+ },
2110
+
2111
+ /**
2112
+ @private
2113
+
2114
+ This method receives opaque data provided by the adapter and
2115
+ preprocesses it, returning an ID.
2116
+
2117
+ The actual preprocessing takes place in the adapter. If you would
2118
+ like to change the default behavior, you should override the
2119
+ appropriate hooks in `DS.Serializer`.
2120
+
2121
+ @see {DS.Serializer}
2122
+ @return {String} id the id represented by the data
2123
+ */
2124
+ preprocessData: function(type, data) {
2125
+ return this.adapterForType(type).extractId(type, data);
2126
+ },
2127
+
2128
+ // .................
2129
+ // . RECORD ARRAYS .
2130
+ // .................
2131
+
2132
+ /**
2133
+ @private
2134
+
2135
+ Register a RecordArray for a given type to be backed by
2136
+ a filter function. This will cause the array to update
2137
+ automatically when records of that type change attribute
2138
+ values or states.
2139
+
2140
+ @param {DS.RecordArray} array
2141
+ @param {Class} type
2142
+ @param {Function} filter
2143
+ */
2144
+ registerRecordArray: function(array, type, filter) {
2145
+ var recordArrays = this.typeMapFor(type).recordArrays;
2146
+
2147
+ recordArrays.push(array);
2148
+
2149
+ this.updateRecordArrayFilter(array, type, filter);
2150
+ },
2151
+
2152
+ /**
2153
+ @private
2154
+
2155
+ Create a `DS.ManyArray` for a type and list of clientIds
2156
+ and index the `ManyArray` under each clientId. This allows
2157
+ us to efficiently remove records from `ManyArray`s when
2158
+ they are deleted.
2159
+
2160
+ @param {Class} type
2161
+ @param {Array} clientIds
2162
+
2163
+ @return {DS.ManyArray}
2164
+ */
2165
+ createManyArray: function(type, clientIds) {
2166
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
2167
+
2168
+ clientIds.forEach(function(clientId) {
2169
+ var recordArrays = this.recordArraysForClientId(clientId);
2170
+ recordArrays.add(array);
2171
+ }, this);
2172
+
2173
+ return array;
2174
+ },
2175
+
2176
+ /**
2177
+ @private
2178
+
2179
+ This method is invoked if the `filterFunction` property is
2180
+ changed on a `DS.FilteredRecordArray`.
2181
+
2182
+ It essentially re-runs the filter from scratch. This same
2183
+ method is invoked when the filter is created in th first place.
2184
+ */
2185
+ updateRecordArrayFilter: function(array, type, filter) {
2186
+ var typeMap = this.typeMapFor(type),
2187
+ cidToData = this.clientIdToData,
2188
+ clientIds = typeMap.clientIds,
2189
+ clientId, data, shouldFilter, record;
2190
+
2191
+ for (var i=0, l=clientIds.length; i<l; i++) {
2192
+ clientId = clientIds[i];
2193
+ shouldFilter = false;
2194
+
2195
+ data = cidToData[clientId];
2196
+
2197
+ if (typeof data === 'object') {
2198
+ if (record = this.recordCache[clientId]) {
2199
+ if (!get(record, 'isDeleted')) { shouldFilter = true; }
2200
+ } else {
2201
+ shouldFilter = true;
2202
+ }
2203
+
2204
+ if (shouldFilter) {
2205
+ this.updateRecordArray(array, filter, type, clientId);
2206
+ }
2207
+ }
2208
+ }
2209
+ },
2210
+
2211
+ updateRecordArraysLater: function(type, clientId) {
2212
+ Ember.run.once(this, function() {
2213
+ this.updateRecordArrays(type, clientId);
2214
+ });
2215
+ },
2216
+
2217
+ /**
2218
+ @private
2219
+
2220
+ This method is invoked whenever data is loaded into the store
2221
+ by the adapter or updated by the adapter, or when an attribute
2222
+ changes on a record.
2223
+
2224
+ It updates all filters that a record belongs to.
2225
+
2226
+ To avoid thrashing, it only runs once per run loop per record.
2227
+
2228
+ @param {Class} type
2229
+ @param {Number|String} clientId
2230
+ */
2231
+ updateRecordArrays: function(type, clientId) {
2232
+ var recordArrays = this.typeMapFor(type).recordArrays,
2233
+ filter;
2234
+
2235
+ recordArrays.forEach(function(array) {
2236
+ filter = get(array, 'filterFunction');
2237
+ this.updateRecordArray(array, filter, type, clientId);
2238
+ }, this);
2239
+
2240
+ // loop through all manyArrays containing an unloaded copy of this
2241
+ // clientId and notify them that the record was loaded.
2242
+ var manyArrays = this.loadingRecordArrays[clientId];
2243
+
2244
+ if (manyArrays) {
2245
+ for (var i=0, l=manyArrays.length; i<l; i++) {
2246
+ manyArrays[i].loadedRecord();
2247
+ }
2248
+
2249
+ this.loadingRecordArrays[clientId] = null;
2250
+ }
2251
+ },
2252
+
2253
+ /**
2254
+ @private
2255
+
2256
+ Update an individual filter.
2257
+
2258
+ @param {DS.FilteredRecordArray} array
2259
+ @param {Function} filter
2260
+ @param {Class} type
2261
+ @param {Number|String} clientId
2262
+ */
2263
+ updateRecordArray: function(array, filter, type, clientId) {
2264
+ var shouldBeInArray, record;
2265
+
2266
+ if (!filter) {
2267
+ shouldBeInArray = true;
2268
+ } else {
2269
+ record = this.findByClientId(type, clientId);
2270
+ shouldBeInArray = filter(record);
2271
+ }
2272
+
2273
+ var content = get(array, 'content');
2274
+ var alreadyInArray = content.indexOf(clientId) !== -1;
2275
+
2276
+ var recordArrays = this.recordArraysForClientId(clientId);
2277
+ var reference = this.referenceForClientId(clientId);
2278
+
2279
+ if (shouldBeInArray) {
2280
+ recordArrays.add(array);
2281
+ array.addReference(reference);
2282
+ } else if (!shouldBeInArray) {
2283
+ recordArrays.remove(array);
2284
+ array.removeReference(reference);
2285
+ }
2286
+ },
2287
+
2288
+ /**
2289
+ @private
2290
+
2291
+ When a record is deleted, it is removed from all its
2292
+ record arrays.
2293
+
2294
+ @param {DS.Model} record
2295
+ */
2296
+ removeFromRecordArrays: function(record) {
2297
+ var reference = get(record, '_reference');
2298
+ var recordArrays = this.recordArraysForClientId(reference.clientId);
2299
+
2300
+ recordArrays.forEach(function(array) {
2301
+ array.removeReference(reference);
2302
+ });
2303
+ },
2304
+
2305
+ // ............
2306
+ // . INDEXING .
2307
+ // ............
2308
+
2309
+ /**
2310
+ @private
2311
+
2312
+ Return a list of all `DS.RecordArray`s a clientId is
2313
+ part of.
2314
+
2315
+ @return {Object(clientId: Ember.OrderedSet)}
2316
+ */
2317
+ recordArraysForClientId: function(clientId) {
2318
+ var recordArrays = get(this, 'recordArraysByClientId');
2319
+ var ret = recordArrays[clientId];
2320
+
2321
+ if (!ret) {
2322
+ ret = recordArrays[clientId] = Ember.OrderedSet.create();
2323
+ }
2324
+
2325
+ return ret;
2326
+ },
2327
+
2328
+ typeMapFor: function(type) {
2329
+ var typeMaps = get(this, 'typeMaps');
2330
+ var guidForType = Ember.guidFor(type);
2331
+
2332
+ var typeMap = typeMaps[guidForType];
2333
+
2334
+ if (typeMap) {
2335
+ return typeMap;
2336
+ } else {
2337
+ return (typeMaps[guidForType] =
2338
+ {
2339
+ idToCid: {},
2340
+ clientIds: [],
2341
+ recordArrays: []
2342
+ });
2343
+ }
2344
+ },
2345
+
2346
+ /** @private
2347
+
2348
+ For a given type and id combination, returns the client id used by the store.
2349
+ If no client id has been assigned yet, one will be created and returned.
2350
+
2351
+ @param {DS.Model} type
2352
+ @param {String|Number} id
2353
+ */
2354
+ clientIdForId: function(type, id) {
2355
+ id = coerceId(id);
2356
+
2357
+ var clientId = this.typeMapFor(type).idToCid[id];
2358
+ if (clientId !== undefined) { return clientId; }
2359
+
2360
+ return this.pushData(UNLOADED, id, type);
2361
+ },
2362
+
2363
+ /**
2364
+ @private
2365
+
2366
+ This method works exactly like `clientIdForId`, but does not
2367
+ require looking up the `typeMap` for every `clientId` and
2368
+ invoking a method per `clientId`.
2369
+ */
2370
+ clientIdsForIds: function(type, ids) {
2371
+ var typeMap = this.typeMapFor(type),
2372
+ idToClientIdMap = typeMap.idToCid;
2373
+
2374
+ return map(ids, function(id) {
2375
+ id = coerceId(id);
2376
+
2377
+ var clientId = idToClientIdMap[id];
2378
+ if (clientId) { return clientId; }
2379
+ return this.pushData(UNLOADED, id, type);
2380
+ }, this);
2381
+ },
2382
+
2383
+ typeForClientId: function(clientId) {
2384
+ return this.clientIdToType[clientId];
2385
+ },
2386
+
2387
+ idForClientId: function(clientId) {
2388
+ return this.clientIdToId[clientId];
2389
+ },
2390
+
2391
+ // ................
2392
+ // . LOADING DATA .
2393
+ // ................
2394
+
2395
+ /**
2396
+ Load new data into the store for a given id and type combination.
2397
+ If data for that record had been loaded previously, the new information
2398
+ overwrites the old.
2399
+
2400
+ If the record you are loading data for has outstanding changes that have not
2401
+ yet been saved, an exception will be thrown.
2402
+
2403
+ @param {DS.Model} type
2404
+ @param {String|Number} id
2405
+ @param {Object} data the data to load
2406
+ */
2407
+ load: function(type, data, prematerialized) {
2408
+ var id;
2409
+
2410
+ if (typeof data === 'number' || typeof data === 'string') {
2411
+ id = data;
2412
+ data = prematerialized;
2413
+ prematerialized = null;
2414
+ }
2415
+
2416
+ if (prematerialized && prematerialized.id) {
2417
+ id = prematerialized.id;
2418
+ } else if (id === undefined) {
2419
+ var adapter = this.adapterForType(type);
2420
+ id = this.preprocessData(type, data);
2421
+ }
2422
+
2423
+ id = coerceId(id);
2424
+
2425
+ var typeMap = this.typeMapFor(type),
2426
+ cidToData = this.clientIdToData,
2427
+ clientId = typeMap.idToCid[id],
2428
+ cidToPrematerialized = this.clientIdToPrematerializedData;
2429
+
2430
+ if (clientId !== undefined) {
2431
+ cidToData[clientId] = data;
2432
+ cidToPrematerialized[clientId] = prematerialized;
2433
+
2434
+ var record = this.recordCache[clientId];
2435
+ if (record) {
2436
+ once(record, 'loadedData');
2437
+ }
2438
+ } else {
2439
+ clientId = this.pushData(data, id, type);
2440
+ cidToPrematerialized[clientId] = prematerialized;
2441
+ }
2442
+
2443
+ this.updateRecordArraysLater(type, clientId);
2444
+
2445
+ return this.referenceForClientId(clientId);
2446
+ },
2447
+
2448
+ prematerialize: function(reference, prematerialized) {
2449
+ this.clientIdToPrematerializedData[reference.clientId] = prematerialized;
2450
+ },
2451
+
2452
+ loadMany: function(type, ids, dataList) {
2453
+ if (dataList === undefined) {
2454
+ dataList = ids;
2455
+ ids = map(dataList, function(data) {
2456
+ return this.preprocessData(type, data);
2457
+ }, this);
2458
+ }
2459
+
2460
+ return map(ids, function(id, i) {
2461
+ return this.load(type, id, dataList[i]);
2462
+ }, this);
2463
+ },