ember-rails-lite 0.8.0

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