ember-rails 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3744 @@
1
+ (function() {
2
+ window.DS = Ember.Namespace.create({
3
+ CURRENT_API_REVISION: 4
4
+ });
5
+
6
+ })();
7
+
8
+
9
+
10
+ (function() {
11
+ var get = Ember.get, set = Ember.set;
12
+
13
+ /**
14
+ A record array is an array that contains records of a certain type. The record
15
+ array materializes records as needed when they are retrieved for the first
16
+ time. You should not create record arrays yourself. Instead, an instance of
17
+ DS.RecordArray or its subclasses will be returned by your application's store
18
+ in response to queries.
19
+ */
20
+
21
+ DS.RecordArray = Ember.ArrayProxy.extend({
22
+
23
+ /**
24
+ The model type contained by this record array.
25
+
26
+ @type DS.Model
27
+ */
28
+ type: null,
29
+
30
+ // The array of client ids backing the record array. When a
31
+ // record is requested from the record array, the record
32
+ // for the client id at the same index is materialized, if
33
+ // necessary, by the store.
34
+ content: null,
35
+
36
+ // The store that created this record array.
37
+ store: null,
38
+
39
+ init: function() {
40
+ set(this, 'recordCache', Ember.A([]));
41
+ this._super();
42
+ },
43
+
44
+ arrayDidChange: function(array, index, removed, added) {
45
+ var recordCache = get(this, 'recordCache');
46
+ recordCache.replace(index, 0, new Array(added));
47
+
48
+ this._super(array, index, removed, added);
49
+ },
50
+
51
+ arrayWillChange: function(array, index, removed, added) {
52
+ this._super(array, index, removed, added);
53
+
54
+ var recordCache = get(this, 'recordCache');
55
+ recordCache.replace(index, removed);
56
+ },
57
+
58
+ objectAtContent: function(index) {
59
+ var recordCache = get(this, 'recordCache');
60
+ var record = recordCache.objectAt(index);
61
+
62
+ if (!record) {
63
+ var store = get(this, 'store');
64
+ var content = get(this, 'content');
65
+
66
+ var contentObject = content.objectAt(index);
67
+
68
+ if (contentObject !== undefined) {
69
+ record = store.findByClientId(get(this, 'type'), contentObject);
70
+ recordCache.replace(index, 1, [record]);
71
+ }
72
+ }
73
+
74
+ return record;
75
+ }
76
+ });
77
+
78
+ })();
79
+
80
+
81
+
82
+ (function() {
83
+ var get = Ember.get;
84
+
85
+ DS.FilteredRecordArray = DS.RecordArray.extend({
86
+ filterFunction: null,
87
+
88
+ replace: function() {
89
+ var type = get(this, 'type').toString();
90
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
91
+ },
92
+
93
+ updateFilter: Ember.observer(function() {
94
+ var store = get(this, 'store');
95
+ store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
96
+ }, 'filterFunction')
97
+ });
98
+
99
+ })();
100
+
101
+
102
+
103
+ (function() {
104
+ var get = Ember.get, set = Ember.set;
105
+
106
+ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
107
+ query: null,
108
+ isLoaded: false,
109
+
110
+ replace: function() {
111
+ var type = get(this, 'type').toString();
112
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
113
+ },
114
+
115
+ load: function(array) {
116
+ var store = get(this, 'store'), type = get(this, 'type');
117
+
118
+ var clientIds = store.loadMany(type, array).clientIds;
119
+
120
+ this.beginPropertyChanges();
121
+ set(this, 'content', Ember.A(clientIds));
122
+ set(this, 'isLoaded', true);
123
+ this.endPropertyChanges();
124
+ }
125
+ });
126
+
127
+
128
+ })();
129
+
130
+
131
+
132
+ (function() {
133
+ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
134
+
135
+ var Set = function() {
136
+ this.hash = {};
137
+ this.list = [];
138
+ };
139
+
140
+ Set.prototype = {
141
+ add: function(item) {
142
+ var hash = this.hash,
143
+ guid = guidFor(item);
144
+
145
+ if (hash.hasOwnProperty(guid)) { return; }
146
+
147
+ hash[guid] = true;
148
+ this.list.push(item);
149
+ },
150
+
151
+ remove: function(item) {
152
+ var hash = this.hash,
153
+ guid = guidFor(item);
154
+
155
+ if (!hash.hasOwnProperty(guid)) { return; }
156
+
157
+ delete hash[guid];
158
+ var list = this.list,
159
+ index = Ember.ArrayUtils.indexOf(this, item);
160
+
161
+ list.splice(index, 1);
162
+ },
163
+
164
+ isEmpty: function() {
165
+ return this.list.length === 0;
166
+ }
167
+ };
168
+
169
+ var ManyArrayState = Ember.State.extend({
170
+ recordWasAdded: function(manager, record) {
171
+ var dirty = manager.dirty, observer;
172
+ dirty.add(record);
173
+
174
+ observer = function() {
175
+ if (!get(record, 'isDirty')) {
176
+ record.removeObserver('isDirty', observer);
177
+ manager.send('childWasSaved', record);
178
+ }
179
+ };
180
+
181
+ record.addObserver('isDirty', observer);
182
+ },
183
+
184
+ recordWasRemoved: function(manager, record) {
185
+ var dirty = manager.dirty, observer;
186
+ dirty.add(record);
187
+
188
+ observer = function() {
189
+ record.removeObserver('isDirty', observer);
190
+ if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); }
191
+ };
192
+
193
+ record.addObserver('isDirty', observer);
194
+ }
195
+ });
196
+
197
+ var states = {
198
+ clean: ManyArrayState.create({
199
+ isDirty: false,
200
+
201
+ recordWasAdded: function(manager, record) {
202
+ this._super(manager, record);
203
+ manager.goToState('dirty');
204
+ },
205
+
206
+ update: function(manager, clientIds) {
207
+ var manyArray = manager.manyArray;
208
+ set(manyArray, 'content', clientIds);
209
+ }
210
+ }),
211
+
212
+ dirty: ManyArrayState.create({
213
+ isDirty: true,
214
+
215
+ childWasSaved: function(manager, child) {
216
+ var dirty = manager.dirty;
217
+ dirty.remove(child);
218
+
219
+ if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); }
220
+ },
221
+
222
+ arrayBecameSaved: function(manager) {
223
+ manager.goToState('clean');
224
+ }
225
+ })
226
+ };
227
+
228
+ DS.ManyArrayStateManager = Ember.StateManager.extend({
229
+ manyArray: null,
230
+ initialState: 'clean',
231
+ states: states,
232
+
233
+ init: function() {
234
+ this._super();
235
+ this.dirty = new Set();
236
+ }
237
+ });
238
+
239
+ })();
240
+
241
+
242
+
243
+ (function() {
244
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
245
+
246
+ DS.ManyArray = DS.RecordArray.extend({
247
+ init: function() {
248
+ set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this }));
249
+
250
+ return this._super();
251
+ },
252
+
253
+ parentRecord: null,
254
+
255
+ isDirty: Ember.computed(function() {
256
+ return getPath(this, 'stateManager.currentState.isDirty');
257
+ }).property('stateManager.currentState').cacheable(),
258
+
259
+ fetch: function() {
260
+ var clientIds = get(this, 'content'),
261
+ store = get(this, 'store'),
262
+ type = get(this, 'type');
263
+
264
+ var ids = clientIds.map(function(clientId) {
265
+ return store.clientIdToId[clientId];
266
+ });
267
+
268
+ store.fetchMany(type, ids);
269
+ },
270
+
271
+ // Overrides Ember.Array's replace method to implement
272
+ replace: function(index, removed, added) {
273
+ var parentRecord = get(this, 'parentRecord');
274
+ var pendingParent = parentRecord && !get(parentRecord, 'id');
275
+ var stateManager = get(this, 'stateManager');
276
+
277
+ // Map the array of record objects into an array of client ids.
278
+ added = added.map(function(record) {
279
+
280
+
281
+ // If the record to which this many array belongs does not yet
282
+ // have an id, notify the newly-added record that it must wait
283
+ // for the parent to receive an id before the child can be
284
+ // saved.
285
+ if (pendingParent) {
286
+ record.send('waitingOn', parentRecord);
287
+ }
288
+
289
+ this.assignInverse(record, parentRecord);
290
+
291
+ stateManager.send('recordWasAdded', record);
292
+
293
+ return record.get('clientId');
294
+ }, this);
295
+
296
+ var store = this.store;
297
+
298
+ var len = index+removed, record;
299
+ for (var i = index; i < len; i++) {
300
+ // TODO: null out inverse FK
301
+ record = this.objectAt(i);
302
+ this.assignInverse(record, parentRecord, true);
303
+
304
+ // If we put the child record into a pending state because
305
+ // we were waiting on the parent record to get an id, we
306
+ // can tell the child it no longer needs to wait.
307
+ if (pendingParent) {
308
+ record.send('doneWaitingOn', parentRecord);
309
+ }
310
+
311
+ stateManager.send('recordWasAdded', record);
312
+ }
313
+
314
+ this._super(index, removed, added);
315
+ },
316
+
317
+ assignInverse: function(record, parentRecord, remove) {
318
+ var associationMap = get(record.constructor, 'associations'),
319
+ possibleAssociations = associationMap.get(parentRecord.constructor),
320
+ possible, actual;
321
+
322
+ if (!possibleAssociations) { return; }
323
+
324
+ for (var i = 0, l = possibleAssociations.length; i < l; i++) {
325
+ possible = possibleAssociations[i];
326
+
327
+ if (possible.kind === 'belongsTo') {
328
+ actual = possible;
329
+ break;
330
+ }
331
+ }
332
+
333
+ if (actual) {
334
+ set(record, actual.name, remove ? null : parentRecord);
335
+ }
336
+ },
337
+
338
+ // Create a child record within the parentRecord
339
+ createRecord: function(hash, transaction) {
340
+ var parentRecord = get(this, 'parentRecord'),
341
+ store = get(parentRecord, 'store'),
342
+ type = get(this, 'type'),
343
+ record;
344
+
345
+ transaction = transaction || get(parentRecord, 'transaction');
346
+
347
+ record = store.createRecord.call(store, type, hash, transaction);
348
+ this.pushObject(record);
349
+
350
+ return record;
351
+ }
352
+ });
353
+
354
+ })();
355
+
356
+
357
+
358
+ (function() {
359
+
360
+ })();
361
+
362
+
363
+
364
+ (function() {
365
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
366
+
367
+ /**
368
+ A transaction allows you to collect multiple records into a unit of work
369
+ that can be committed or rolled back as a group.
370
+
371
+ For example, if a record has local modifications that have not yet
372
+ been saved, calling `commit()` on its transaction will cause those
373
+ modifications to be sent to the adapter to be saved. Calling
374
+ `rollback()` on its transaction would cause all of the modifications to
375
+ be discarded and the record to return to the last known state before
376
+ changes were made.
377
+
378
+ If a newly created record's transaction is rolled back, it will
379
+ immediately transition to the deleted state.
380
+
381
+ If you do not explicitly create a transaction, a record is assigned to
382
+ an implicit transaction called the default transaction. In these cases,
383
+ you can treat your application's instance of `DS.Store` as a transaction
384
+ and call the `commit()` and `rollback()` methods on the store itself.
385
+
386
+ Once a record has been successfully committed or rolled back, it will
387
+ be moved back to the implicit transaction. Because it will now be in
388
+ a clean state, it can be moved to a new transaction if you wish.
389
+
390
+ ### Creating a Transaction
391
+
392
+ To create a new transaction, call the `transaction()` method of your
393
+ application's `DS.Store` instance:
394
+
395
+ var transaction = App.store.transaction();
396
+
397
+ This will return a new instance of `DS.Transaction` with no records
398
+ yet assigned to it.
399
+
400
+ ### Adding Existing Records
401
+
402
+ Add records to a transaction using the `add()` method:
403
+
404
+ record = App.store.find(Person, 1);
405
+ transaction.add(record);
406
+
407
+ Note that only records whose `isDirty` flag is `false` may be added
408
+ to a transaction. Once modifications to a record have been made
409
+ (its `isDirty` flag is `true`), it is not longer able to be added to
410
+ a transaction.
411
+
412
+ ### Creating New Records
413
+
414
+ Because newly created records are dirty from the time they are created,
415
+ and because dirty records can not be added to a transaction, you must
416
+ use the `createRecord()` method to assign new records to a transaction.
417
+
418
+ For example, instead of this:
419
+
420
+ var transaction = store.transaction();
421
+ var person = Person.createRecord({ name: "Steve" });
422
+
423
+ // won't work because person is dirty
424
+ transaction.add(person);
425
+
426
+ Call `createRecord()` on the transaction directly:
427
+
428
+ var transaction = store.transaction();
429
+ transaction.createRecord(Person, { name: "Steve" });
430
+
431
+ ### Asynchronous Commits
432
+
433
+ Typically, all of the records in a transaction will be committed
434
+ together. However, new records that have a dependency on other new
435
+ records need to wait for their parent record to be saved and assigned an
436
+ ID. In that case, the child record will continue to live in the
437
+ transaction until its parent is saved, at which time the transaction will
438
+ attempt to commit again.
439
+
440
+ For this reason, you should not re-use transactions once you have committed
441
+ them. Always make a new transaction and move the desired records to it before
442
+ calling commit.
443
+ */
444
+
445
+ DS.Transaction = Ember.Object.extend({
446
+ /**
447
+ @private
448
+
449
+ Creates the bucket data structure used to segregate records by
450
+ type.
451
+ */
452
+ init: function() {
453
+ set(this, 'buckets', {
454
+ clean: Ember.Map.create(),
455
+ created: Ember.Map.create(),
456
+ updated: Ember.Map.create(),
457
+ deleted: Ember.Map.create(),
458
+ inflight: Ember.Map.create()
459
+ });
460
+ },
461
+
462
+ /**
463
+ Creates a new record of the given type and assigns it to the transaction
464
+ on which the method was called.
465
+
466
+ This is useful as only clean records can be added to a transaction and
467
+ new records created using other methods immediately become dirty.
468
+
469
+ @param {DS.Model} type the model type to create
470
+ @param {Object} hash the data hash to assign the new record
471
+ */
472
+ createRecord: function(type, hash) {
473
+ var store = get(this, 'store');
474
+
475
+ return store.createRecord(type, hash, this);
476
+ },
477
+
478
+ /**
479
+ Adds an existing record to this transaction. Only records without
480
+ modficiations (i.e., records whose `isDirty` property is `false`)
481
+ can be added to a transaction.
482
+
483
+ @param {DS.Model} record the record to add to the transaction
484
+ */
485
+ add: function(record) {
486
+ // we could probably make this work if someone has a valid use case. Do you?
487
+
488
+
489
+ var recordTransaction = get(record, 'transaction'),
490
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
491
+
492
+
493
+ this.adoptRecord(record);
494
+ },
495
+
496
+ /**
497
+ Commits the transaction, which causes all of the modified records that
498
+ belong to the transaction to be sent to the adapter to be saved.
499
+
500
+ Once you call `commit()` on a transaction, you should not re-use it.
501
+
502
+ When a record is saved, it will be removed from this transaction and
503
+ moved back to the store's default transaction.
504
+ */
505
+ commit: function() {
506
+ var self = this,
507
+ iterate;
508
+
509
+ iterate = function(bucketType, fn, binding) {
510
+ var dirty = self.bucketForType(bucketType);
511
+
512
+ dirty.forEach(function(type, records) {
513
+ if (records.isEmpty()) { return; }
514
+
515
+ var array = [];
516
+
517
+ records.forEach(function(record) {
518
+ record.send('willCommit');
519
+
520
+ if (get(record, 'isPending') === false) {
521
+ array.push(record);
522
+ }
523
+ });
524
+
525
+ fn.call(binding, type, array);
526
+ });
527
+ };
528
+
529
+ var commitDetails = {
530
+ updated: {
531
+ eachType: function(fn, binding) { iterate('updated', fn, binding); }
532
+ },
533
+
534
+ created: {
535
+ eachType: function(fn, binding) { iterate('created', fn, binding); }
536
+ },
537
+
538
+ deleted: {
539
+ eachType: function(fn, binding) { iterate('deleted', fn, binding); }
540
+ }
541
+ };
542
+
543
+ var store = get(this, 'store');
544
+ var adapter = get(store, '_adapter');
545
+
546
+ this.removeCleanRecords();
547
+
548
+ if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
549
+ else { throw fmt("Adapter is either null or does not implement `commit` method", this); }
550
+ },
551
+
552
+ /**
553
+ Rolling back a transaction resets the records that belong to
554
+ that transaction.
555
+
556
+ Updated records have their properties reset to the last known
557
+ value from the persistence layer. Deleted records are reverted
558
+ to a clean, non-deleted state. Newly created records immediately
559
+ become deleted, and are not sent to the adapter to be persisted.
560
+
561
+ After the transaction is rolled back, any records that belong
562
+ to it will return to the store's default transaction, and the
563
+ current transaction should not be used again.
564
+ */
565
+ rollback: function() {
566
+ var store = get(this, 'store'),
567
+ dirty;
568
+
569
+ // Loop through all of the records in each of the dirty states
570
+ // and initiate a rollback on them. As a side effect of telling
571
+ // the record to roll back, it should also move itself out of
572
+ // the dirty bucket and into the clean bucket.
573
+ ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) {
574
+ dirty = this.bucketForType(bucketType);
575
+
576
+ dirty.forEach(function(type, records) {
577
+ records.forEach(function(record) {
578
+ record.send('rollback');
579
+ });
580
+ });
581
+ }, this);
582
+
583
+ // Now that all records in the transaction are guaranteed to be
584
+ // clean, migrate them all to the store's default transaction.
585
+ this.removeCleanRecords();
586
+ },
587
+
588
+ /**
589
+ @private
590
+
591
+ Removes a record from this transaction and back to the store's
592
+ default transaction.
593
+
594
+ Note: This method is private for now, but should probably be exposed
595
+ in the future once we have stricter error checking (for example, in the
596
+ case of the record being dirty).
597
+
598
+ @param {DS.Model} record
599
+ */
600
+ remove: function(record) {
601
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
602
+ defaultTransaction.adoptRecord(record);
603
+ },
604
+
605
+ /**
606
+ @private
607
+
608
+ Removes all of the records in the transaction's clean bucket.
609
+ */
610
+ removeCleanRecords: function() {
611
+ var clean = this.bucketForType('clean'),
612
+ self = this;
613
+
614
+ clean.forEach(function(type, records) {
615
+ records.forEach(function(record) {
616
+ self.remove(record);
617
+ });
618
+ });
619
+ },
620
+
621
+ /**
622
+ @private
623
+
624
+ Returns the bucket for the given bucket type. For example, you might call
625
+ `this.bucketForType('updated')` to get the `Ember.Map` that contains all
626
+ of the records that have changes pending.
627
+
628
+ @param {String} bucketType the type of bucket
629
+ @returns Ember.Map
630
+ */
631
+ bucketForType: function(bucketType) {
632
+ var buckets = get(this, 'buckets');
633
+
634
+ return get(buckets, bucketType);
635
+ },
636
+
637
+ /**
638
+ @private
639
+
640
+ This method moves a record into a different transaction without the normal
641
+ checks that ensure that the user is not doing something weird, like moving
642
+ a dirty record into a new transaction.
643
+
644
+ It is designed for internal use, such as when we are moving a clean record
645
+ into a new transaction when the transaction is committed.
646
+
647
+ This method must not be called unless the record is clean.
648
+
649
+ @param {DS.Model} record
650
+ */
651
+ adoptRecord: function(record) {
652
+ var oldTransaction = get(record, 'transaction');
653
+
654
+ if (oldTransaction) {
655
+ oldTransaction.removeFromBucket('clean', record);
656
+ }
657
+
658
+ this.addToBucket('clean', record);
659
+ set(record, 'transaction', this);
660
+ },
661
+
662
+ /**
663
+ @private
664
+
665
+ Adds a record to the named bucket.
666
+
667
+ @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
668
+ */
669
+ addToBucket: function(bucketType, record) {
670
+ var bucket = this.bucketForType(bucketType),
671
+ type = record.constructor;
672
+
673
+ var records = bucket.get(type);
674
+
675
+ if (!records) {
676
+ records = Ember.OrderedSet.create();
677
+ bucket.set(type, records);
678
+ }
679
+
680
+ records.add(record);
681
+ },
682
+
683
+ /**
684
+ @private
685
+
686
+ Removes a record from the named bucket.
687
+
688
+ @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
689
+ */
690
+ removeFromBucket: function(bucketType, record) {
691
+ var bucket = this.bucketForType(bucketType),
692
+ type = record.constructor;
693
+
694
+ var records = bucket.get(type);
695
+ records.remove(record);
696
+ },
697
+
698
+ /**
699
+ @private
700
+
701
+ Called by a record's state manager to indicate that the record has entered
702
+ a dirty state. The record will be moved from the `clean` bucket and into
703
+ the appropriate dirty bucket.
704
+
705
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
706
+ */
707
+ recordBecameDirty: function(bucketType, record) {
708
+ this.removeFromBucket('clean', record);
709
+ this.addToBucket(bucketType, record);
710
+ },
711
+
712
+ /**
713
+ @private
714
+
715
+ Called by a record's state manager to indicate that the record has entered
716
+ inflight state. The record will be moved from its current dirty bucket and into
717
+ the `inflight` bucket.
718
+
719
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
720
+ */
721
+ recordBecameInFlight: function(kind, record) {
722
+ this.removeFromBucket(kind, record);
723
+ this.addToBucket('inflight', record);
724
+ },
725
+
726
+ /**
727
+ @private
728
+
729
+ Called by a record's state manager to indicate that the record has entered
730
+ a clean state. The record will be moved from its current dirty or inflight bucket and into
731
+ the `clean` bucket.
732
+
733
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
734
+ */
735
+ recordBecameClean: function(kind, record) {
736
+ this.removeFromBucket(kind, record);
737
+
738
+ this.remove(record);
739
+ }
740
+ });
741
+
742
+ })();
743
+
744
+
745
+
746
+ (function() {
747
+ /*globals Ember*/
748
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
749
+
750
+ var DATA_PROXY = {
751
+ get: function(name) {
752
+ return this.savedData[name];
753
+ }
754
+ };
755
+
756
+ // These values are used in the data cache when clientIds are
757
+ // needed but the underlying data has not yet been loaded by
758
+ // the server.
759
+ var UNLOADED = 'unloaded';
760
+ var LOADING = 'loading';
761
+
762
+ // Implementors Note:
763
+ //
764
+ // The variables in this file are consistently named according to the following
765
+ // scheme:
766
+ //
767
+ // * +id+ means an identifier managed by an external source, provided inside the
768
+ // data hash provided by that source.
769
+ // * +clientId+ means a transient numerical identifier generated at runtime by
770
+ // the data store. It is important primarily because newly created objects may
771
+ // not yet have an externally generated id.
772
+ // * +type+ means a subclass of DS.Model.
773
+
774
+ /**
775
+ The store contains all of the hashes for records loaded from the server.
776
+ It is also responsible for creating instances of DS.Model when you request one
777
+ of these data hashes, so that they can be bound to in your Handlebars templates.
778
+
779
+ Create a new store like this:
780
+
781
+ MyApp.store = DS.Store.create();
782
+
783
+ You can retrieve DS.Model instances from the store in several ways. To retrieve
784
+ a record for a specific id, use the `find()` method:
785
+
786
+ var record = MyApp.store.find(MyApp.Contact, 123);
787
+
788
+ By default, the store will talk to your backend using a standard REST mechanism.
789
+ You can customize how the store talks to your backend by specifying a custom adapter:
790
+
791
+ MyApp.store = DS.Store.create({
792
+ adapter: 'MyApp.CustomAdapter'
793
+ });
794
+
795
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
796
+ documentation.
797
+ */
798
+ DS.Store = Ember.Object.extend({
799
+
800
+ /**
801
+ Many methods can be invoked without specifying which store should be used.
802
+ In those cases, the first store created will be used as the default. If
803
+ an application has multiple stores, it should specify which store to use
804
+ when performing actions, such as finding records by id.
805
+
806
+ The init method registers this store as the default if none is specified.
807
+ */
808
+ init: function() {
809
+ // Enforce API revisioning. See BREAKING_CHANGES.md for more.
810
+ var revision = get(this, 'revision');
811
+
812
+ if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
813
+ 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);
814
+ }
815
+
816
+ if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
817
+ set(DS, 'defaultStore', this);
818
+ }
819
+
820
+ // internal bookkeeping; not observable
821
+ this.typeMaps = {};
822
+ this.recordCache = [];
823
+ this.clientIdToId = {};
824
+ this.recordArraysByClientId = {};
825
+
826
+ set(this, 'defaultTransaction', this.transaction());
827
+
828
+ return this._super();
829
+ },
830
+
831
+ /**
832
+ Returns a new transaction scoped to this store.
833
+
834
+ @see {DS.Transaction}
835
+ @returns DS.Transaction
836
+ */
837
+ transaction: function() {
838
+ return DS.Transaction.create({ store: this });
839
+ },
840
+
841
+ /**
842
+ @private
843
+
844
+ This is used only by the record's DataProxy. Do not use this directly.
845
+ */
846
+ dataForRecord: function(record) {
847
+ var type = record.constructor,
848
+ clientId = get(record, 'clientId'),
849
+ typeMap = this.typeMapFor(type);
850
+
851
+ return typeMap.cidToHash[clientId];
852
+ },
853
+
854
+ /**
855
+ The adapter to use to communicate to a backend server or other persistence layer.
856
+
857
+ This can be specified as an instance, a class, or a property path that specifies
858
+ where the adapter can be located.
859
+
860
+ @property {DS.Adapter|String}
861
+ */
862
+ adapter: null,
863
+
864
+ /**
865
+ @private
866
+
867
+ This property returns the adapter, after resolving a possible String.
868
+
869
+ @returns DS.Adapter
870
+ */
871
+ _adapter: Ember.computed(function() {
872
+ var adapter = get(this, 'adapter');
873
+ if (typeof adapter === 'string') {
874
+ return getPath(this, adapter, false) || getPath(window, adapter);
875
+ }
876
+ return adapter;
877
+ }).property('adapter').cacheable(),
878
+
879
+ // A monotonically increasing number to be used to uniquely identify
880
+ // data hashes and records.
881
+ clientIdCounter: 1,
882
+
883
+ // .....................
884
+ // . CREATE NEW RECORD .
885
+ // .....................
886
+
887
+ /**
888
+ Create a new record in the current store. The properties passed
889
+ to this method are set on the newly created record.
890
+
891
+ @param {subclass of DS.Model} type
892
+ @param {Object} properties a hash of properties to set on the
893
+ newly created record.
894
+ @returns DS.Model
895
+ */
896
+ createRecord: function(type, properties, transaction) {
897
+ properties = properties || {};
898
+
899
+ // Create a new instance of the model `type` and put it
900
+ // into the specified `transaction`. If no transaction is
901
+ // specified, the default transaction will be used.
902
+ //
903
+ // NOTE: A `transaction` is specified when the
904
+ // `transaction.createRecord` API is used.
905
+ var record = type._create({
906
+ store: this
907
+ });
908
+
909
+ transaction = transaction || get(this, 'defaultTransaction');
910
+ transaction.adoptRecord(record);
911
+
912
+ // Extract the primary key from the `properties` hash,
913
+ // based on the `primaryKey` for the model type.
914
+ var primaryKey = get(record, 'primaryKey'),
915
+ id = properties[primaryKey] || null;
916
+
917
+ // If the passed properties do not include a primary key,
918
+ // give the adapter an opportunity to generate one.
919
+ var adapter;
920
+ if (Ember.none(id)) {
921
+ adapter = get(this, 'adapter');
922
+ if (adapter && adapter.generateIdForRecord) {
923
+ id = adapter.generateIdForRecord(this, record);
924
+ properties.id = id;
925
+ }
926
+ }
927
+
928
+ var hash = {}, clientId;
929
+
930
+ // Push the hash into the store. If present, associate the
931
+ // extracted `id` with the hash.
932
+ clientId = this.pushHash(hash, id, type);
933
+
934
+ record.send('didChangeData');
935
+
936
+ var recordCache = get(this, 'recordCache');
937
+
938
+ // Now that we have a clientId, attach it to the record we
939
+ // just created.
940
+ set(record, 'clientId', clientId);
941
+
942
+ // Store the record we just created in the record cache for
943
+ // this clientId.
944
+ recordCache[clientId] = record;
945
+
946
+ // Set the properties specified on the record.
947
+ record.setProperties(properties);
948
+
949
+ this.updateRecordArrays(type, clientId, get(record, 'data'));
950
+
951
+ return record;
952
+ },
953
+
954
+ // .................
955
+ // . DELETE RECORD .
956
+ // .................
957
+
958
+ /**
959
+ For symmetry, a record can be deleted via the store.
960
+
961
+ @param {DS.Model} record
962
+ */
963
+ deleteRecord: function(record) {
964
+ record.send('deleteRecord');
965
+ },
966
+
967
+ // ................
968
+ // . FIND RECORDS .
969
+ // ................
970
+
971
+ /**
972
+ This is the main entry point into finding records. The first
973
+ parameter to this method is always a subclass of `DS.Model`.
974
+
975
+ You can use the `find` method on a subclass of `DS.Model`
976
+ directly if your application only has one store. For
977
+ example, instead of `store.find(App.Person, 1)`, you could
978
+ say `App.Person.find(1)`.
979
+
980
+ ---
981
+
982
+ To find a record by ID, pass the `id` as the second parameter:
983
+
984
+ store.find(App.Person, 1);
985
+ App.Person.find(1);
986
+
987
+ If the record with that `id` had not previously been loaded,
988
+ the store will return an empty record immediately and ask
989
+ the adapter to find the data by calling the adapter's `find`
990
+ method.
991
+
992
+ The `find` method will always return the same object for a
993
+ given type and `id`. To check whether the adapter has populated
994
+ a record, you can check its `isLoaded` property.
995
+
996
+ ---
997
+
998
+ To find all records for a type, call `find` with no additional
999
+ parameters:
1000
+
1001
+ store.find(App.Person);
1002
+ App.Person.find();
1003
+
1004
+ This will return a `RecordArray` representing all known records
1005
+ for the given type and kick off a request to the adapter's
1006
+ `findAll` method to load any additional records for the type.
1007
+
1008
+ The `RecordArray` returned by `find()` is live. If any more
1009
+ records for the type are added at a later time through any
1010
+ mechanism, it will automatically update to reflect the change.
1011
+
1012
+ ---
1013
+
1014
+ To find a record by a query, call `find` with a hash as the
1015
+ second parameter:
1016
+
1017
+ store.find(App.Person, { page: 1 });
1018
+ App.Person.find({ page: 1 });
1019
+
1020
+ This will return a `RecordArray` immediately, but it will always
1021
+ be an empty `RecordArray` at first. It will call the adapter's
1022
+ `findQuery` method, which will populate the `RecordArray` once
1023
+ the server has returned results.
1024
+
1025
+ You can check whether a query results `RecordArray` has loaded
1026
+ by checking its `isLoaded` property.
1027
+ */
1028
+ find: function(type, id, query) {
1029
+ if (id === undefined) {
1030
+ return this.findAll(type);
1031
+ }
1032
+
1033
+ if (query !== undefined) {
1034
+ return this.findMany(type, id, query);
1035
+ } else if (Ember.typeOf(id) === 'object') {
1036
+ return this.findQuery(type, id);
1037
+ }
1038
+
1039
+ if (Ember.isArray(id)) {
1040
+ return this.findMany(type, id);
1041
+ }
1042
+
1043
+ var clientId = this.typeMapFor(type).idToCid[id];
1044
+
1045
+ return this.findByClientId(type, clientId, id);
1046
+ },
1047
+
1048
+ findByClientId: function(type, clientId, id) {
1049
+ var recordCache = get(this, 'recordCache'),
1050
+ dataCache = this.typeMapFor(type).cidToHash,
1051
+ record;
1052
+
1053
+ // If there is already a clientId assigned for this
1054
+ // type/id combination, try to find an existing
1055
+ // record for that id and return. Otherwise,
1056
+ // materialize a new record and set its data to the
1057
+ // value we already have.
1058
+ if (clientId !== undefined) {
1059
+ record = recordCache[clientId];
1060
+
1061
+ if (!record) {
1062
+ // create a new instance of the model type in the
1063
+ // 'isLoading' state
1064
+ record = this.materializeRecord(type, clientId);
1065
+
1066
+ if (typeof dataCache[clientId] === 'object') {
1067
+ record.send('didChangeData');
1068
+ }
1069
+ }
1070
+ } else {
1071
+ clientId = this.pushHash(LOADING, id, type);
1072
+
1073
+ // create a new instance of the model type in the
1074
+ // 'isLoading' state
1075
+ record = this.materializeRecord(type, clientId);
1076
+
1077
+ // let the adapter set the data, possibly async
1078
+ var adapter = get(this, '_adapter');
1079
+ if (adapter && adapter.find) { adapter.find(this, type, id); }
1080
+ else { throw fmt("Adapter is either null or does not implement `find` method", this); }
1081
+ }
1082
+
1083
+ return record;
1084
+ },
1085
+
1086
+ /**
1087
+ @private
1088
+
1089
+ Ask the adapter to fetch IDs that are not already loaded.
1090
+
1091
+ This method will convert `id`s to `clientId`s, filter out
1092
+ `clientId`s that already have a data hash present, and pass
1093
+ the remaining `id`s to the adapter.
1094
+
1095
+ @param {Class} type A model class
1096
+ @param {Array} ids An array of ids
1097
+ @param {Object} query
1098
+
1099
+ @returns {Array} An Array of all clientIds for the
1100
+ specified ids.
1101
+ */
1102
+ fetchMany: function(type, ids, query) {
1103
+ var typeMap = this.typeMapFor(type),
1104
+ idToClientIdMap = typeMap.idToCid,
1105
+ dataCache = typeMap.cidToHash,
1106
+ data = typeMap.cidToHash,
1107
+ needed;
1108
+
1109
+ var clientIds = Ember.A([]);
1110
+
1111
+ if (ids) {
1112
+ needed = [];
1113
+
1114
+ ids.forEach(function(id) {
1115
+ // Get the clientId for the given id
1116
+ var clientId = idToClientIdMap[id];
1117
+
1118
+ // If there is no `clientId` yet
1119
+ if (clientId === undefined) {
1120
+ // Create a new `clientId`, marking its data hash
1121
+ // as loading. Once the adapter returns the data
1122
+ // hash, it will be updated
1123
+ clientId = this.pushHash(LOADING, id, type);
1124
+ needed.push(id);
1125
+
1126
+ // If there is a clientId, but its data hash is
1127
+ // marked as unloaded (this happens when a
1128
+ // hasMany association creates clientIds for its
1129
+ // referenced ids before they were loaded)
1130
+ } else if (clientId && data[clientId] === UNLOADED) {
1131
+ // change the data hash marker to loading
1132
+ dataCache[clientId] = LOADING;
1133
+ needed.push(id);
1134
+ }
1135
+
1136
+ // this method is expected to return a list of
1137
+ // all of the clientIds for the specified ids,
1138
+ // unconditionally add it.
1139
+ clientIds.push(clientId);
1140
+ }, this);
1141
+ } else {
1142
+ needed = null;
1143
+ }
1144
+
1145
+ // If there are any needed ids, ask the adapter to load them
1146
+ if ((needed && get(needed, 'length') > 0) || query) {
1147
+ var adapter = get(this, '_adapter');
1148
+ if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
1149
+ else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
1150
+ }
1151
+
1152
+ return clientIds;
1153
+ },
1154
+
1155
+ /** @private
1156
+ */
1157
+ findMany: function(type, ids, query) {
1158
+ var clientIds = this.fetchMany(type, ids, query);
1159
+
1160
+ return this.createManyArray(type, clientIds);
1161
+ },
1162
+
1163
+ findQuery: function(type, query) {
1164
+ var array = DS.AdapterPopulatedRecordArray.create({ type: type, content: Ember.A([]), store: this });
1165
+ var adapter = get(this, '_adapter');
1166
+ if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
1167
+ else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); }
1168
+ return array;
1169
+ },
1170
+
1171
+ findAll: function(type) {
1172
+
1173
+ var typeMap = this.typeMapFor(type),
1174
+ findAllCache = typeMap.findAllCache;
1175
+
1176
+ if (findAllCache) { return findAllCache; }
1177
+
1178
+ var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this });
1179
+ this.registerRecordArray(array, type);
1180
+
1181
+ var adapter = get(this, '_adapter');
1182
+ if (adapter && adapter.findAll) { adapter.findAll(this, type); }
1183
+
1184
+ typeMap.findAllCache = array;
1185
+ return array;
1186
+ },
1187
+
1188
+ filter: function(type, query, filter) {
1189
+ // allow an optional server query
1190
+ if (arguments.length === 3) {
1191
+ this.findQuery(type, query);
1192
+ } else if (arguments.length === 2) {
1193
+ filter = query;
1194
+ }
1195
+
1196
+ var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
1197
+
1198
+ this.registerRecordArray(array, type, filter);
1199
+
1200
+ return array;
1201
+ },
1202
+
1203
+ // ............
1204
+ // . UPDATING .
1205
+ // ............
1206
+
1207
+ hashWasUpdated: function(type, clientId, record) {
1208
+ // Because hash updates are invoked at the end of the run loop,
1209
+ // it is possible that a record might be deleted after its hash
1210
+ // has been modified and this method was scheduled to be called.
1211
+ //
1212
+ // If that's the case, the record would have already been removed
1213
+ // from all record arrays; calling updateRecordArrays would just
1214
+ // add it back. If the record is deleted, just bail. It shouldn't
1215
+ // give us any more trouble after this.
1216
+
1217
+ if (get(record, 'isDeleted')) { return; }
1218
+ this.updateRecordArrays(type, clientId, get(record, 'data'));
1219
+ },
1220
+
1221
+ // ..............
1222
+ // . PERSISTING .
1223
+ // ..............
1224
+
1225
+ commit: function() {
1226
+ var defaultTransaction = get(this, 'defaultTransaction');
1227
+ set(this, 'defaultTransaction', this.transaction());
1228
+
1229
+ defaultTransaction.commit();
1230
+ },
1231
+
1232
+ didUpdateRecords: function(array, hashes) {
1233
+ if (hashes) {
1234
+ array.forEach(function(record, idx) {
1235
+ this.didUpdateRecord(record, hashes[idx]);
1236
+ }, this);
1237
+ } else {
1238
+ array.forEach(function(record) {
1239
+ this.didUpdateRecord(record);
1240
+ }, this);
1241
+ }
1242
+ },
1243
+
1244
+ didUpdateRecord: function(record, hash) {
1245
+ if (hash) {
1246
+ var clientId = get(record, 'clientId'),
1247
+ dataCache = this.typeMapFor(record.constructor).cidToHash;
1248
+
1249
+ dataCache[clientId] = hash;
1250
+ record.send('didChangeData');
1251
+ record.hashWasUpdated();
1252
+ } else {
1253
+ record.send('didSaveData');
1254
+ }
1255
+
1256
+ record.send('didCommit');
1257
+ },
1258
+
1259
+ didDeleteRecords: function(array) {
1260
+ array.forEach(function(record) {
1261
+ record.send('didCommit');
1262
+ });
1263
+ },
1264
+
1265
+ didDeleteRecord: function(record) {
1266
+ record.send('didCommit');
1267
+ },
1268
+
1269
+ _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) {
1270
+ var recordData = get(record, 'data'), id, changes;
1271
+
1272
+ if (hash) {
1273
+ typeMap.cidToHash[clientId] = hash;
1274
+
1275
+ // If the server returns a hash, we assume that the server's version
1276
+ // of the data supercedes the local changes.
1277
+ record.beginPropertyChanges();
1278
+ record.send('didChangeData');
1279
+ recordData.adapterDidUpdate();
1280
+ record.hashWasUpdated();
1281
+ record.endPropertyChanges();
1282
+
1283
+ id = hash[primaryKey];
1284
+
1285
+ typeMap.idToCid[id] = clientId;
1286
+ this.clientIdToId[clientId] = id;
1287
+ } else {
1288
+ recordData.commit();
1289
+ }
1290
+
1291
+ record.send('didCommit');
1292
+ },
1293
+
1294
+
1295
+ didCreateRecords: function(type, array, hashes) {
1296
+ var primaryKey = type.proto().primaryKey,
1297
+ typeMap = this.typeMapFor(type),
1298
+ clientId;
1299
+
1300
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
1301
+ var record = array[i], hash = hashes[i];
1302
+ clientId = get(record, 'clientId');
1303
+
1304
+ this._didCreateRecord(record, hash, typeMap, clientId, primaryKey);
1305
+ }
1306
+ },
1307
+
1308
+ didCreateRecord: function(record, hash) {
1309
+ var type = record.constructor,
1310
+ typeMap = this.typeMapFor(type),
1311
+ clientId, primaryKey;
1312
+
1313
+ // The hash is optional, but if it is not provided, the client must have
1314
+ // provided a primary key.
1315
+
1316
+ primaryKey = type.proto().primaryKey;
1317
+
1318
+ // TODO: Make Ember.assert more flexible
1319
+ if (hash) {
1320
+
1321
+ } else {
1322
+
1323
+ }
1324
+
1325
+ clientId = get(record, 'clientId');
1326
+
1327
+ this._didCreateRecord(record, hash, typeMap, clientId, primaryKey);
1328
+ },
1329
+
1330
+ recordWasInvalid: function(record, errors) {
1331
+ record.send('becameInvalid', errors);
1332
+ },
1333
+
1334
+ // .................
1335
+ // . RECORD ARRAYS .
1336
+ // .................
1337
+
1338
+ registerRecordArray: function(array, type, filter) {
1339
+ var recordArrays = this.typeMapFor(type).recordArrays;
1340
+
1341
+ recordArrays.push(array);
1342
+
1343
+ this.updateRecordArrayFilter(array, type, filter);
1344
+ },
1345
+
1346
+ createManyArray: function(type, clientIds) {
1347
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
1348
+
1349
+ clientIds.forEach(function(clientId) {
1350
+ var recordArrays = this.recordArraysForClientId(clientId);
1351
+ recordArrays.add(array);
1352
+ }, this);
1353
+
1354
+ return array;
1355
+ },
1356
+
1357
+ updateRecordArrayFilter: function(array, type, filter) {
1358
+ var typeMap = this.typeMapFor(type),
1359
+ dataCache = typeMap.cidToHash,
1360
+ clientIds = typeMap.clientIds,
1361
+ clientId, hash, proxy;
1362
+
1363
+ var recordCache = get(this, 'recordCache'), record;
1364
+
1365
+ for (var i=0, l=clientIds.length; i<l; i++) {
1366
+ clientId = clientIds[i];
1367
+
1368
+ hash = dataCache[clientId];
1369
+ if (typeof hash === 'object') {
1370
+ if (record = recordCache[clientId]) {
1371
+ proxy = get(record, 'data');
1372
+ } else {
1373
+ DATA_PROXY.savedData = hash;
1374
+ proxy = DATA_PROXY;
1375
+ }
1376
+
1377
+ this.updateRecordArray(array, filter, type, clientId, proxy);
1378
+ }
1379
+ }
1380
+ },
1381
+
1382
+ updateRecordArrays: function(type, clientId, dataProxy) {
1383
+ var recordArrays = this.typeMapFor(type).recordArrays,
1384
+ filter;
1385
+
1386
+ recordArrays.forEach(function(array) {
1387
+ filter = get(array, 'filterFunction');
1388
+ this.updateRecordArray(array, filter, type, clientId, dataProxy);
1389
+ }, this);
1390
+ },
1391
+
1392
+ updateRecordArray: function(array, filter, type, clientId, dataProxy) {
1393
+ var shouldBeInArray;
1394
+
1395
+ if (!filter) {
1396
+ shouldBeInArray = true;
1397
+ } else {
1398
+ shouldBeInArray = filter(dataProxy);
1399
+ }
1400
+
1401
+ var content = get(array, 'content');
1402
+ var alreadyInArray = content.indexOf(clientId) !== -1;
1403
+
1404
+ var recordArrays = this.recordArraysForClientId(clientId);
1405
+
1406
+ if (shouldBeInArray && !alreadyInArray) {
1407
+ recordArrays.add(array);
1408
+ content.pushObject(clientId);
1409
+ } else if (!shouldBeInArray && alreadyInArray) {
1410
+ recordArrays.remove(array);
1411
+ content.removeObject(clientId);
1412
+ }
1413
+ },
1414
+
1415
+ removeFromRecordArrays: function(record) {
1416
+ var clientId = get(record, 'clientId');
1417
+ var recordArrays = this.recordArraysForClientId(clientId);
1418
+
1419
+ recordArrays.forEach(function(array) {
1420
+ var content = get(array, 'content');
1421
+ content.removeObject(clientId);
1422
+ });
1423
+ },
1424
+
1425
+ // ............
1426
+ // . INDEXING .
1427
+ // ............
1428
+
1429
+ recordArraysForClientId: function(clientId) {
1430
+ var recordArrays = get(this, 'recordArraysByClientId');
1431
+ var ret = recordArrays[clientId];
1432
+
1433
+ if (!ret) {
1434
+ ret = recordArrays[clientId] = Ember.OrderedSet.create();
1435
+ }
1436
+
1437
+ return ret;
1438
+ },
1439
+
1440
+ typeMapFor: function(type) {
1441
+ var typeMaps = get(this, 'typeMaps');
1442
+ var guidForType = Ember.guidFor(type);
1443
+
1444
+ var typeMap = typeMaps[guidForType];
1445
+
1446
+ if (typeMap) {
1447
+ return typeMap;
1448
+ } else {
1449
+ return (typeMaps[guidForType] =
1450
+ {
1451
+ idToCid: {},
1452
+ clientIds: [],
1453
+ cidToHash: {},
1454
+ recordArrays: []
1455
+ });
1456
+ }
1457
+ },
1458
+
1459
+ /** @private
1460
+
1461
+ For a given type and id combination, returns the client id used by the store.
1462
+ If no client id has been assigned yet, one will be created and returned.
1463
+
1464
+ @param {DS.Model} type
1465
+ @param {String|Number} id
1466
+ */
1467
+ clientIdForId: function(type, id) {
1468
+ var clientId = this.typeMapFor(type).idToCid[id];
1469
+
1470
+ if (clientId !== undefined) { return clientId; }
1471
+
1472
+ return this.pushHash(UNLOADED, id, type);
1473
+ },
1474
+
1475
+ // ................
1476
+ // . LOADING DATA .
1477
+ // ................
1478
+
1479
+ /**
1480
+ Load a new data hash into the store for a given id and type combination.
1481
+ If data for that record had been loaded previously, the new information
1482
+ overwrites the old.
1483
+
1484
+ If the record you are loading data for has outstanding changes that have not
1485
+ yet been saved, an exception will be thrown.
1486
+
1487
+ @param {DS.Model} type
1488
+ @param {String|Number} id
1489
+ @param {Object} hash the data hash to load
1490
+ */
1491
+ load: function(type, id, hash) {
1492
+ if (hash === undefined) {
1493
+ hash = id;
1494
+ var primaryKey = type.proto().primaryKey;
1495
+
1496
+ id = hash[primaryKey];
1497
+ }
1498
+
1499
+ var typeMap = this.typeMapFor(type),
1500
+ dataCache = typeMap.cidToHash,
1501
+ clientId = typeMap.idToCid[id],
1502
+ recordCache = get(this, 'recordCache');
1503
+
1504
+ if (clientId !== undefined) {
1505
+ dataCache[clientId] = hash;
1506
+
1507
+ var record = recordCache[clientId];
1508
+ if (record) {
1509
+ record.send('didChangeData');
1510
+ }
1511
+ } else {
1512
+ clientId = this.pushHash(hash, id, type);
1513
+ }
1514
+
1515
+ DATA_PROXY.savedData = hash;
1516
+ this.updateRecordArrays(type, clientId, DATA_PROXY);
1517
+
1518
+ return { id: id, clientId: clientId };
1519
+ },
1520
+
1521
+ loadMany: function(type, ids, hashes) {
1522
+ var clientIds = Ember.A([]);
1523
+
1524
+ if (hashes === undefined) {
1525
+ hashes = ids;
1526
+ ids = [];
1527
+ var primaryKey = type.proto().primaryKey;
1528
+
1529
+ ids = Ember.ArrayUtils.map(hashes, function(hash) {
1530
+ return hash[primaryKey];
1531
+ });
1532
+ }
1533
+
1534
+ for (var i=0, l=get(ids, 'length'); i<l; i++) {
1535
+ var loaded = this.load(type, ids[i], hashes[i]);
1536
+ clientIds.pushObject(loaded.clientId);
1537
+ }
1538
+
1539
+ return { clientIds: clientIds, ids: ids };
1540
+ },
1541
+
1542
+ /** @private
1543
+
1544
+ Stores a data hash for the specified type and id combination and returns
1545
+ the client id.
1546
+
1547
+ @param {Object} hash
1548
+ @param {String|Number} id
1549
+ @param {DS.Model} type
1550
+ @returns {Number}
1551
+ */
1552
+ pushHash: function(hash, id, type) {
1553
+ var typeMap = this.typeMapFor(type);
1554
+
1555
+ var idToClientIdMap = typeMap.idToCid,
1556
+ clientIdToIdMap = this.clientIdToId,
1557
+ clientIds = typeMap.clientIds,
1558
+ dataCache = typeMap.cidToHash;
1559
+
1560
+ var clientId = ++this.clientIdCounter;
1561
+
1562
+ dataCache[clientId] = hash;
1563
+
1564
+ // if we're creating an item, this process will be done
1565
+ // later, once the object has been persisted.
1566
+ if (id) {
1567
+ idToClientIdMap[id] = clientId;
1568
+ clientIdToIdMap[clientId] = id;
1569
+ }
1570
+
1571
+ clientIds.push(clientId);
1572
+
1573
+ return clientId;
1574
+ },
1575
+
1576
+ // ..........................
1577
+ // . RECORD MATERIALIZATION .
1578
+ // ..........................
1579
+
1580
+ materializeRecord: function(type, clientId) {
1581
+ var record;
1582
+
1583
+ get(this, 'recordCache')[clientId] = record = type._create({
1584
+ store: this,
1585
+ clientId: clientId
1586
+ });
1587
+
1588
+ get(this, 'defaultTransaction').adoptRecord(record);
1589
+
1590
+ record.send('loadingData');
1591
+ return record;
1592
+ },
1593
+
1594
+ destroy: function() {
1595
+ if (get(DS, 'defaultStore') === this) {
1596
+ set(DS, 'defaultStore', null);
1597
+ }
1598
+
1599
+ return this._super();
1600
+ }
1601
+ });
1602
+
1603
+ })();
1604
+
1605
+
1606
+
1607
+ (function() {
1608
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
1609
+
1610
+ /**
1611
+ This file encapsulates the various states that a record can transition
1612
+ through during its lifecycle.
1613
+
1614
+ ### State Manager
1615
+
1616
+ A record's state manager explicitly tracks what state a record is in
1617
+ at any given time. For instance, if a record is newly created and has
1618
+ not yet been sent to the adapter to be saved, it would be in the
1619
+ `created.uncommitted` state. If a record has had local modifications
1620
+ made to it that are in the process of being saved, the record would be
1621
+ in the `updated.inFlight` state. (These state paths will be explained
1622
+ in more detail below.)
1623
+
1624
+ Events are sent by the record or its store to the record's state manager.
1625
+ How the state manager reacts to these events is dependent on which state
1626
+ it is in. In some states, certain events will be invalid and will cause
1627
+ an exception to be raised.
1628
+
1629
+ States are hierarchical. For example, a record can be in the
1630
+ `deleted.start` state, then transition into the `deleted.inFlight` state.
1631
+ If a child state does not implement an event handler, the state manager
1632
+ will attempt to invoke the event on all parent states until the root state is
1633
+ reached. The state hierarchy of a record is described in terms of a path
1634
+ string. You can determine a record's current state by getting its manager's
1635
+ current state path:
1636
+
1637
+ record.getPath('stateManager.currentState.path');
1638
+ //=> "created.uncommitted"
1639
+
1640
+ The `DS.Model` states are themselves stateless. What we mean is that,
1641
+ though each instance of a record also has a unique instance of a
1642
+ `DS.StateManager`, the hierarchical states that each of *those* points
1643
+ to is a shared data structure. For performance reasons, instead of each
1644
+ record getting its own copy of the hierarchy of states, each state
1645
+ manager points to this global, immutable shared instance. How does a
1646
+ state know which record it should be acting on? We pass a reference to
1647
+ the current state manager as the first parameter to every method invoked
1648
+ on a state.
1649
+
1650
+ The state manager passed as the first parameter is where you should stash
1651
+ state about the record if needed; you should never store data on the state
1652
+ object itself. If you need access to the record being acted on, you can
1653
+ retrieve the state manager's `record` property. For example, if you had
1654
+ an event handler `myEvent`:
1655
+
1656
+ myEvent: function(manager) {
1657
+ var record = manager.get('record');
1658
+ record.doSomething();
1659
+ }
1660
+
1661
+ For more information about state managers in general, see the Ember.js
1662
+ documentation on `Ember.StateManager`.
1663
+
1664
+ ### Events, Flags, and Transitions
1665
+
1666
+ A state may implement zero or more events, flags, or transitions.
1667
+
1668
+ #### Events
1669
+
1670
+ Events are named functions that are invoked when sent to a record. The
1671
+ state manager will first look for a method with the given name on the
1672
+ current state. If no method is found, it will search the current state's
1673
+ parent, and then its grandparent, and so on until reaching the top of
1674
+ the hierarchy. If the root is reached without an event handler being found,
1675
+ an exception will be raised. This can be very helpful when debugging new
1676
+ features.
1677
+
1678
+ Here's an example implementation of a state with a `myEvent` event handler:
1679
+
1680
+ aState: DS.State.create({
1681
+ myEvent: function(manager, param) {
1682
+ console.log("Received myEvent with "+param);
1683
+ }
1684
+ })
1685
+
1686
+ To trigger this event:
1687
+
1688
+ record.send('myEvent', 'foo');
1689
+ //=> "Received myEvent with foo"
1690
+
1691
+ Note that an optional parameter can be sent to a record's `send()` method,
1692
+ which will be passed as the second parameter to the event handler.
1693
+
1694
+ Events should transition to a different state if appropriate. This can be
1695
+ done by calling the state manager's `goToState()` method with a path to the
1696
+ desired state. The state manager will attempt to resolve the state path
1697
+ relative to the current state. If no state is found at that path, it will
1698
+ attempt to resolve it relative to the current state's parent, and then its
1699
+ parent, and so on until the root is reached. For example, imagine a hierarchy
1700
+ like this:
1701
+
1702
+ * created
1703
+ * start <-- currentState
1704
+ * inFlight
1705
+ * updated
1706
+ * inFlight
1707
+
1708
+ If we are currently in the `start` state, calling
1709
+ `goToState('inFlight')` would transition to the `created.inFlight` state,
1710
+ while calling `goToState('updated.inFlight')` would transition to
1711
+ the `updated.inFlight` state.
1712
+
1713
+ Remember that *only events* should ever cause a state transition. You should
1714
+ never call `goToState()` from outside a state's event handler. If you are
1715
+ tempted to do so, create a new event and send that to the state manager.
1716
+
1717
+ #### Flags
1718
+
1719
+ Flags are Boolean values that can be used to introspect a record's current
1720
+ state in a more user-friendly way than examining its state path. For example,
1721
+ instead of doing this:
1722
+
1723
+ var statePath = record.getPath('stateManager.currentState.path');
1724
+ if (statePath === 'created.inFlight') {
1725
+ doSomething();
1726
+ }
1727
+
1728
+ You can say:
1729
+
1730
+ if (record.get('isNew') && record.get('isSaving')) {
1731
+ doSomething();
1732
+ }
1733
+
1734
+ If your state does not set a value for a given flag, the value will
1735
+ be inherited from its parent (or the first place in the state hierarchy
1736
+ where it is defined).
1737
+
1738
+ The current set of flags are defined below. If you want to add a new flag,
1739
+ in addition to the area below, you will also need to declare it in the
1740
+ `DS.Model` class.
1741
+
1742
+ #### Transitions
1743
+
1744
+ Transitions are like event handlers but are called automatically upon
1745
+ entering or exiting a state. To implement a transition, just call a method
1746
+ either `enter` or `exit`:
1747
+
1748
+ myState: DS.State.create({
1749
+ // Gets called automatically when entering
1750
+ // this state.
1751
+ enter: function(manager) {
1752
+ console.log("Entered myState");
1753
+ }
1754
+ })
1755
+
1756
+ Note that enter and exit events are called once per transition. If the
1757
+ current state changes, but changes to another child state of the parent,
1758
+ the transition event on the parent will not be triggered.
1759
+ */
1760
+
1761
+ var stateProperty = Ember.computed(function(key) {
1762
+ var parent = get(this, 'parentState');
1763
+ if (parent) {
1764
+ return get(parent, key);
1765
+ }
1766
+ }).property();
1767
+
1768
+ var isEmptyObject = function(object) {
1769
+ for (var name in object) {
1770
+ if (object.hasOwnProperty(name)) { return false; }
1771
+ }
1772
+
1773
+ return true;
1774
+ };
1775
+
1776
+ var hasDefinedProperties = function(object) {
1777
+ for (var name in object) {
1778
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
1779
+ }
1780
+
1781
+ return false;
1782
+ };
1783
+
1784
+ DS.State = Ember.State.extend({
1785
+ isLoaded: stateProperty,
1786
+ isDirty: stateProperty,
1787
+ isSaving: stateProperty,
1788
+ isDeleted: stateProperty,
1789
+ isError: stateProperty,
1790
+ isNew: stateProperty,
1791
+ isValid: stateProperty,
1792
+ isPending: stateProperty,
1793
+
1794
+ // For states that are substates of a
1795
+ // DirtyState (updated or created), it is
1796
+ // useful to be able to determine which
1797
+ // type of dirty state it is.
1798
+ dirtyType: stateProperty
1799
+ });
1800
+
1801
+ var setProperty = function(manager, context) {
1802
+ var key = context.key, value = context.value;
1803
+
1804
+ var record = get(manager, 'record'),
1805
+ data = get(record, 'data');
1806
+
1807
+ set(data, key, value);
1808
+ };
1809
+
1810
+ var setAssociation = function(manager, context) {
1811
+ var key = context.key, value = context.value;
1812
+
1813
+ var record = get(manager, 'record'),
1814
+ data = get(record, 'data');
1815
+
1816
+ data.setAssociation(key, value);
1817
+ };
1818
+
1819
+ var didChangeData = function(manager) {
1820
+ var record = get(manager, 'record'),
1821
+ data = get(record, 'data');
1822
+
1823
+ data._savedData = null;
1824
+ record.notifyPropertyChange('data');
1825
+ };
1826
+
1827
+ // The waitingOn event shares common functionality
1828
+ // between the different dirty states, but each is
1829
+ // treated slightly differently. This method is exposed
1830
+ // so that each implementation can invoke the common
1831
+ // behavior, and then implement the behavior specific
1832
+ // to the state.
1833
+ var waitingOn = function(manager, object) {
1834
+ var record = get(manager, 'record'),
1835
+ pendingQueue = get(record, 'pendingQueue'),
1836
+ objectGuid = guidFor(object);
1837
+
1838
+ var observer = function() {
1839
+ if (get(object, 'id')) {
1840
+ manager.send('doneWaitingOn', object);
1841
+ Ember.removeObserver(object, 'id', observer);
1842
+ }
1843
+ };
1844
+
1845
+ pendingQueue[objectGuid] = [object, observer];
1846
+ Ember.addObserver(object, 'id', observer);
1847
+ };
1848
+
1849
+ // Implementation notes:
1850
+ //
1851
+ // Each state has a boolean value for all of the following flags:
1852
+ //
1853
+ // * isLoaded: The record has a populated `data` property. When a
1854
+ // record is loaded via `store.find`, `isLoaded` is false
1855
+ // until the adapter sets it. When a record is created locally,
1856
+ // its `isLoaded` property is always true.
1857
+ // * isDirty: The record has local changes that have not yet been
1858
+ // saved by the adapter. This includes records that have been
1859
+ // created (but not yet saved) or deleted.
1860
+ // * isSaving: The record's transaction has been committed, but
1861
+ // the adapter has not yet acknowledged that the changes have
1862
+ // been persisted to the backend.
1863
+ // * isDeleted: The record was marked for deletion. When `isDeleted`
1864
+ // is true and `isDirty` is true, the record is deleted locally
1865
+ // but the deletion was not yet persisted. When `isSaving` is
1866
+ // true, the change is in-flight. When both `isDirty` and
1867
+ // `isSaving` are false, the change has persisted.
1868
+ // * isError: The adapter reported that it was unable to save
1869
+ // local changes to the backend. This may also result in the
1870
+ // record having its `isValid` property become false if the
1871
+ // adapter reported that server-side validations failed.
1872
+ // * isNew: The record was created on the client and the adapter
1873
+ // did not yet report that it was successfully saved.
1874
+ // * isValid: No client-side validations have failed and the
1875
+ // adapter did not report any server-side validation failures.
1876
+ // * isPending: A record `isPending` when it belongs to an
1877
+ // association on another record and that record has not been
1878
+ // saved. A record in this state cannot be saved because it
1879
+ // lacks a "foreign key" that will be supplied by its parent
1880
+ // association when the parent record has been created. When
1881
+ // the adapter reports that the parent has saved, the
1882
+ // `isPending` property on all children will become `false`
1883
+ // and the transaction will try to commit the records.
1884
+
1885
+ // This mixin is mixed into various uncommitted states. Make
1886
+ // sure to mix it in *after* the class definition, so its
1887
+ // super points to the class definition.
1888
+ var Uncommitted = Ember.Mixin.create({
1889
+ setProperty: setProperty,
1890
+ setAssociation: setAssociation,
1891
+ });
1892
+
1893
+ // These mixins are mixed into substates of the concrete
1894
+ // subclasses of DirtyState.
1895
+
1896
+ var CreatedUncommitted = Ember.Mixin.create({
1897
+ deleteRecord: function(manager) {
1898
+ var record = get(manager, 'record');
1899
+ this._super(manager);
1900
+
1901
+ record.withTransaction(function(t) {
1902
+ t.recordBecameClean('created', record);
1903
+ });
1904
+ manager.goToState('deleted.saved');
1905
+ }
1906
+ });
1907
+
1908
+ var UpdatedUncommitted = Ember.Mixin.create({
1909
+ deleteRecord: function(manager) {
1910
+ this._super(manager);
1911
+
1912
+ var record = get(manager, 'record');
1913
+
1914
+ record.withTransaction(function(t) {
1915
+ t.recordBecameClean('updated', record);
1916
+ });
1917
+
1918
+ manager.goToState('deleted');
1919
+ }
1920
+ });
1921
+
1922
+ // The dirty state is a abstract state whose functionality is
1923
+ // shared between the `created` and `updated` states.
1924
+ //
1925
+ // The deleted state shares the `isDirty` flag with the
1926
+ // subclasses of `DirtyState`, but with a very different
1927
+ // implementation.
1928
+ var DirtyState = DS.State.extend({
1929
+ initialState: 'uncommitted',
1930
+
1931
+ // FLAGS
1932
+ isDirty: true,
1933
+
1934
+ // SUBSTATES
1935
+
1936
+ // When a record first becomes dirty, it is `uncommitted`.
1937
+ // This means that there are local pending changes,
1938
+ // but they have not yet begun to be saved.
1939
+ uncommitted: DS.State.extend({
1940
+ // TRANSITIONS
1941
+ enter: function(manager) {
1942
+ var dirtyType = get(this, 'dirtyType'),
1943
+ record = get(manager, 'record');
1944
+
1945
+ record.withTransaction(function (t) {
1946
+ t.recordBecameDirty(dirtyType, record);
1947
+ });
1948
+ },
1949
+
1950
+ // EVENTS
1951
+ deleteRecord: Ember.K,
1952
+
1953
+ waitingOn: function(manager, object) {
1954
+ waitingOn(manager, object);
1955
+ manager.goToState('pending');
1956
+ },
1957
+
1958
+ willCommit: function(manager) {
1959
+ manager.goToState('inFlight');
1960
+ },
1961
+
1962
+ becameInvalid: function(manager) {
1963
+ var dirtyType = get(this, 'dirtyType'),
1964
+ record = get(manager, 'record');
1965
+
1966
+ record.withTransaction(function (t) {
1967
+ t.recordBecameInFlight(dirtyType, record);
1968
+ });
1969
+
1970
+ manager.goToState('invalid');
1971
+ },
1972
+
1973
+ rollback: function(manager) {
1974
+ var record = get(manager, 'record'),
1975
+ dirtyType = get(this, 'dirtyType'),
1976
+ data = get(record, 'data');
1977
+
1978
+ data.rollback();
1979
+
1980
+ record.withTransaction(function(t) {
1981
+ t.recordBecameClean(dirtyType, record);
1982
+ });
1983
+
1984
+ manager.goToState('loaded');
1985
+ }
1986
+ }, Uncommitted),
1987
+
1988
+ // Once a record has been handed off to the adapter to be
1989
+ // saved, it is in the 'in flight' state. Changes to the
1990
+ // record cannot be made during this window.
1991
+ inFlight: DS.State.extend({
1992
+ // FLAGS
1993
+ isSaving: true,
1994
+
1995
+ // TRANSITIONS
1996
+ enter: function(manager) {
1997
+ var dirtyType = get(this, 'dirtyType'),
1998
+ record = get(manager, 'record');
1999
+
2000
+ record.withTransaction(function (t) {
2001
+ t.recordBecameInFlight(dirtyType, record);
2002
+ });
2003
+ },
2004
+
2005
+ // EVENTS
2006
+ didCommit: function(manager) {
2007
+ var dirtyType = get(this, 'dirtyType'),
2008
+ record = get(manager, 'record');
2009
+
2010
+ record.withTransaction(function(t) {
2011
+ t.recordBecameClean('inflight', record);
2012
+ });
2013
+
2014
+ manager.goToState('loaded');
2015
+ manager.send('invokeLifecycleCallbacks', dirtyType);
2016
+ },
2017
+
2018
+ becameInvalid: function(manager, errors) {
2019
+ var record = get(manager, 'record');
2020
+
2021
+ set(record, 'errors', errors);
2022
+
2023
+ manager.goToState('invalid');
2024
+ manager.send('invokeLifecycleCallbacks');
2025
+ },
2026
+
2027
+ becameError: function(manager) {
2028
+ manager.goToState('error');
2029
+ manager.send('invokeLifecycleCallbacks');
2030
+ },
2031
+
2032
+ didChangeData: didChangeData
2033
+ }),
2034
+
2035
+ // If a record becomes associated with a newly created
2036
+ // parent record, it will be `pending` until the parent
2037
+ // record has successfully persisted. Once this happens,
2038
+ // this record can use the parent's primary key as its
2039
+ // foreign key.
2040
+ //
2041
+ // If the record's transaction had already started to
2042
+ // commit, the record will transition to the `inFlight`
2043
+ // state. If it had not, the record will transition to
2044
+ // the `uncommitted` state.
2045
+ pending: DS.State.extend({
2046
+ initialState: 'uncommitted',
2047
+
2048
+ // FLAGS
2049
+ isPending: true,
2050
+
2051
+ // SUBSTATES
2052
+
2053
+ // A pending record whose transaction has not yet
2054
+ // started to commit is in this state.
2055
+ uncommitted: DS.State.extend({
2056
+ // EVENTS
2057
+ deleteRecord: function(manager) {
2058
+ var record = get(manager, 'record'),
2059
+ pendingQueue = get(record, 'pendingQueue'),
2060
+ tuple;
2061
+
2062
+ // since we are leaving the pending state, remove any
2063
+ // observers we have registered on other records.
2064
+ for (var prop in pendingQueue) {
2065
+ if (!pendingQueue.hasOwnProperty(prop)) { continue; }
2066
+
2067
+ tuple = pendingQueue[prop];
2068
+ Ember.removeObserver(tuple[0], 'id', tuple[1]);
2069
+ }
2070
+ },
2071
+
2072
+ willCommit: function(manager) {
2073
+ manager.goToState('committing');
2074
+ },
2075
+
2076
+ doneWaitingOn: function(manager, object) {
2077
+ var record = get(manager, 'record'),
2078
+ pendingQueue = get(record, 'pendingQueue'),
2079
+ objectGuid = guidFor(object);
2080
+
2081
+ delete pendingQueue[objectGuid];
2082
+
2083
+ if (isEmptyObject(pendingQueue)) {
2084
+ manager.send('doneWaiting');
2085
+ }
2086
+ },
2087
+
2088
+ doneWaiting: function(manager) {
2089
+ var dirtyType = get(this, 'dirtyType');
2090
+ manager.goToState(dirtyType + '.uncommitted');
2091
+ }
2092
+ }, Uncommitted),
2093
+
2094
+ // A pending record whose transaction has started
2095
+ // to commit is in this state. Since it has not yet
2096
+ // been sent to the adapter, it is not `inFlight`
2097
+ // until all of its dependencies have been committed.
2098
+ committing: DS.State.extend({
2099
+ // FLAGS
2100
+ isSaving: true,
2101
+
2102
+ // EVENTS
2103
+ doneWaitingOn: function(manager, object) {
2104
+ var record = get(manager, 'record'),
2105
+ pendingQueue = get(record, 'pendingQueue'),
2106
+ objectGuid = guidFor(object);
2107
+
2108
+ delete pendingQueue[objectGuid];
2109
+
2110
+ if (isEmptyObject(pendingQueue)) {
2111
+ manager.send('doneWaiting');
2112
+ }
2113
+ },
2114
+
2115
+ doneWaiting: function(manager) {
2116
+ var record = get(manager, 'record'),
2117
+ transaction = get(record, 'transaction');
2118
+
2119
+ // Now that the record is no longer pending, schedule
2120
+ // the transaction to commit.
2121
+ Ember.run.once(transaction, transaction.commit);
2122
+ },
2123
+
2124
+ willCommit: function(manager) {
2125
+ var record = get(manager, 'record'),
2126
+ pendingQueue = get(record, 'pendingQueue');
2127
+
2128
+ if (isEmptyObject(pendingQueue)) {
2129
+ var dirtyType = get(this, 'dirtyType');
2130
+ manager.goToState(dirtyType + '.inFlight');
2131
+ }
2132
+ }
2133
+ })
2134
+ }),
2135
+
2136
+ // A record is in the `invalid` state when its client-side
2137
+ // invalidations have failed, or if the adapter has indicated
2138
+ // the the record failed server-side invalidations.
2139
+ invalid: DS.State.extend({
2140
+ // FLAGS
2141
+ isValid: false,
2142
+
2143
+ exit: function(manager) {
2144
+ var record = get(manager, 'record');
2145
+
2146
+ record.withTransaction(function (t) {
2147
+ t.recordBecameClean('inflight', record);
2148
+ });
2149
+ },
2150
+
2151
+ // EVENTS
2152
+ deleteRecord: function(manager) {
2153
+ manager.goToState('deleted');
2154
+ },
2155
+
2156
+ setAssociation: setAssociation,
2157
+
2158
+ setProperty: function(manager, context) {
2159
+ setProperty(manager, context);
2160
+
2161
+ var record = get(manager, 'record'),
2162
+ errors = get(record, 'errors'),
2163
+ key = context.key;
2164
+
2165
+ delete errors[key];
2166
+
2167
+ if (!hasDefinedProperties(errors)) {
2168
+ manager.send('becameValid');
2169
+ }
2170
+ },
2171
+
2172
+ rollback: function(manager) {
2173
+ manager.send('becameValid');
2174
+ manager.send('rollback');
2175
+ },
2176
+
2177
+ becameValid: function(manager) {
2178
+ manager.goToState('uncommitted');
2179
+ },
2180
+
2181
+ invokeLifecycleCallbacks: function(manager) {
2182
+ var record = get(manager, 'record');
2183
+ record.fire('becameInvalid', record);
2184
+ }
2185
+ })
2186
+ });
2187
+
2188
+ // The created and updated states are created outside the state
2189
+ // chart so we can reopen their substates and add mixins as
2190
+ // necessary.
2191
+
2192
+ var createdState = DirtyState.create({
2193
+ dirtyType: 'created',
2194
+
2195
+ // FLAGS
2196
+ isNew: true
2197
+ });
2198
+
2199
+ var updatedState = DirtyState.create({
2200
+ dirtyType: 'updated'
2201
+ });
2202
+
2203
+ // The created.uncommitted state and created.pending.uncommitted share
2204
+ // some logic defined in CreatedUncommitted.
2205
+ createdState.states.uncommitted.reopen(CreatedUncommitted);
2206
+ createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
2207
+
2208
+ // The created.uncommitted state needs to immediately transition to the
2209
+ // deleted state if it is rolled back.
2210
+ createdState.states.uncommitted.reopen({
2211
+ rollback: function(manager) {
2212
+ this._super(manager);
2213
+ manager.goToState('deleted.saved');
2214
+ }
2215
+ });
2216
+
2217
+ // The updated.uncommitted state and updated.pending.uncommitted share
2218
+ // some logic defined in UpdatedUncommitted.
2219
+ updatedState.states.uncommitted.reopen(UpdatedUncommitted);
2220
+ updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
2221
+ updatedState.states.inFlight.reopen({
2222
+ didSaveData: function(manager) {
2223
+ var record = get(manager, 'record'),
2224
+ data = get(record, 'data');
2225
+
2226
+ data.saveData();
2227
+ data.adapterDidUpdate();
2228
+ }
2229
+ });
2230
+
2231
+ var states = {
2232
+ rootState: Ember.State.create({
2233
+ // FLAGS
2234
+ isLoaded: false,
2235
+ isDirty: false,
2236
+ isSaving: false,
2237
+ isDeleted: false,
2238
+ isError: false,
2239
+ isNew: false,
2240
+ isValid: true,
2241
+ isPending: false,
2242
+
2243
+ // SUBSTATES
2244
+
2245
+ // A record begins its lifecycle in the `empty` state.
2246
+ // If its data will come from the adapter, it will
2247
+ // transition into the `loading` state. Otherwise, if
2248
+ // the record is being created on the client, it will
2249
+ // transition into the `created` state.
2250
+ empty: DS.State.create({
2251
+ // EVENTS
2252
+ loadingData: function(manager) {
2253
+ manager.goToState('loading');
2254
+ },
2255
+
2256
+ didChangeData: function(manager) {
2257
+ didChangeData(manager);
2258
+
2259
+ manager.goToState('loaded.created');
2260
+ }
2261
+ }),
2262
+
2263
+ // A record enters this state when the store askes
2264
+ // the adapter for its data. It remains in this state
2265
+ // until the adapter provides the requested data.
2266
+ //
2267
+ // Usually, this process is asynchronous, using an
2268
+ // XHR to retrieve the data.
2269
+ loading: DS.State.create({
2270
+ // TRANSITIONS
2271
+ exit: function(manager) {
2272
+ var record = get(manager, 'record');
2273
+ record.fire('didLoad');
2274
+ },
2275
+
2276
+ // EVENTS
2277
+ didChangeData: function(manager, data) {
2278
+ didChangeData(manager);
2279
+ manager.send('loadedData');
2280
+ },
2281
+
2282
+ loadedData: function(manager) {
2283
+ manager.goToState('loaded');
2284
+ }
2285
+ }),
2286
+
2287
+ // A record enters this state when its data is populated.
2288
+ // Most of a record's lifecycle is spent inside substates
2289
+ // of the `loaded` state.
2290
+ loaded: DS.State.create({
2291
+ initialState: 'saved',
2292
+
2293
+ // FLAGS
2294
+ isLoaded: true,
2295
+
2296
+ // SUBSTATES
2297
+
2298
+ // If there are no local changes to a record, it remains
2299
+ // in the `saved` state.
2300
+ saved: DS.State.create({
2301
+
2302
+ // EVENTS
2303
+ setProperty: function(manager, context) {
2304
+ setProperty(manager, context);
2305
+ manager.goToState('updated');
2306
+ },
2307
+
2308
+ setAssociation: function(manager, context) {
2309
+ setAssociation(manager, context);
2310
+ manager.goToState('updated');
2311
+ },
2312
+
2313
+ didChangeData: didChangeData,
2314
+
2315
+ deleteRecord: function(manager) {
2316
+ manager.goToState('deleted');
2317
+ },
2318
+
2319
+ waitingOn: function(manager, object) {
2320
+ waitingOn(manager, object);
2321
+ manager.goToState('updated.pending');
2322
+ },
2323
+
2324
+ invokeLifecycleCallbacks: function(manager, dirtyType) {
2325
+ var record = get(manager, 'record');
2326
+ if (dirtyType === 'created') {
2327
+ record.fire('didCreate', record);
2328
+ } else {
2329
+ record.fire('didUpdate', record);
2330
+ }
2331
+ }
2332
+ }),
2333
+
2334
+ // A record is in this state after it has been locally
2335
+ // created but before the adapter has indicated that
2336
+ // it has been saved.
2337
+ created: createdState,
2338
+
2339
+ // A record is in this state if it has already been
2340
+ // saved to the server, but there are new local changes
2341
+ // that have not yet been saved.
2342
+ updated: updatedState
2343
+ }),
2344
+
2345
+ // A record is in this state if it was deleted from the store.
2346
+ deleted: DS.State.create({
2347
+ // FLAGS
2348
+ isDeleted: true,
2349
+ isLoaded: true,
2350
+ isDirty: true,
2351
+
2352
+ // TRANSITIONS
2353
+ enter: function(manager) {
2354
+ var record = get(manager, 'record'),
2355
+ store = get(record, 'store');
2356
+
2357
+ store.removeFromRecordArrays(record);
2358
+ },
2359
+
2360
+ // SUBSTATES
2361
+
2362
+ // When a record is deleted, it enters the `start`
2363
+ // state. It will exit this state when the record's
2364
+ // transaction starts to commit.
2365
+ start: DS.State.create({
2366
+ // TRANSITIONS
2367
+ enter: function(manager) {
2368
+ var record = get(manager, 'record');
2369
+
2370
+ record.withTransaction(function(t) {
2371
+ t.recordBecameDirty('deleted', record);
2372
+ });
2373
+ },
2374
+
2375
+ // EVENTS
2376
+ willCommit: function(manager) {
2377
+ manager.goToState('inFlight');
2378
+ },
2379
+
2380
+ rollback: function(manager) {
2381
+ var record = get(manager, 'record'),
2382
+ data = get(record, 'data');
2383
+
2384
+ data.rollback();
2385
+ record.withTransaction(function(t) {
2386
+ t.recordBecameClean('deleted', record);
2387
+ });
2388
+ manager.goToState('loaded');
2389
+ }
2390
+ }),
2391
+
2392
+ // After a record's transaction is committing, but
2393
+ // before the adapter indicates that the deletion
2394
+ // has saved to the server, a record is in the
2395
+ // `inFlight` substate of `deleted`.
2396
+ inFlight: DS.State.create({
2397
+ // FLAGS
2398
+ isSaving: true,
2399
+
2400
+ // TRANSITIONS
2401
+ enter: function(manager) {
2402
+ var record = get(manager, 'record');
2403
+
2404
+ record.withTransaction(function (t) {
2405
+ t.recordBecameInFlight('deleted', record);
2406
+ });
2407
+ },
2408
+
2409
+ // EVENTS
2410
+ didCommit: function(manager) {
2411
+ var record = get(manager, 'record');
2412
+
2413
+ record.withTransaction(function(t) {
2414
+ t.recordBecameClean('inflight', record);
2415
+ });
2416
+
2417
+ manager.goToState('saved');
2418
+
2419
+ manager.send('invokeLifecycleCallbacks');
2420
+ }
2421
+ }),
2422
+
2423
+ // Once the adapter indicates that the deletion has
2424
+ // been saved, the record enters the `saved` substate
2425
+ // of `deleted`.
2426
+ saved: DS.State.create({
2427
+ // FLAGS
2428
+ isDirty: false,
2429
+
2430
+ invokeLifecycleCallbacks: function(manager) {
2431
+ var record = get(manager, 'record');
2432
+ record.fire('didDelete', record);
2433
+ }
2434
+ })
2435
+ }),
2436
+
2437
+ // If the adapter indicates that there was an unknown
2438
+ // error saving a record, the record enters the `error`
2439
+ // state.
2440
+ error: DS.State.create({
2441
+ isError: true,
2442
+
2443
+ // EVENTS
2444
+
2445
+ invokeLifecycleCallbacks: function(manager) {
2446
+ var record = get(manager, 'record');
2447
+ record.fire('becameError', record);
2448
+ }
2449
+ })
2450
+ })
2451
+ };
2452
+
2453
+ DS.StateManager = Ember.StateManager.extend({
2454
+ record: null,
2455
+ initialState: 'rootState',
2456
+ states: states
2457
+ });
2458
+
2459
+ })();
2460
+
2461
+
2462
+
2463
+ (function() {
2464
+ var get = Ember.get, set = Ember.set;
2465
+
2466
+ // When a record is changed on the client, it is considered "dirty"--there are
2467
+ // pending changes that need to be saved to a persistence layer, such as a
2468
+ // server.
2469
+ //
2470
+ // If the record is rolled back, it re-enters a clean state, any changes are
2471
+ // discarded, and its attributes are reset back to the last known good copy
2472
+ // of the data that came from the server.
2473
+ //
2474
+ // If the record is committed, the changes are sent to the server to be saved,
2475
+ // and once the server confirms that they are valid, the record's "canonical"
2476
+ // data becomes the original canonical data plus the changes merged in.
2477
+ //
2478
+ // A DataProxy is an object that encapsulates this change tracking. It
2479
+ // contains three buckets:
2480
+ //
2481
+ // * `savedData` - the last-known copy of the data from the server
2482
+ // * `unsavedData` - a hash that contains any changes that have not yet
2483
+ // been committed
2484
+ // * `associations` - this is similar to `savedData`, but holds the client
2485
+ // ids of associated records
2486
+ //
2487
+ // When setting a property on the object, the value is placed into the
2488
+ // `unsavedData` bucket:
2489
+ //
2490
+ // proxy.set('key', 'value');
2491
+ //
2492
+ // // unsavedData:
2493
+ // {
2494
+ // key: "value"
2495
+ // }
2496
+ //
2497
+ // When retrieving a property from the object, it first looks to see
2498
+ // if that value exists in the `unsavedData` bucket, and returns it if so.
2499
+ // Otherwise, it returns the value from the `savedData` bucket.
2500
+ //
2501
+ // When the adapter notifies a record that it has been saved, it merges the
2502
+ // `unsavedData` bucket into the `savedData` bucket. If the record's
2503
+ // transaction is rolled back, the `unsavedData` hash is simply discarded.
2504
+ //
2505
+ // This object is a regular JS object for performance. It is only
2506
+ // used internally for bookkeeping purposes.
2507
+
2508
+ var DataProxy = DS._DataProxy = function(record) {
2509
+ this.record = record;
2510
+
2511
+ this.unsavedData = {};
2512
+
2513
+ this.associations = {};
2514
+ };
2515
+
2516
+ DataProxy.prototype = {
2517
+ get: function(key) { return Ember.get(this, key); },
2518
+ set: function(key, value) { return Ember.set(this, key, value); },
2519
+
2520
+ setAssociation: function(key, value) {
2521
+ this.associations[key] = value;
2522
+ },
2523
+
2524
+ savedData: function() {
2525
+ var savedData = this._savedData;
2526
+ if (savedData) { return savedData; }
2527
+
2528
+ var record = this.record,
2529
+ clientId = get(record, 'clientId'),
2530
+ store = get(record, 'store');
2531
+
2532
+ if (store) {
2533
+ savedData = store.dataForRecord(record);
2534
+ this._savedData = savedData;
2535
+ return savedData;
2536
+ }
2537
+ },
2538
+
2539
+ unknownProperty: function(key) {
2540
+ var unsavedData = this.unsavedData,
2541
+ associations = this.associations,
2542
+ savedData = this.savedData(),
2543
+ store;
2544
+
2545
+ var value = unsavedData[key], association;
2546
+
2547
+ // if this is a belongsTo association, this will
2548
+ // be a clientId.
2549
+ association = associations[key];
2550
+
2551
+ if (association !== undefined) {
2552
+ store = get(this.record, 'store');
2553
+ return store.clientIdToId[association];
2554
+ }
2555
+
2556
+ if (savedData && value === undefined) {
2557
+ value = savedData[key];
2558
+ }
2559
+
2560
+ return value;
2561
+ },
2562
+
2563
+ setUnknownProperty: function(key, value) {
2564
+ var record = this.record,
2565
+ unsavedData = this.unsavedData;
2566
+
2567
+ unsavedData[key] = value;
2568
+
2569
+ record.hashWasUpdated();
2570
+
2571
+ return value;
2572
+ },
2573
+
2574
+ commit: function() {
2575
+ this.saveData();
2576
+
2577
+ this.record.notifyPropertyChange('data');
2578
+ },
2579
+
2580
+ rollback: function() {
2581
+ this.unsavedData = {};
2582
+
2583
+ this.record.notifyPropertyChange('data');
2584
+ },
2585
+
2586
+ saveData: function() {
2587
+ var record = this.record;
2588
+
2589
+ var unsavedData = this.unsavedData;
2590
+ var savedData = this.savedData();
2591
+
2592
+ for (var prop in unsavedData) {
2593
+ if (unsavedData.hasOwnProperty(prop)) {
2594
+ savedData[prop] = unsavedData[prop];
2595
+ delete unsavedData[prop];
2596
+ }
2597
+ }
2598
+ },
2599
+
2600
+ adapterDidUpdate: function() {
2601
+ this.unsavedData = {};
2602
+ }
2603
+ };
2604
+
2605
+ })();
2606
+
2607
+
2608
+
2609
+ (function() {
2610
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none;
2611
+
2612
+ var retrieveFromCurrentState = Ember.computed(function(key) {
2613
+ return get(getPath(this, 'stateManager.currentState'), key);
2614
+ }).property('stateManager.currentState').cacheable();
2615
+
2616
+ DS.Model = Ember.Object.extend(Ember.Evented, {
2617
+ isLoaded: retrieveFromCurrentState,
2618
+ isDirty: retrieveFromCurrentState,
2619
+ isSaving: retrieveFromCurrentState,
2620
+ isDeleted: retrieveFromCurrentState,
2621
+ isError: retrieveFromCurrentState,
2622
+ isNew: retrieveFromCurrentState,
2623
+ isPending: retrieveFromCurrentState,
2624
+ isValid: retrieveFromCurrentState,
2625
+
2626
+ clientId: null,
2627
+ transaction: null,
2628
+ stateManager: null,
2629
+ pendingQueue: null,
2630
+ errors: null,
2631
+
2632
+ // because unknownProperty is used, any internal property
2633
+ // must be initialized here.
2634
+ primaryKey: 'id',
2635
+ id: Ember.computed(function(key, value) {
2636
+ var primaryKey = get(this, 'primaryKey'),
2637
+ data = get(this, 'data');
2638
+
2639
+ if (arguments.length === 2) {
2640
+ set(data, primaryKey, value);
2641
+ return value;
2642
+ }
2643
+
2644
+ return data && get(data, primaryKey);
2645
+ }).property('primaryKey', 'data'),
2646
+
2647
+ // The following methods are callbacks invoked by `toJSON`. You
2648
+ // can override one of the callbacks to override specific behavior,
2649
+ // or toJSON itself.
2650
+ //
2651
+ // If you override toJSON, you can invoke these callbacks manually
2652
+ // to get the default behavior.
2653
+
2654
+ /**
2655
+ Add the record's primary key to the JSON hash.
2656
+
2657
+ The default implementation uses the record's specified `primaryKey`
2658
+ and the `id` computed property, which are passed in as parameters.
2659
+
2660
+ @param {Object} json the JSON hash being built
2661
+ @param {Number|String} id the record's id
2662
+ @param {String} key the primaryKey for the record
2663
+ */
2664
+ addIdToJSON: function(json, id, key) {
2665
+ if (id) { json[key] = id; }
2666
+ },
2667
+
2668
+ /**
2669
+ Add the attributes' current values to the JSON hash.
2670
+
2671
+ The default implementation gets the current value of each
2672
+ attribute from the `data`, and uses a `defaultValue` if
2673
+ specified in the `DS.attr` definition.
2674
+
2675
+ @param {Object} json the JSON hash being build
2676
+ @param {Ember.Map} attributes a Map of attributes
2677
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2678
+ */
2679
+ addAttributesToJSON: function(json, attributes, data) {
2680
+ attributes.forEach(function(name, meta) {
2681
+ var key = meta.key(this.constructor),
2682
+ value = get(data, key);
2683
+
2684
+ if (value === undefined) {
2685
+ value = meta.options.defaultValue;
2686
+ }
2687
+
2688
+ json[key] = value;
2689
+ }, this);
2690
+ },
2691
+
2692
+ /**
2693
+ Add the value of a `hasMany` association to the JSON hash.
2694
+
2695
+ The default implementation honors the `embedded` option
2696
+ passed to `DS.hasMany`. If embedded, `toJSON` is recursively
2697
+ called on the child records. If not, the `id` of each
2698
+ record is added.
2699
+
2700
+ Note that if a record is not embedded and does not
2701
+ yet have an `id` (usually provided by the server), it
2702
+ will not be included in the output.
2703
+
2704
+ @param {Object} json the JSON hash being built
2705
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2706
+ @param {Object} meta information about the association
2707
+ @param {Object} options options passed to `toJSON`
2708
+ */
2709
+ addHasManyToJSON: function(json, data, meta, options) {
2710
+ var key = meta.key,
2711
+ manyArray = get(this, key),
2712
+ records = [], i, l,
2713
+ clientId, id;
2714
+
2715
+ if (meta.options.embedded) {
2716
+ // TODO: Avoid materializing embedded hashes if possible
2717
+ manyArray.forEach(function(record) {
2718
+ records.push(record.toJSON(options));
2719
+ });
2720
+ } else {
2721
+ var clientIds = get(manyArray, 'content');
2722
+
2723
+ for (i=0, l=clientIds.length; i<l; i++) {
2724
+ clientId = clientIds[i];
2725
+ id = get(this, 'store').clientIdToId[clientId];
2726
+
2727
+ if (id !== undefined) {
2728
+ records.push(id);
2729
+ }
2730
+ }
2731
+ }
2732
+
2733
+ key = meta.options.key || get(this, 'namingConvention').keyToJSONKey(key);
2734
+ json[key] = records;
2735
+ },
2736
+
2737
+ /**
2738
+ Add the value of a `belongsTo` association to the JSON hash.
2739
+
2740
+ The default implementation always includes the `id`.
2741
+
2742
+ @param {Object} json the JSON hash being built
2743
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2744
+ @param {Object} meta information about the association
2745
+ @param {Object} options options passed to `toJSON`
2746
+ */
2747
+ addBelongsToToJSON: function(json, data, meta, options) {
2748
+ var key = meta.key, value, id;
2749
+
2750
+ if (meta.options.embedded) {
2751
+ key = meta.options.key || get(this, 'namingConvention').keyToJSONKey(key);
2752
+ value = get(data.record, key);
2753
+ json[key] = value ? value.toJSON(options) : null;
2754
+ } else {
2755
+ key = meta.options.key || get(this, 'namingConvention').foreignKey(key);
2756
+ id = data.get(key);
2757
+ json[key] = none(id) ? null : id;
2758
+ }
2759
+ },
2760
+ /**
2761
+ Create a JSON representation of the record, including its `id`,
2762
+ attributes and associations. Honor any settings defined on the
2763
+ attributes or associations (such as `embedded` or `key`).
2764
+ */
2765
+ toJSON: function(options) {
2766
+ var data = get(this, 'data'),
2767
+ result = {},
2768
+ type = this.constructor,
2769
+ attributes = get(type, 'attributes'),
2770
+ primaryKey = get(this, 'primaryKey'),
2771
+ id = get(this, 'id'),
2772
+ store = get(this, 'store'),
2773
+ associations;
2774
+
2775
+ options = options || {};
2776
+
2777
+ // delegate to `addIdToJSON` callback
2778
+ this.addIdToJSON(result, id, primaryKey);
2779
+
2780
+ // delegate to `addAttributesToJSON` callback
2781
+ this.addAttributesToJSON(result, attributes, data);
2782
+
2783
+ associations = get(type, 'associationsByName');
2784
+
2785
+ // add associations, delegating to `addHasManyToJSON` and
2786
+ // `addBelongsToToJSON`.
2787
+ associations.forEach(function(key, meta) {
2788
+ if (options.associations && meta.kind === 'hasMany') {
2789
+ this.addHasManyToJSON(result, data, meta, options);
2790
+ } else if (meta.kind === 'belongsTo') {
2791
+ this.addBelongsToToJSON(result, data, meta, options);
2792
+ }
2793
+ }, this);
2794
+
2795
+ return result;
2796
+ },
2797
+
2798
+ data: Ember.computed(function() {
2799
+ return new DS._DataProxy(this);
2800
+ }).cacheable(),
2801
+
2802
+ didLoad: Ember.K,
2803
+ didUpdate: Ember.K,
2804
+ didCreate: Ember.K,
2805
+ didDelete: Ember.K,
2806
+ becameInvalid: Ember.K,
2807
+ becameError: Ember.K,
2808
+
2809
+ init: function() {
2810
+ var stateManager = DS.StateManager.create({
2811
+ record: this
2812
+ });
2813
+
2814
+ set(this, 'pendingQueue', {});
2815
+
2816
+ set(this, 'stateManager', stateManager);
2817
+ stateManager.goToState('empty');
2818
+ },
2819
+
2820
+ destroy: function() {
2821
+ if (!get(this, 'isDeleted')) {
2822
+ this.deleteRecord();
2823
+ }
2824
+ this._super();
2825
+ },
2826
+
2827
+ send: function(name, context) {
2828
+ return get(this, 'stateManager').send(name, context);
2829
+ },
2830
+
2831
+ withTransaction: function(fn) {
2832
+ var transaction = get(this, 'transaction');
2833
+ if (transaction) { fn(transaction); }
2834
+ },
2835
+
2836
+ setProperty: function(key, value) {
2837
+ this.send('setProperty', { key: key, value: value });
2838
+ },
2839
+
2840
+ deleteRecord: function() {
2841
+ this.send('deleteRecord');
2842
+ },
2843
+
2844
+ waitingOn: function(record) {
2845
+ this.send('waitingOn', record);
2846
+ },
2847
+
2848
+ notifyHashWasUpdated: function() {
2849
+ var store = get(this, 'store');
2850
+ if (store) {
2851
+ store.hashWasUpdated(this.constructor, get(this, 'clientId'), this);
2852
+ }
2853
+ },
2854
+
2855
+ unknownProperty: function(key) {
2856
+ var data = get(this, 'data');
2857
+
2858
+ if (data && key in data) {
2859
+
2860
+ }
2861
+ },
2862
+
2863
+ setUnknownProperty: function(key, value) {
2864
+ var data = get(this, 'data');
2865
+
2866
+ if (data && key in data) {
2867
+
2868
+ } else {
2869
+ return this._super(key, value);
2870
+ }
2871
+ },
2872
+
2873
+ namingConvention: {
2874
+ keyToJSONKey: function(key) {
2875
+ // TODO: Strip off `is` from the front. Example: `isHipster` becomes `hipster`
2876
+ return Ember.String.decamelize(key);
2877
+ },
2878
+
2879
+ foreignKey: function(key) {
2880
+ return Ember.String.decamelize(key) + '_id';
2881
+ }
2882
+ },
2883
+
2884
+ /** @private */
2885
+ hashWasUpdated: function() {
2886
+ // At the end of the run loop, notify record arrays that
2887
+ // this record has changed so they can re-evaluate its contents
2888
+ // to determine membership.
2889
+ Ember.run.once(this, this.notifyHashWasUpdated);
2890
+ },
2891
+
2892
+ dataDidChange: Ember.observer(function() {
2893
+ var associations = get(this.constructor, 'associationsByName'),
2894
+ data = get(this, 'data'), store = get(this, 'store'),
2895
+ idToClientId = store.idToClientId,
2896
+ cachedValue;
2897
+
2898
+ associations.forEach(function(name, association) {
2899
+ if (association.kind === 'hasMany') {
2900
+ cachedValue = this.cacheFor(name);
2901
+
2902
+ if (cachedValue) {
2903
+ var key = association.options.key || name,
2904
+ ids = data.get(key) || [];
2905
+ var clientIds = Ember.ArrayUtils.map(ids, function(id) {
2906
+ return store.clientIdForId(association.type, id);
2907
+ });
2908
+
2909
+ set(cachedValue, 'content', Ember.A(clientIds));
2910
+ cachedValue.fetch();
2911
+ }
2912
+ }
2913
+ }, this);
2914
+ }, 'data'),
2915
+
2916
+ /**
2917
+ @private
2918
+
2919
+ Override the default event firing from Ember.Evented to
2920
+ also call methods with the given name.
2921
+ */
2922
+ fire: function(name) {
2923
+ this[name].apply(this, [].slice.call(arguments, 1));
2924
+ this._super.apply(this, arguments);
2925
+ }
2926
+ });
2927
+
2928
+ // Helper function to generate store aliases.
2929
+ // This returns a function that invokes the named alias
2930
+ // on the default store, but injects the class as the
2931
+ // first parameter.
2932
+ var storeAlias = function(methodName) {
2933
+ return function() {
2934
+ var store = get(DS, 'defaultStore'),
2935
+ args = [].slice.call(arguments);
2936
+
2937
+ args.unshift(this);
2938
+ return store[methodName].apply(store, args);
2939
+ };
2940
+ };
2941
+
2942
+ DS.Model.reopenClass({
2943
+ find: storeAlias('find'),
2944
+ filter: storeAlias('filter'),
2945
+
2946
+ _create: DS.Model.create,
2947
+
2948
+ create: function() {
2949
+ throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set.");
2950
+ },
2951
+
2952
+ createRecord: storeAlias('createRecord')
2953
+ });
2954
+
2955
+ })();
2956
+
2957
+
2958
+
2959
+ (function() {
2960
+ var get = Ember.get, getPath = Ember.getPath;
2961
+ DS.Model.reopenClass({
2962
+ attributes: Ember.computed(function() {
2963
+ var map = Ember.Map.create();
2964
+
2965
+ this.eachComputedProperty(function(name, meta) {
2966
+ if (meta.isAttribute) { map.set(name, meta); }
2967
+ });
2968
+
2969
+ return map;
2970
+ }).cacheable(),
2971
+
2972
+ processAttributeKeys: function() {
2973
+ if (this.processedAttributeKeys) { return; }
2974
+
2975
+ var namingConvention = this.proto().namingConvention;
2976
+
2977
+ this.eachComputedProperty(function(name, meta) {
2978
+ if (meta.isAttribute && !meta.options.key) {
2979
+ meta.options.key = namingConvention.keyToJSONKey(name, this);
2980
+ }
2981
+ }, this);
2982
+ }
2983
+ });
2984
+
2985
+ DS.attr = function(type, options) {
2986
+ var transform = DS.attr.transforms[type];
2987
+
2988
+
2989
+ var transformFrom = transform.from;
2990
+ var transformTo = transform.to;
2991
+
2992
+ options = options || {};
2993
+
2994
+ var meta = {
2995
+ type: type,
2996
+ isAttribute: true,
2997
+ options: options,
2998
+
2999
+ // this will ensure that the key always takes naming
3000
+ // conventions into consideration.
3001
+ key: function(recordType) {
3002
+ recordType.processAttributeKeys();
3003
+ return options.key;
3004
+ }
3005
+ };
3006
+
3007
+ return Ember.computed(function(key, value) {
3008
+ var data;
3009
+
3010
+ key = meta.key(this.constructor);
3011
+
3012
+ if (arguments.length === 2) {
3013
+ value = transformTo(value);
3014
+ this.setProperty(key, value);
3015
+ } else {
3016
+ data = get(this, 'data');
3017
+ value = get(data, key);
3018
+
3019
+ if (value === undefined) {
3020
+ value = options.defaultValue;
3021
+ }
3022
+ }
3023
+
3024
+ return transformFrom(value);
3025
+ // `data` is never set directly. However, it may be
3026
+ // invalidated from the state manager's setData
3027
+ // event.
3028
+ }).property('data').cacheable().meta(meta);
3029
+ };
3030
+
3031
+ DS.attr.transforms = {
3032
+ string: {
3033
+ from: function(serialized) {
3034
+ return Ember.none(serialized) ? null : String(serialized);
3035
+ },
3036
+
3037
+ to: function(deserialized) {
3038
+ return Ember.none(deserialized) ? null : String(deserialized);
3039
+ }
3040
+ },
3041
+
3042
+ number: {
3043
+ from: function(serialized) {
3044
+ return Ember.none(serialized) ? null : Number(serialized);
3045
+ },
3046
+
3047
+ to: function(deserialized) {
3048
+ return Ember.none(deserialized) ? null : Number(deserialized);
3049
+ }
3050
+ },
3051
+
3052
+ 'boolean': {
3053
+ from: function(serialized) {
3054
+ return Boolean(serialized);
3055
+ },
3056
+
3057
+ to: function(deserialized) {
3058
+ return Boolean(deserialized);
3059
+ }
3060
+ },
3061
+
3062
+ date: {
3063
+ from: function(serialized) {
3064
+ var type = typeof serialized;
3065
+
3066
+ if (type === "string" || type === "number") {
3067
+ return new Date(serialized);
3068
+ } else if (serialized === null || serialized === undefined) {
3069
+ // if the value is not present in the data,
3070
+ // return undefined, not null.
3071
+ return serialized;
3072
+ } else {
3073
+ return null;
3074
+ }
3075
+ },
3076
+
3077
+ to: function(date) {
3078
+ if (date instanceof Date) {
3079
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3080
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
3081
+
3082
+ var pad = function(num) {
3083
+ return num < 10 ? "0"+num : ""+num;
3084
+ };
3085
+
3086
+ var utcYear = date.getUTCFullYear(),
3087
+ utcMonth = date.getUTCMonth(),
3088
+ utcDayOfMonth = date.getUTCDate(),
3089
+ utcDay = date.getUTCDay(),
3090
+ utcHours = date.getUTCHours(),
3091
+ utcMinutes = date.getUTCMinutes(),
3092
+ utcSeconds = date.getUTCSeconds();
3093
+
3094
+
3095
+ var dayOfWeek = days[utcDay];
3096
+ var dayOfMonth = pad(utcDayOfMonth);
3097
+ var month = months[utcMonth];
3098
+
3099
+ return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " +
3100
+ pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT";
3101
+ } else if (date === undefined) {
3102
+ return undefined;
3103
+ } else {
3104
+ return null;
3105
+ }
3106
+ }
3107
+ }
3108
+ };
3109
+
3110
+
3111
+ })();
3112
+
3113
+
3114
+
3115
+ (function() {
3116
+
3117
+ })();
3118
+
3119
+
3120
+
3121
+ (function() {
3122
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath,
3123
+ none = Ember.none;
3124
+
3125
+ var embeddedFindRecord = function(store, type, data, key, one) {
3126
+ var association = get(data, key);
3127
+ return none(association) ? undefined : store.load(type, association).id;
3128
+ };
3129
+
3130
+ var referencedFindRecord = function(store, type, data, key, one) {
3131
+ return get(data, key);
3132
+ };
3133
+
3134
+ var hasAssociation = function(type, options, one) {
3135
+ options = options || {};
3136
+
3137
+ var embedded = options.embedded,
3138
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
3139
+
3140
+ var meta = { type: type, isAssociation: true, options: options, kind: 'belongsTo' };
3141
+
3142
+ return Ember.computed(function(key, value) {
3143
+ var data = get(this, 'data'), ids, id, association,
3144
+ store = get(this, 'store');
3145
+
3146
+ if (typeof type === 'string') {
3147
+ type = getPath(this, type, false) || getPath(window, type);
3148
+ }
3149
+
3150
+ if (arguments.length === 2) {
3151
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
3152
+ this.send('setAssociation', { key: key, value: value === null ? null : get(value, 'clientId') });
3153
+ //data.setAssociation(key, get(value, 'clientId'));
3154
+ // put the client id in `key` in the data hash
3155
+ return value;
3156
+ } else {
3157
+ // Embedded belongsTo associations should not look for
3158
+ // a foreign key.
3159
+ if (embedded) {
3160
+ key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
3161
+
3162
+ // Non-embedded associations should look for a foreign key.
3163
+ // For example, instead of person, we might look for person_id
3164
+ } else {
3165
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
3166
+ }
3167
+ id = findRecord(store, type, data, key, true);
3168
+ association = id ? store.find(type, id) : null;
3169
+ }
3170
+
3171
+ return association;
3172
+ }).property('data').cacheable().meta(meta);
3173
+ };
3174
+
3175
+ DS.belongsTo = function(type, options) {
3176
+
3177
+ return hasAssociation(type, options);
3178
+ };
3179
+
3180
+ })();
3181
+
3182
+
3183
+
3184
+ (function() {
3185
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
3186
+ var embeddedFindRecord = function(store, type, data, key) {
3187
+ var association = get(data, key);
3188
+ return association ? store.loadMany(type, association).ids : [];
3189
+ };
3190
+
3191
+ var referencedFindRecord = function(store, type, data, key, one) {
3192
+ return get(data, key);
3193
+ };
3194
+
3195
+ var hasAssociation = function(type, options) {
3196
+ options = options || {};
3197
+
3198
+ var embedded = options.embedded,
3199
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
3200
+
3201
+ var meta = { type: type, isAssociation: true, options: options, kind: 'hasMany' };
3202
+
3203
+ return Ember.computed(function(key, value) {
3204
+ var data = get(this, 'data'),
3205
+ store = get(this, 'store'),
3206
+ ids, id, association;
3207
+
3208
+ if (typeof type === 'string') {
3209
+ type = getPath(this, type, false) || getPath(window, type);
3210
+ }
3211
+
3212
+ key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
3213
+ ids = findRecord(store, type, data, key);
3214
+ association = store.findMany(type, ids);
3215
+ set(association, 'parentRecord', this);
3216
+
3217
+ return association;
3218
+ }).property().cacheable().meta(meta);
3219
+ };
3220
+
3221
+ DS.hasMany = function(type, options) {
3222
+
3223
+ return hasAssociation(type, options);
3224
+ };
3225
+
3226
+ })();
3227
+
3228
+
3229
+
3230
+ (function() {
3231
+ var get = Ember.get, getPath = Ember.getPath;
3232
+
3233
+ DS.Model.reopenClass({
3234
+ typeForAssociation: function(name) {
3235
+ var association = get(this, 'associationsByName').get(name);
3236
+ return association && association.type;
3237
+ },
3238
+
3239
+ associations: Ember.computed(function() {
3240
+ var map = Ember.Map.create();
3241
+
3242
+ this.eachComputedProperty(function(name, meta) {
3243
+ if (meta.isAssociation) {
3244
+ var type = meta.type,
3245
+ typeList = map.get(type);
3246
+
3247
+ if (typeof type === 'string') {
3248
+ type = getPath(this, type, false) || getPath(window, type);
3249
+ meta.type = type;
3250
+ }
3251
+
3252
+ if (!typeList) {
3253
+ typeList = [];
3254
+ map.set(type, typeList);
3255
+ }
3256
+
3257
+ typeList.push({ name: name, kind: meta.kind });
3258
+ }
3259
+ });
3260
+
3261
+ return map;
3262
+ }).cacheable(),
3263
+
3264
+ associationsByName: Ember.computed(function() {
3265
+ var map = Ember.Map.create(), type;
3266
+
3267
+ this.eachComputedProperty(function(name, meta) {
3268
+ if (meta.isAssociation) {
3269
+ meta.key = name;
3270
+ type = meta.type;
3271
+
3272
+ if (typeof type === 'string') {
3273
+ type = getPath(this, type, false) || getPath(window, type);
3274
+ meta.type = type;
3275
+ }
3276
+
3277
+ map.set(name, meta);
3278
+ }
3279
+ });
3280
+
3281
+ return map;
3282
+ }).cacheable()
3283
+ });
3284
+
3285
+ })();
3286
+
3287
+
3288
+
3289
+ (function() {
3290
+
3291
+ })();
3292
+
3293
+
3294
+
3295
+ (function() {
3296
+ /**
3297
+ An adapter is an object that receives requests from a store and
3298
+ translates them into the appropriate action to take against your
3299
+ persistence layer. The persistence layer is usually an HTTP API, but may
3300
+ be anything, such as the browser's local storage.
3301
+
3302
+ ### Creating an Adapter
3303
+
3304
+ First, create a new subclass of `DS.Adapter`:
3305
+
3306
+ App.MyAdapter = DS.Adapter.extend({
3307
+ // ...your code here
3308
+ });
3309
+
3310
+ To tell your store which adapter to use, set its `adapter` property:
3311
+
3312
+ App.store = DS.Store.create({
3313
+ revision: 3,
3314
+ adapter: App.MyAdapter.create()
3315
+ });
3316
+
3317
+ `DS.Adapter` is an abstract base class that you should override in your
3318
+ application to customize it for your backend. The minimum set of methods
3319
+ that you should implement is:
3320
+
3321
+ * `find()`
3322
+ * `createRecord()`
3323
+ * `updateRecord()`
3324
+ * `deleteRecord()`
3325
+
3326
+ To improve the network performance of your application, you can optimize
3327
+ your adapter by overriding these lower-level methods:
3328
+
3329
+ * `findMany()`
3330
+ * `createRecords()`
3331
+ * `updateRecords()`
3332
+ * `deleteRecords()`
3333
+ * `commit()`
3334
+
3335
+ For more information about the adapter API, please see `README.md`.
3336
+ */
3337
+
3338
+ DS.Adapter = Ember.Object.extend({
3339
+ /**
3340
+ The `find()` method is invoked when the store is asked for a record that
3341
+ has not previously been loaded. In response to `find()` being called, you
3342
+ should query your persistence layer for a record with the given ID. Once
3343
+ found, you can asynchronously call the store's `load()` method to load
3344
+ the record.
3345
+
3346
+ Here is an example `find` implementation:
3347
+
3348
+ find: function(store, type, id) {
3349
+ var url = type.url;
3350
+ url = url.fmt(id);
3351
+
3352
+ jQuery.getJSON(url, function(data) {
3353
+ // data is a Hash of key/value pairs. If your server returns a
3354
+ // root, simply do something like:
3355
+ // store.load(type, id, data.person)
3356
+ store.load(type, id, data);
3357
+ });
3358
+ }
3359
+ */
3360
+ find: null,
3361
+
3362
+ /**
3363
+ If the globally unique IDs for your records should be generated on the client,
3364
+ implement the `generateIdForRecord()` method. This method will be invoked
3365
+ each time you create a new record, and the value returned from it will be
3366
+ assigned to the record's `primaryKey`.
3367
+
3368
+ Most traditional REST-like HTTP APIs will not use this method. Instead, the ID
3369
+ of the record will be set by the server, and your adapter will update the store
3370
+ with the new ID when it calls `didCreateRecord()`. Only implement this method if
3371
+ you intend to generate record IDs on the client-side.
3372
+
3373
+ The `generateIdForRecord()` method will be invoked with the requesting store as
3374
+ the first parameter and the newly created record as the second parameter:
3375
+
3376
+ generateIdForRecord: function(store, record) {
3377
+ var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision();
3378
+ return uuid;
3379
+ }
3380
+ */
3381
+ generateIdForRecord: null,
3382
+
3383
+ commit: function(store, commitDetails) {
3384
+ commitDetails.updated.eachType(function(type, array) {
3385
+ this.updateRecords(store, type, array.slice());
3386
+ }, this);
3387
+
3388
+ commitDetails.created.eachType(function(type, array) {
3389
+ this.createRecords(store, type, array.slice());
3390
+ }, this);
3391
+
3392
+ commitDetails.deleted.eachType(function(type, array) {
3393
+ this.deleteRecords(store, type, array.slice());
3394
+ }, this);
3395
+ },
3396
+
3397
+ createRecords: function(store, type, records) {
3398
+ records.forEach(function(record) {
3399
+ this.createRecord(store, type, record);
3400
+ }, this);
3401
+ },
3402
+
3403
+ updateRecords: function(store, type, records) {
3404
+ records.forEach(function(record) {
3405
+ this.updateRecord(store, type, record);
3406
+ }, this);
3407
+ },
3408
+
3409
+ deleteRecords: function(store, type, records) {
3410
+ records.forEach(function(record) {
3411
+ this.deleteRecord(store, type, record);
3412
+ }, this);
3413
+ },
3414
+
3415
+ findMany: function(store, type, ids) {
3416
+ ids.forEach(function(id) {
3417
+ this.find(store, type, id);
3418
+ }, this);
3419
+ }
3420
+ });
3421
+
3422
+ })();
3423
+
3424
+
3425
+
3426
+ (function() {
3427
+ var set = Ember.set;
3428
+
3429
+ Ember.onLoad('application', function(app) {
3430
+ app.registerInjection(function(app, stateManager, property) {
3431
+ if (property === 'Store') {
3432
+ set(stateManager, 'store', app[property].create());
3433
+ }
3434
+ });
3435
+ });
3436
+
3437
+ })();
3438
+
3439
+
3440
+
3441
+ (function() {
3442
+ DS.fixtureAdapter = DS.Adapter.create({
3443
+ find: function(store, type, id) {
3444
+ var fixtures = type.FIXTURES;
3445
+
3446
+ if (fixtures.hasLoaded) { return; }
3447
+
3448
+ setTimeout(function() {
3449
+ store.loadMany(type, fixtures);
3450
+ fixtures.hasLoaded = true;
3451
+ }, 300);
3452
+ },
3453
+
3454
+ findMany: function() {
3455
+ this.find.apply(this, arguments);
3456
+ },
3457
+
3458
+ findAll: function(store, type) {
3459
+ var fixtures = type.FIXTURES;
3460
+
3461
+
3462
+ var ids = fixtures.map(function(item, index, self){ return item.id; });
3463
+ store.loadMany(type, ids, fixtures);
3464
+ }
3465
+
3466
+ });
3467
+
3468
+ })();
3469
+
3470
+
3471
+
3472
+ (function() {
3473
+ /*global jQuery*/
3474
+
3475
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
3476
+
3477
+ DS.RESTAdapter = DS.Adapter.extend({
3478
+ bulkCommit: false,
3479
+
3480
+ createRecord: function(store, type, record) {
3481
+ var root = this.rootForType(type);
3482
+
3483
+ var data = {};
3484
+ data[root] = record.toJSON();
3485
+
3486
+ this.ajax(this.buildURL(root), "POST", {
3487
+ data: data,
3488
+ success: function(json) {
3489
+ this.sideload(store, type, json, root);
3490
+ store.didCreateRecord(record, json[root]);
3491
+ }
3492
+ });
3493
+ },
3494
+
3495
+ createRecords: function(store, type, records) {
3496
+ if (get(this, 'bulkCommit') === false) {
3497
+ return this._super(store, type, records);
3498
+ }
3499
+
3500
+ var root = this.rootForType(type),
3501
+ plural = this.pluralize(root);
3502
+
3503
+ var data = {};
3504
+ data[plural] = records.map(function(record) {
3505
+ return record.toJSON();
3506
+ });
3507
+
3508
+ this.ajax(this.buildURL(root), "POST", {
3509
+ data: data,
3510
+
3511
+ success: function(json) {
3512
+ this.sideload(store, type, json, plural);
3513
+ store.didCreateRecords(type, records, json[plural]);
3514
+ }
3515
+ });
3516
+ },
3517
+
3518
+ updateRecord: function(store, type, record) {
3519
+ var id = get(record, 'id');
3520
+ var root = this.rootForType(type);
3521
+
3522
+ var data = {};
3523
+ data[root] = record.toJSON();
3524
+
3525
+ this.ajax(this.buildURL(root, id), "PUT", {
3526
+ data: data,
3527
+ success: function(json) {
3528
+ this.sideload(store, type, json, root);
3529
+ store.didUpdateRecord(record, json && json[root]);
3530
+ }
3531
+ });
3532
+ },
3533
+
3534
+ updateRecords: function(store, type, records) {
3535
+ if (get(this, 'bulkCommit') === false) {
3536
+ return this._super(store, type, records);
3537
+ }
3538
+
3539
+ var root = this.rootForType(type),
3540
+ plural = this.pluralize(root);
3541
+
3542
+ var data = {};
3543
+ data[plural] = records.map(function(record) {
3544
+ return record.toJSON();
3545
+ });
3546
+
3547
+ this.ajax(this.buildURL(root, "bulk"), "PUT", {
3548
+ data: data,
3549
+ success: function(json) {
3550
+ this.sideload(store, type, json, plural);
3551
+ store.didUpdateRecords(records, json[plural]);
3552
+ }
3553
+ });
3554
+ },
3555
+
3556
+ deleteRecord: function(store, type, record) {
3557
+ var id = get(record, 'id');
3558
+ var root = this.rootForType(type);
3559
+
3560
+ this.ajax(this.buildURL(root, id), "DELETE", {
3561
+ success: function(json) {
3562
+ if (json) { this.sideload(store, type, json); }
3563
+ store.didDeleteRecord(record);
3564
+ }
3565
+ });
3566
+ },
3567
+
3568
+ deleteRecords: function(store, type, records) {
3569
+ if (get(this, 'bulkCommit') === false) {
3570
+ return this._super(store, type, records);
3571
+ }
3572
+
3573
+ var root = this.rootForType(type),
3574
+ plural = this.pluralize(root);
3575
+
3576
+ var data = {};
3577
+ data[plural] = records.map(function(record) {
3578
+ return get(record, 'id');
3579
+ });
3580
+
3581
+ this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
3582
+ data: data,
3583
+ success: function(json) {
3584
+ if (json) { this.sideload(store, type, json); }
3585
+ store.didDeleteRecords(records);
3586
+ }
3587
+ });
3588
+ },
3589
+
3590
+ find: function(store, type, id) {
3591
+ var root = this.rootForType(type);
3592
+
3593
+ this.ajax(this.buildURL(root, id), "GET", {
3594
+ success: function(json) {
3595
+ store.load(type, json[root]);
3596
+ this.sideload(store, type, json, root);
3597
+ }
3598
+ });
3599
+ },
3600
+
3601
+ findMany: function(store, type, ids) {
3602
+ var root = this.rootForType(type), plural = this.pluralize(root);
3603
+
3604
+ this.ajax(this.buildURL(root), "GET", {
3605
+ data: { ids: ids },
3606
+ success: function(json) {
3607
+ store.loadMany(type, json[plural]);
3608
+ this.sideload(store, type, json, plural);
3609
+ }
3610
+ });
3611
+ },
3612
+
3613
+ findAll: function(store, type) {
3614
+ var root = this.rootForType(type), plural = this.pluralize(root);
3615
+
3616
+ this.ajax(this.buildURL(root), "GET", {
3617
+ success: function(json) {
3618
+ store.loadMany(type, json[plural]);
3619
+ this.sideload(store, type, json, plural);
3620
+ }
3621
+ });
3622
+ },
3623
+
3624
+ findQuery: function(store, type, query, recordArray) {
3625
+ var root = this.rootForType(type), plural = this.pluralize(root);
3626
+
3627
+ this.ajax(this.buildURL(root), "GET", {
3628
+ data: query,
3629
+ success: function(json) {
3630
+ recordArray.load(json[plural]);
3631
+ this.sideload(store, type, json, plural);
3632
+ }
3633
+ });
3634
+ },
3635
+
3636
+ // HELPERS
3637
+
3638
+ plurals: {},
3639
+
3640
+ // define a plurals hash in your subclass to define
3641
+ // special-case pluralization
3642
+ pluralize: function(name) {
3643
+ return this.plurals[name] || name + "s";
3644
+ },
3645
+
3646
+ rootForType: function(type) {
3647
+ if (type.url) { return type.url; }
3648
+
3649
+ // use the last part of the name as the URL
3650
+ var parts = type.toString().split(".");
3651
+ var name = parts[parts.length - 1];
3652
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
3653
+ },
3654
+
3655
+ ajax: function(url, type, hash) {
3656
+ hash.url = url;
3657
+ hash.type = type;
3658
+ hash.dataType = 'json';
3659
+ hash.contentType = 'application/json; charset=utf-8';
3660
+ hash.context = this;
3661
+
3662
+ if (hash.data && type !== 'GET') {
3663
+ hash.data = JSON.stringify(hash.data);
3664
+ }
3665
+
3666
+ jQuery.ajax(hash);
3667
+ },
3668
+
3669
+ sideload: function(store, type, json, root) {
3670
+ var sideloadedType, mappings;
3671
+
3672
+ for (var prop in json) {
3673
+ if (!json.hasOwnProperty(prop)) { continue; }
3674
+ if (prop === root) { continue; }
3675
+
3676
+ sideloadedType = type.typeForAssociation(prop);
3677
+
3678
+ if (!sideloadedType) {
3679
+ mappings = get(this, 'mappings');
3680
+
3681
+
3682
+ sideloadedType = get(mappings, prop);
3683
+
3684
+ }
3685
+
3686
+ this.loadValue(store, sideloadedType, json[prop]);
3687
+ }
3688
+ },
3689
+
3690
+ loadValue: function(store, type, value) {
3691
+ if (value instanceof Array) {
3692
+ store.loadMany(type, value);
3693
+ } else {
3694
+ store.load(type, value);
3695
+ }
3696
+ },
3697
+
3698
+ buildURL: function(record, suffix) {
3699
+ var url = [""];
3700
+
3701
+
3702
+
3703
+
3704
+ if (this.namespace !== undefined) {
3705
+ url.push(this.namespace);
3706
+ }
3707
+
3708
+ url.push(this.pluralize(record));
3709
+ if (suffix !== undefined) {
3710
+ url.push(suffix);
3711
+ }
3712
+
3713
+ return url.join("/");
3714
+ }
3715
+ });
3716
+
3717
+
3718
+ })();
3719
+
3720
+
3721
+
3722
+ (function() {
3723
+ //Copyright (C) 2011 by Living Social, Inc.
3724
+
3725
+ //Permission is hereby granted, free of charge, to any person obtaining a copy of
3726
+ //this software and associated documentation files (the "Software"), to deal in
3727
+ //the Software without restriction, including without limitation the rights to
3728
+ //use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
3729
+ //of the Software, and to permit persons to whom the Software is furnished to do
3730
+ //so, subject to the following conditions:
3731
+
3732
+ //The above copyright notice and this permission notice shall be included in all
3733
+ //copies or substantial portions of the Software.
3734
+
3735
+ //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3736
+ //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3737
+ //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3738
+ //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3739
+ //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3740
+ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3741
+ //SOFTWARE.
3742
+
3743
+ })();
3744
+