embient 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- embient (0.0.5)
4
+ embient (0.0.6)
5
5
  emberjs-rails
6
6
  rails (>= 3.1.0)
7
7
 
@@ -50,7 +50,7 @@ GEM
50
50
  i18n (0.6.0)
51
51
  journey (1.0.3)
52
52
  json (1.6.5)
53
- mail (2.4.1)
53
+ mail (2.4.3)
54
54
  i18n (>= 0.4.0)
55
55
  mime-types (~> 1.16)
56
56
  treetop (~> 1.4.8)
@@ -58,7 +58,7 @@ GEM
58
58
  multi_json (1.1.0)
59
59
  polyglot (0.3.3)
60
60
  rack (1.4.1)
61
- rack-cache (1.1)
61
+ rack-cache (1.2)
62
62
  rack (>= 0.4)
63
63
  rack-ssl (1.3.2)
64
64
  rack
@@ -91,7 +91,7 @@ GEM
91
91
  treetop (1.4.10)
92
92
  polyglot
93
93
  polyglot (>= 0.3.1)
94
- tzinfo (0.3.31)
94
+ tzinfo (0.3.32)
95
95
 
96
96
  PLATFORMS
97
97
  ruby
@@ -1,3 +1,3 @@
1
1
  module Embient
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
@@ -1,6 +1,8 @@
1
1
 
2
2
  (function(exports) {
3
- window.DS = Ember.Namespace.create();
3
+ window.DS = Ember.Namespace.create({
4
+ CURRENT_API_REVISION: 3
5
+ });
4
6
 
5
7
  })({});
6
8
 
@@ -89,11 +91,12 @@ DS.RESTAdapter = DS.Adapter.extend({
89
91
  var root = this.rootForType(type);
90
92
 
91
93
  var data = {};
92
- data[root] = get(model, 'data');
94
+ data[root] = model.toJSON();
93
95
 
94
- this.ajax("/" + this.pluralize(root), "POST", {
96
+ this.ajax(this.buildURL(root), "POST", {
95
97
  data: data,
96
98
  success: function(json) {
99
+ this.sideload(store, type, json, root);
97
100
  store.didCreateRecord(model, json[root]);
98
101
  }
99
102
  });
@@ -109,12 +112,14 @@ DS.RESTAdapter = DS.Adapter.extend({
109
112
 
110
113
  var data = {};
111
114
  data[plural] = models.map(function(model) {
112
- return get(model, 'data');
115
+ return model.toJSON();
113
116
  });
114
117
 
115
- this.ajax("/" + this.pluralize(root), "POST", {
118
+ this.ajax(this.buildURL(root), "POST", {
116
119
  data: data,
120
+
117
121
  success: function(json) {
122
+ this.sideload(store, type, json, plural);
118
123
  store.didCreateRecords(type, models, json[plural]);
119
124
  }
120
125
  });
@@ -125,14 +130,13 @@ DS.RESTAdapter = DS.Adapter.extend({
125
130
  var root = this.rootForType(type);
126
131
 
127
132
  var data = {};
128
- data[root] = get(model, 'data');
133
+ data[root] = model.toJSON();
129
134
 
130
- var url = ["", this.pluralize(root), id].join("/");
131
-
132
- this.ajax(url, "PUT", {
135
+ this.ajax(this.buildURL(root, id), "PUT", {
133
136
  data: data,
134
137
  success: function(json) {
135
- store.didUpdateRecord(model, json[root]);
138
+ this.sideload(store, type, json, root);
139
+ store.didUpdateRecord(model, json && json[root]);
136
140
  }
137
141
  });
138
142
  },
@@ -147,12 +151,13 @@ DS.RESTAdapter = DS.Adapter.extend({
147
151
 
148
152
  var data = {};
149
153
  data[plural] = models.map(function(model) {
150
- return get(model, 'data');
154
+ return model.toJSON();
151
155
  });
152
156
 
153
- this.ajax("/" + this.pluralize(root), "POST", {
157
+ this.ajax(this.buildURL(root, "bulk"), "PUT", {
154
158
  data: data,
155
159
  success: function(json) {
160
+ this.sideload(store, type, json, plural);
156
161
  store.didUpdateRecords(models, json[plural]);
157
162
  }
158
163
  });
@@ -162,10 +167,9 @@ DS.RESTAdapter = DS.Adapter.extend({
162
167
  var id = get(model, 'id');
163
168
  var root = this.rootForType(type);
164
169
 
165
- var url = ["", this.pluralize(root), id].join("/");
166
-
167
- this.ajax(url, "DELETE", {
170
+ this.ajax(this.buildURL(root, id), "DELETE", {
168
171
  success: function(json) {
172
+ if (json) { this.sideload(store, type, json); }
169
173
  store.didDeleteRecord(model);
170
174
  }
171
175
  });
@@ -184,9 +188,10 @@ DS.RESTAdapter = DS.Adapter.extend({
184
188
  return get(model, 'id');
185
189
  });
186
190
 
187
- this.ajax("/" + this.pluralize(root) + "/delete", "POST", {
191
+ this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
188
192
  data: data,
189
193
  success: function(json) {
194
+ if (json) { this.sideload(store, type, json); }
190
195
  store.didDeleteRecords(models);
191
196
  }
192
197
  });
@@ -195,11 +200,10 @@ DS.RESTAdapter = DS.Adapter.extend({
195
200
  find: function(store, type, id) {
196
201
  var root = this.rootForType(type);
197
202
 
198
- var url = ["", this.pluralize(root), id].join("/");
199
-
200
- this.ajax(url, "GET", {
203
+ this.ajax(this.buildURL(root, id), "GET", {
201
204
  success: function(json) {
202
205
  store.load(type, json[root]);
206
+ this.sideload(store, type, json, root);
203
207
  }
204
208
  });
205
209
  },
@@ -207,10 +211,11 @@ DS.RESTAdapter = DS.Adapter.extend({
207
211
  findMany: function(store, type, ids) {
208
212
  var root = this.rootForType(type), plural = this.pluralize(root);
209
213
 
210
- this.ajax("/" + plural, "GET", {
214
+ this.ajax(this.buildURL(root), "GET", {
211
215
  data: { ids: ids },
212
216
  success: function(json) {
213
217
  store.loadMany(type, ids, json[plural]);
218
+ this.sideload(store, type, json, plural);
214
219
  }
215
220
  });
216
221
  },
@@ -218,9 +223,10 @@ DS.RESTAdapter = DS.Adapter.extend({
218
223
  findAll: function(store, type) {
219
224
  var root = this.rootForType(type), plural = this.pluralize(root);
220
225
 
221
- this.ajax("/" + plural, "GET", {
226
+ this.ajax(this.buildURL(root), "GET", {
222
227
  success: function(json) {
223
228
  store.loadMany(type, json[plural]);
229
+ this.sideload(store, type, json, plural);
224
230
  }
225
231
  });
226
232
  },
@@ -228,10 +234,11 @@ DS.RESTAdapter = DS.Adapter.extend({
228
234
  findQuery: function(store, type, query, modelArray) {
229
235
  var root = this.rootForType(type), plural = this.pluralize(root);
230
236
 
231
- this.ajax("/" + plural, "GET", {
237
+ this.ajax(this.buildURL(root), "GET", {
232
238
  data: query,
233
239
  success: function(json) {
234
240
  modelArray.load(json[plural]);
241
+ this.sideload(store, type, json, plural);
235
242
  }
236
243
  });
237
244
  },
@@ -258,9 +265,61 @@ DS.RESTAdapter = DS.Adapter.extend({
258
265
  ajax: function(url, type, hash) {
259
266
  hash.url = url;
260
267
  hash.type = type;
261
- hash.dataType = "json";
268
+ hash.dataType = 'json';
269
+ hash.contentType = 'application/json';
270
+ hash.context = this;
271
+
272
+ if (hash.data && type !== 'GET') {
273
+ hash.data = JSON.stringify(hash.data);
274
+ }
262
275
 
263
276
  jQuery.ajax(hash);
277
+ },
278
+
279
+ sideload: function(store, type, json, root) {
280
+ var sideloadedType, mappings;
281
+
282
+ for (var prop in json) {
283
+ if (!json.hasOwnProperty(prop)) { continue; }
284
+ if (prop === root) { continue; }
285
+
286
+ sideloadedType = type.typeForAssociation(prop);
287
+
288
+ if (!sideloadedType) {
289
+ mappings = get(this, 'mappings');
290
+
291
+ ember_assert("Your server returned a hash with the key " + prop + " but you have no mappings", !!mappings);
292
+
293
+ sideloadedType = get(get(this, 'mappings'), prop);
294
+
295
+ ember_assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType);
296
+ }
297
+
298
+ this.loadValue(store, sideloadedType, json[prop]);
299
+ }
300
+ },
301
+
302
+ loadValue: function(store, type, value) {
303
+ if (value instanceof Array) {
304
+ store.loadMany(type, value);
305
+ } else {
306
+ store.load(type, value);
307
+ }
308
+ },
309
+
310
+ buildURL: function(model, suffix) {
311
+ var url = [""];
312
+
313
+ if (this.namespace !== undefined) {
314
+ url.push(this.namespace);
315
+ }
316
+
317
+ url.push(this.pluralize(model));
318
+ if (suffix !== undefined) {
319
+ url.push(suffix);
320
+ }
321
+
322
+ return url.join("/");
264
323
  }
265
324
  });
266
325
 
@@ -297,29 +356,11 @@ DS.ModelArray = Ember.ArrayProxy.extend({
297
356
  // The store that created this model array.
298
357
  store: null,
299
358
 
300
- // for associations, the model that this association belongs to.
301
- parentModel: null,
302
-
303
359
  init: function() {
304
360
  set(this, 'modelCache', Ember.A([]));
305
361
  this._super();
306
362
  },
307
363
 
308
- // Overrides Ember.Array's replace method to implement
309
- replace: function(index, removed, added) {
310
- var parentRecord = get(this, 'parentRecord');
311
- var pendingParent = parentRecord && !get(parentRecord, 'id');
312
-
313
- added = added.map(function(item) {
314
- ember_assert("You can only add items of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === item.constructor));
315
-
316
- if (pendingParent) { item.send('waitingOn', parentRecord); }
317
- return item.get('clientId');
318
- });
319
-
320
- this._super(index, removed, added);
321
- },
322
-
323
364
  arrayDidChange: function(array, index, removed, added) {
324
365
  var modelCache = get(this, 'modelCache');
325
366
  modelCache.replace(index, 0, new Array(added));
@@ -363,6 +404,11 @@ var get = Ember.get;
363
404
  DS.FilteredModelArray = DS.ModelArray.extend({
364
405
  filterFunction: null,
365
406
 
407
+ replace: function() {
408
+ var type = get(this, 'type').toString();
409
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
410
+ },
411
+
366
412
  updateFilter: Ember.observer(function() {
367
413
  var store = get(this, 'store');
368
414
  store.updateModelArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
@@ -379,6 +425,11 @@ DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
379
425
  query: null,
380
426
  isLoaded: false,
381
427
 
428
+ replace: function() {
429
+ var type = get(this, 'type').toString();
430
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
431
+ },
432
+
382
433
  load: function(array) {
383
434
  var store = get(this, 'store'), type = get(this, 'type');
384
435
 
@@ -396,168 +447,161 @@ DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
396
447
 
397
448
 
398
449
  (function(exports) {
399
- })({});
450
+ var get = Ember.get, set = Ember.set;
400
451
 
452
+ DS.ManyArray = DS.ModelArray.extend({
453
+ parentRecord: null,
401
454
 
402
- (function(exports) {
403
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
455
+ // Overrides Ember.Array's replace method to implement
456
+ replace: function(index, removed, added) {
457
+ var parentRecord = get(this, 'parentRecord');
458
+ var pendingParent = parentRecord && !get(parentRecord, 'id');
404
459
 
405
- var OrderedSet = Ember.Object.extend({
406
- init: function() {
407
- this.clear();
408
- },
460
+ added = added.map(function(record) {
461
+ ember_assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === record.constructor));
409
462
 
410
- clear: function() {
411
- this.set('presenceSet', {});
412
- this.set('list', Ember.NativeArray.apply([]));
413
- },
463
+ if (pendingParent) {
464
+ record.send('waitingOn', parentRecord);
465
+ }
414
466
 
415
- add: function(obj) {
416
- var guid = Ember.guidFor(obj),
417
- presenceSet = get(this, 'presenceSet'),
418
- list = get(this, 'list');
467
+ this.assignInverse(record, parentRecord);
419
468
 
420
- if (guid in presenceSet) { return; }
469
+ return record.get('clientId');
470
+ }, this);
421
471
 
422
- presenceSet[guid] = true;
423
- list.pushObject(obj);
472
+ this._super(index, removed, added);
424
473
  },
425
474
 
426
- remove: function(obj) {
427
- var guid = Ember.guidFor(obj),
428
- presenceSet = get(this, 'presenceSet'),
429
- list = get(this, 'list');
475
+ assignInverse: function(record, parentRecord) {
476
+ var associationMap = get(record.constructor, 'associations'),
477
+ possibleAssociations = associationMap.get(record.constructor),
478
+ possible, actual;
430
479
 
431
- delete presenceSet[guid];
432
- list.removeObject(obj);
433
- },
480
+ if (!possibleAssociations) { return; }
434
481
 
435
- isEmpty: function() {
436
- return getPath(this, 'list.length') === 0;
437
- },
482
+ for (var i = 0, l = possibleAssociations.length; i < l; i++) {
483
+ possible = possibleAssociations[i];
438
484
 
439
- forEach: function(fn, self) {
440
- // allow mutation during iteration
441
- get(this, 'list').slice().forEach(function(item) {
442
- fn.call(self, item);
443
- });
444
- },
485
+ if (possible.kind === 'belongsTo') {
486
+ actual = possible;
487
+ break;
488
+ }
489
+ }
445
490
 
446
- toArray: function() {
447
- return get(this, 'list').slice();
491
+ if (actual) {
492
+ set(record, actual.name, parentRecord);
493
+ }
448
494
  }
449
495
  });
450
496
 
451
- /**
452
- A Hash stores values indexed by keys. Unlike JavaScript's
453
- default Objects, the keys of a Hash can be any JavaScript
454
- object.
497
+ })({});
455
498
 
456
- Internally, a Hash has two data structures:
457
499
 
458
- `keys`: an OrderedSet of all of the existing keys
459
- `values`: a JavaScript Object indexed by the
460
- Ember.guidFor(key)
500
+ (function(exports) {
501
+ })({});
461
502
 
462
- When a key/value pair is added for the first time, we
463
- add the key to the `keys` OrderedSet, and create or
464
- replace an entry in `values`. When an entry is deleted,
465
- we delete its entry in `keys` and `values`.
466
- */
467
503
 
468
- var Hash = Ember.Object.extend({
504
+ (function(exports) {
505
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
506
+
507
+ DS.Transaction = Ember.Object.extend({
469
508
  init: function() {
470
- set(this, 'keys', OrderedSet.create());
471
- set(this, 'values', {});
509
+ set(this, 'buckets', {
510
+ clean: Ember.Map.create(),
511
+ created: Ember.Map.create(),
512
+ updated: Ember.Map.create(),
513
+ deleted: Ember.Map.create()
514
+ });
472
515
  },
473
516
 
474
- add: function(key, value) {
475
- var keys = get(this, 'keys'), values = get(this, 'values');
476
- var guid = Ember.guidFor(key);
477
-
478
- keys.add(key);
479
- values[guid] = value;
517
+ createRecord: function(type, hash) {
518
+ var store = get(this, 'store');
480
519
 
481
- return value;
520
+ return store.createRecord(type, hash, this);
482
521
  },
483
522
 
484
- remove: function(key) {
485
- var keys = get(this, 'keys'), values = get(this, 'values');
486
- var guid = Ember.guidFor(key), value;
523
+ add: function(record) {
524
+ // we could probably make this work if someone has a valid use case. Do you?
525
+ ember_assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
487
526
 
488
- keys.remove(key);
527
+ var modelTransaction = get(record, 'transaction'),
528
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
489
529
 
490
- value = values[guid];
491
- delete values[guid];
530
+ ember_assert("Models cannot belong to more than one transaction at a time.", modelTransaction === defaultTransaction);
492
531
 
493
- return value;
532
+ this.adoptRecord(record);
494
533
  },
495
534
 
496
- fetch: function(key) {
497
- var values = get(this, 'values');
498
- var guid = Ember.guidFor(key);
535
+ remove: function(record) {
536
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
499
537
 
500
- return values[guid];
538
+ defaultTransaction.adoptRecord(record);
501
539
  },
502
540
 
503
- forEach: function(fn, binding) {
504
- var keys = get(this, 'keys'),
505
- values = get(this, 'values');
541
+ /**
542
+ @private
506
543
 
507
- keys.forEach(function(key) {
508
- var guid = Ember.guidFor(key);
509
- fn.call(binding, key, values[guid]);
510
- });
511
- }
512
- });
544
+ This method moves a record into a different transaction without the normal
545
+ checks that ensure that the user is not doing something weird, like moving
546
+ a dirty record into a new transaction.
513
547
 
514
- DS.Transaction = Ember.Object.extend({
515
- init: function() {
516
- set(this, 'dirty', {
517
- created: Hash.create(),
518
- updated: Hash.create(),
519
- deleted: Hash.create()
520
- });
521
- },
548
+ It is designed for internal use, such as when we are moving a clean record
549
+ into a new transaction when the transaction is committed.
522
550
 
523
- createRecord: function(type, hash) {
524
- var store = get(this, 'store');
551
+ This method must not be called unless the record is clean.
552
+ */
553
+ adoptRecord: function(record) {
554
+ var oldTransaction = get(record, 'transaction');
525
555
 
526
- return store.createRecord(type, hash, this);
527
- },
556
+ if (oldTransaction) {
557
+ oldTransaction.removeFromBucket('clean', record);
558
+ }
528
559
 
529
- add: function(model) {
530
- var modelTransaction = get(model, 'transaction');
531
- ember_assert("Models cannot belong to more than one transaction at a time.", !modelTransaction);
560
+ this.addToBucket('clean', record);
561
+ set(record, 'transaction', this);
562
+ },
532
563
 
533
- set(model, 'transaction', this);
564
+ modelBecameDirty: function(kind, record) {
565
+ this.removeFromBucket('clean', record);
566
+ this.addToBucket(kind, record);
534
567
  },
535
568
 
536
- modelBecameDirty: function(kind, model) {
537
- var dirty = get(get(this, 'dirty'), kind),
538
- type = model.constructor;
569
+ /** @private */
570
+ addToBucket: function(kind, record) {
571
+ var bucket = get(get(this, 'buckets'), kind),
572
+ type = record.constructor;
573
+
574
+ var records = bucket.get(type);
539
575
 
540
- var models = dirty.fetch(type);
576
+ if (!records) {
577
+ records = Ember.OrderedSet.create();
578
+ bucket.set(type, records);
579
+ }
541
580
 
542
- models = models || dirty.add(type, OrderedSet.create());
543
- models.add(model);
581
+ records.add(record);
544
582
  },
545
583
 
546
- modelBecameClean: function(kind, model) {
547
- var dirty = get(get(this, 'dirty'), kind),
548
- type = model.constructor;
584
+ /** @private */
585
+ removeFromBucket: function(kind, record) {
586
+ var bucket = get(get(this, 'buckets'), kind),
587
+ type = record.constructor;
588
+
589
+ var records = bucket.get(type);
590
+ records.remove(record);
591
+ },
549
592
 
550
- var models = dirty.fetch(type);
551
- models.remove(model);
593
+ modelBecameClean: function(kind, record) {
594
+ this.removeFromBucket(kind, record);
552
595
 
553
- set(model, 'transaction', null);
596
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
597
+ defaultTransaction.adoptRecord(record);
554
598
  },
555
599
 
556
600
  commit: function() {
557
- var dirtyMap = get(this, 'dirty');
601
+ var buckets = get(this, 'buckets');
558
602
 
559
603
  var iterate = function(kind, fn, binding) {
560
- var dirty = get(dirtyMap, kind);
604
+ var dirty = get(buckets, kind);
561
605
 
562
606
  dirty.forEach(function(type, models) {
563
607
  if (models.isEmpty()) { return; }
@@ -592,6 +636,16 @@ DS.Transaction = Ember.Object.extend({
592
636
 
593
637
  var store = get(this, 'store');
594
638
  var adapter = get(store, '_adapter');
639
+
640
+ var clean = get(buckets, 'clean');
641
+ var defaultTransaction = get(store, 'defaultTransaction');
642
+
643
+ clean.forEach(function(type, records) {
644
+ records.forEach(function(record) {
645
+ this.remove(record);
646
+ }, this);
647
+ }, this);
648
+
595
649
  if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
596
650
  else { throw fmt("Adapter is either null or do not implement `commit` method", this); }
597
651
  }
@@ -603,46 +657,12 @@ DS.Transaction = Ember.Object.extend({
603
657
  (function(exports) {
604
658
  var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
605
659
 
606
- var OrderedSet = Ember.Object.extend({
607
- init: function() {
608
- this.clear();
609
- },
610
-
611
- clear: function() {
612
- this.set('presenceSet', {});
613
- this.set('list', Ember.NativeArray.apply([]));
614
- },
615
-
616
- add: function(obj) {
617
- var guid = Ember.guidFor(obj),
618
- presenceSet = get(this, 'presenceSet'),
619
- list = get(this, 'list');
620
-
621
- if (guid in presenceSet) { return; }
622
-
623
- presenceSet[guid] = true;
624
- list.pushObject(obj);
625
- },
626
-
627
- remove: function(obj) {
628
- var guid = Ember.guidFor(obj),
629
- presenceSet = get(this, 'presenceSet'),
630
- list = get(this, 'list');
631
-
632
- delete presenceSet[guid];
633
- list.removeObject(obj);
634
- },
635
-
636
- isEmpty: function() {
637
- return getPath(this, 'list.length') === 0;
638
- },
639
-
640
- forEach: function(fn, self) {
641
- get(this, 'list').forEach(function(item) {
642
- fn.call(self, item);
643
- });
660
+ var DATA_PROXY = {
661
+ get: function(name) {
662
+ return this.savedData[name];
644
663
  }
645
- });
664
+ };
665
+
646
666
 
647
667
  // Implementors Note:
648
668
  //
@@ -691,33 +711,49 @@ DS.Store = Ember.Object.extend({
691
711
  The init method registers this store as the default if none is specified.
692
712
  */
693
713
  init: function() {
714
+ // Enforce API revisioning. See BREAKING_CHANGES.md for more.
715
+ var revision = get(this, 'revision');
716
+
717
+ if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
718
+ 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);
719
+ }
720
+
694
721
  if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
695
722
  set(DS, 'defaultStore', this);
696
723
  }
697
724
 
698
- set(this, 'data', []);
699
- set(this, '_typeMap', {});
700
- set(this, 'models', []);
701
- set(this, 'modelArrays', []);
702
- set(this, 'modelArraysByClientId', {});
703
- set(this, 'defaultTransaction', DS.Transaction.create({ store: this }));
725
+ // internal bookkeeping; not observable
726
+ this.typeMaps = {};
727
+ this.recordCache = [];
728
+ this.clientIdToId = {};
729
+ this.modelArraysByClientId = {};
730
+
731
+ set(this, 'defaultTransaction', this.transaction());
704
732
 
705
733
  return this._super();
706
734
  },
707
735
 
736
+ /**
737
+ Returns a new transaction scoped to this store.
738
+
739
+ @see {DS.Transaction}
740
+ @returns DS.Transaction
741
+ */
708
742
  transaction: function() {
709
743
  return DS.Transaction.create({ store: this });
710
744
  },
711
745
 
712
- modelArraysForClientId: function(clientId) {
713
- var modelArrays = get(this, 'modelArraysByClientId');
714
- var ret = modelArrays[clientId];
746
+ /**
747
+ @private
715
748
 
716
- if (!ret) {
717
- ret = modelArrays[clientId] = OrderedSet.create();
718
- }
749
+ This is used only by the model's DataProxy. Do not use this directly.
750
+ */
751
+ dataForRecord: function(record) {
752
+ var type = record.constructor,
753
+ clientId = get(record, 'clientId'),
754
+ typeMap = this.typeMapFor(type);
719
755
 
720
- return ret;
756
+ return typeMap.cidToHash[clientId];
721
757
  },
722
758
 
723
759
  /**
@@ -730,6 +766,13 @@ DS.Store = Ember.Object.extend({
730
766
  */
731
767
  adapter: null,
732
768
 
769
+ /**
770
+ @private
771
+
772
+ This property returns the adapter, after resolving a possible String.
773
+
774
+ @returns DS.Adapter
775
+ */
733
776
  _adapter: Ember.computed(function() {
734
777
  var adapter = get(this, 'adapter');
735
778
  if (typeof adapter === 'string') {
@@ -738,44 +781,80 @@ DS.Store = Ember.Object.extend({
738
781
  return adapter;
739
782
  }).property('adapter').cacheable(),
740
783
 
784
+ // A monotonically increasing number to be used to uniquely identify
785
+ // data hashes and records.
741
786
  clientIdCounter: -1,
742
787
 
743
788
  // ....................
744
789
  // . CREATE NEW MODEL .
745
790
  // ....................
746
791
 
792
+ /**
793
+ Create a new record in the current store. The properties passed
794
+ to this method are set on the newly created record.
795
+
796
+ @param {subclass of DS.Model} type
797
+ @param {Object} properties a hash of properties to set on the
798
+ newly created record.
799
+ @returns DS.Model
800
+ */
747
801
  createRecord: function(type, properties, transaction) {
748
802
  properties = properties || {};
749
803
 
750
- var id = properties[getPath(type, 'proto.primaryKey')] || null;
751
-
752
- var model = type._create({
753
- store: this,
754
- transaction: transaction
804
+ // Create a new instance of the model `type` and put it
805
+ // into the specified `transaction`. If no transaction is
806
+ // specified, the default transaction will be used.
807
+ //
808
+ // NOTE: A `transaction` is specified when the
809
+ // `transaction.createRecord` API is used.
810
+ var record = type._create({
811
+ store: this
755
812
  });
756
813
 
814
+ transaction = transaction || get(this, 'defaultTransaction');
815
+ transaction.adoptRecord(record);
816
+
817
+ // Extract the primary key from the `properties` hash,
818
+ // based on the `primaryKey` for the model type.
819
+ var id = properties[get(record, 'primaryKey')] || null;
820
+
757
821
  var hash = {}, clientId;
758
822
 
823
+ // Push the hash into the store. If present, associate the
824
+ // extracted `id` with the hash.
759
825
  clientId = this.pushHash(hash, id, type);
760
- model.send('setData', hash);
761
826
 
762
- var models = get(this, 'models');
827
+ record.send('didChangeData');
763
828
 
764
- set(model, 'clientId', clientId);
765
- models[clientId] = model;
829
+ var recordCache = get(this, 'recordCache');
766
830
 
767
- model.setProperties(properties);
768
- this.updateModelArrays(type, clientId, hash);
831
+ // Now that we have a clientId, attach it to the record we
832
+ // just created.
833
+ set(record, 'clientId', clientId);
769
834
 
770
- return model;
835
+ // Store the record we just created in the record cache for
836
+ // this clientId.
837
+ recordCache[clientId] = record;
838
+
839
+ // Set the properties specified on the record.
840
+ record.setProperties(properties);
841
+
842
+ this.updateModelArrays(type, clientId, get(record, 'data'));
843
+
844
+ return record;
771
845
  },
772
846
 
773
847
  // ................
774
848
  // . DELETE MODEL .
775
849
  // ................
776
850
 
777
- deleteRecord: function(model) {
778
- model.send('deleteRecord');
851
+ /**
852
+ For symmetry, a record can be deleted via the store.
853
+
854
+ @param {DS.Model} record
855
+ */
856
+ deleteRecord: function(record) {
857
+ record.send('deleteRecord');
779
858
  },
780
859
 
781
860
  // ...............
@@ -783,22 +862,60 @@ DS.Store = Ember.Object.extend({
783
862
  // ...............
784
863
 
785
864
  /**
786
- Finds a model by its id. If the data for that model has already been
787
- loaded, an instance of DS.Model with that data will be returned
788
- immediately. Otherwise, an empty DS.Model instance will be returned in
789
- the loading state. As soon as the requested data is available, the model
790
- will be moved into the loaded state and all of the information will be
791
- available.
865
+ This is the main entry point into finding records. The first
866
+ parameter to this method is always a subclass of `DS.Model`.
792
867
 
793
- Note that only one DS.Model instance is ever created per unique id for a
794
- given type.
868
+ You can use the `find` method on a subclass of `DS.Model`
869
+ directly if your application only has one store. For
870
+ example, instead of `store.find(App.Person, 1)`, you could
871
+ say `App.Person.find(1)`.
795
872
 
796
- Example:
873
+ ---
797
874
 
798
- var record = MyApp.store.find(MyApp.Person, 1234);
875
+ To find a record by ID, pass the `id` as the second parameter:
799
876
 
800
- @param {DS.Model} type
801
- @param {String|Number} id
877
+ store.find(App.Person, 1);
878
+ App.Person.find(1);
879
+
880
+ If the record with that `id` had not previously been loaded,
881
+ the store will return an empty record immediately and ask
882
+ the adapter to find the data by calling its `find` method.
883
+
884
+ The `find` method will always return the same object for a
885
+ given type and `id`. To check whether the adapter has populated
886
+ a record, you can check its `isLoaded` property.
887
+
888
+ ---
889
+
890
+ To find all records for a type, call `find` with no additional
891
+ parameters:
892
+
893
+ store.find(App.Person);
894
+ App.Person.find();
895
+
896
+ This will return a `ModelArray` representing all known records
897
+ for the given type and kick off a request to the adapter's
898
+ `findAll` method to load any additional records for the type.
899
+
900
+ The `ModelArray` returned by `find()` is live. If any more
901
+ records for the type are added at a later time through any
902
+ mechanism, it will automatically update to reflect the change.
903
+
904
+ ---
905
+
906
+ To find a record by a query, call `find` with a hash as the
907
+ second parameter:
908
+
909
+ store.find(App.Person, { page: 1 });
910
+ App.Person.find({ page: 1 });
911
+
912
+ This will return a `ModelArray` immediately, but it will always
913
+ be an empty `ModelArray` at first. It will call the adapter's
914
+ `findQuery` method, which will populate the `ModelArray` once
915
+ the server has returned results.
916
+
917
+ You can check whether a query results `ModelArray` has loaded
918
+ by checking its `isLoaded` property.
802
919
  */
803
920
  find: function(type, id, query) {
804
921
  if (id === undefined) {
@@ -821,10 +938,9 @@ DS.Store = Ember.Object.extend({
821
938
  },
822
939
 
823
940
  findByClientId: function(type, clientId, id) {
824
- var model;
825
-
826
- var models = get(this, 'models');
827
- var data = this.clientIdToHashMap(type);
941
+ var recordCache = get(this, 'recordCache'),
942
+ dataCache = this.typeMapFor(type).cidToHash,
943
+ model;
828
944
 
829
945
  // If there is already a clientId assigned for this
830
946
  // type/id combination, try to find an existing
@@ -832,15 +948,16 @@ DS.Store = Ember.Object.extend({
832
948
  // materialize a new model and set its data to the
833
949
  // value we already have.
834
950
  if (clientId !== undefined) {
835
- model = models[clientId];
951
+ model = recordCache[clientId];
836
952
 
837
953
  if (!model) {
838
954
  // create a new instance of the model in the
839
955
  // 'isLoading' state
840
956
  model = this.materializeRecord(type, clientId);
841
957
 
842
- // immediately set its data
843
- model.send('setData', data[clientId] || null);
958
+ if (dataCache[clientId]) {
959
+ model.send('didChangeData');
960
+ }
844
961
  }
845
962
  } else {
846
963
  clientId = this.pushHash(null, id, type);
@@ -861,8 +978,10 @@ DS.Store = Ember.Object.extend({
861
978
  /** @private
862
979
  */
863
980
  findMany: function(type, ids, query) {
864
- var idToClientIdMap = this.idToClientIdMap(type);
865
- var data = this.clientIdToHashMap(type), needed;
981
+ var typeMap = this.typeMapFor(type),
982
+ idToClientIdMap = typeMap.idToCid,
983
+ data = typeMap.cidToHash,
984
+ needed;
866
985
 
867
986
  var clientIds = Ember.A([]);
868
987
 
@@ -888,7 +1007,7 @@ DS.Store = Ember.Object.extend({
888
1007
  else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
889
1008
  }
890
1009
 
891
- return this.createModelArray(type, clientIds);
1010
+ return this.createManyArray(type, clientIds);
892
1011
  },
893
1012
 
894
1013
  findQuery: function(type, query) {
@@ -916,7 +1035,14 @@ DS.Store = Ember.Object.extend({
916
1035
  return array;
917
1036
  },
918
1037
 
919
- filter: function(type, filter) {
1038
+ filter: function(type, query, filter) {
1039
+ // allow an optional server query
1040
+ if (arguments.length === 3) {
1041
+ this.findQuery(type, query);
1042
+ } else if (arguments.length === 2) {
1043
+ filter = query;
1044
+ }
1045
+
920
1046
  var array = DS.FilteredModelArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
921
1047
 
922
1048
  this.registerModelArray(array, type, filter);
@@ -928,11 +1054,8 @@ DS.Store = Ember.Object.extend({
928
1054
  // . UPDATING .
929
1055
  // ............
930
1056
 
931
- hashWasUpdated: function(type, clientId) {
932
- var clientIdToHashMap = this.clientIdToHashMap(type);
933
- var hash = clientIdToHashMap[clientId];
934
-
935
- this.updateModelArrays(type, clientId, hash);
1057
+ hashWasUpdated: function(type, clientId, record) {
1058
+ this.updateModelArrays(type, clientId, get(record, 'data'));
936
1059
  },
937
1060
 
938
1061
  // ..............
@@ -940,11 +1063,14 @@ DS.Store = Ember.Object.extend({
940
1063
  // ..............
941
1064
 
942
1065
  commit: function() {
943
- get(this, 'defaultTransaction').commit();
1066
+ var defaultTransaction = get(this, 'defaultTransaction');
1067
+ set(this, 'defaultTransaction', this.transaction());
1068
+
1069
+ defaultTransaction.commit();
944
1070
  },
945
1071
 
946
1072
  didUpdateRecords: function(array, hashes) {
947
- if (arguments.length === 2) {
1073
+ if (hashes) {
948
1074
  array.forEach(function(model, idx) {
949
1075
  this.didUpdateRecord(model, hashes[idx]);
950
1076
  }, this);
@@ -956,12 +1082,13 @@ DS.Store = Ember.Object.extend({
956
1082
  },
957
1083
 
958
1084
  didUpdateRecord: function(model, hash) {
959
- if (arguments.length === 2) {
960
- var clientId = get(model, 'clientId');
961
- var data = this.clientIdToHashMap(model.constructor);
1085
+ if (hash) {
1086
+ var clientId = get(model, 'clientId'),
1087
+ dataCache = this.typeMapFor(model.constructor).cidToHash;
962
1088
 
963
- data[clientId] = hash;
964
- model.send('setData', hash);
1089
+ dataCache[clientId] = hash;
1090
+ model.send('didChangeData');
1091
+ model.hashWasUpdated();
965
1092
  }
966
1093
 
967
1094
  model.send('didCommit');
@@ -977,41 +1104,54 @@ DS.Store = Ember.Object.extend({
977
1104
  model.send('didCommit');
978
1105
  },
979
1106
 
980
- didCreateRecords: function(type, array, hashes) {
981
- var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
1107
+ _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) {
1108
+ var recordData = get(record, 'data'), id, changes;
982
1109
 
983
- var idToClientIdMap = this.idToClientIdMap(type);
984
- var data = this.clientIdToHashMap(type);
985
- var idList = this.idList(type);
1110
+ if (hash) {
1111
+ typeMap.cidToHash[clientId] = hash;
1112
+
1113
+ // If the server returns a hash, we assume that the server's version
1114
+ // of the data supercedes the local changes.
1115
+ record.beginPropertyChanges();
1116
+ record.send('didChangeData');
1117
+ recordData.adapterDidUpdate(hash);
1118
+ record.hashWasUpdated();
1119
+ record.endPropertyChanges();
986
1120
 
987
- for (var i=0, l=get(array, 'length'); i<l; i++) {
988
- var model = array[i], hash = hashes[i];
989
1121
  id = hash[primaryKey];
990
- clientId = get(model, 'clientId');
991
1122
 
992
- data[clientId] = hash;
993
- model.send('setData', hash);
1123
+ typeMap.idToCid[id] = clientId;
1124
+ this.clientIdToId[clientId] = id;
1125
+ } else {
1126
+ recordData.commit();
1127
+ }
1128
+
1129
+ record.send('didCommit');
1130
+ },
994
1131
 
995
- idToClientIdMap[id] = clientId;
996
- idList.push(id);
997
1132
 
998
- model.send('didCommit');
1133
+ didCreateRecords: function(type, array, hashes) {
1134
+ var primaryKey = type.proto().primaryKey,
1135
+ typeMap = this.typeMapFor(type),
1136
+ id, clientId;
1137
+
1138
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
1139
+ var model = array[i], hash = hashes[i];
1140
+ clientId = get(model, 'clientId');
1141
+
1142
+ this._didCreateRecord(model, hash, typeMap, clientId, primaryKey);
999
1143
  }
1000
1144
  },
1001
1145
 
1002
1146
  didCreateRecord: function(model, hash) {
1003
- var type = model.constructor;
1004
-
1005
- var id, clientId, primaryKey;
1006
-
1007
- var idToClientIdMap = this.idToClientIdMap(type);
1008
- var data = this.clientIdToHashMap(type);
1009
- var idList = this.idList(type);
1147
+ var type = model.constructor,
1148
+ typeMap = this.typeMapFor(type),
1149
+ id, clientId, primaryKey;
1010
1150
 
1011
1151
  // The hash is optional, but if it is not provided, the client must have
1012
1152
  // provided a primary key.
1013
1153
 
1014
- primaryKey = getPath(type, 'proto.primaryKey');
1154
+ primaryKey = type.proto().primaryKey;
1015
1155
 
1016
1156
  // TODO: Make ember_assert more flexible and convert this into an ember_assert
1017
1157
  if (hash) {
@@ -1020,21 +1160,9 @@ DS.Store = Ember.Object.extend({
1020
1160
  ember_assert("The server did not return data, and you did not create a primary key (" + primaryKey + ") on the client", get(get(model, 'data'), primaryKey));
1021
1161
  }
1022
1162
 
1023
- // If a hash was provided, index it under the model's client ID
1024
- // and update the model.
1025
- if (arguments.length === 2) {
1026
- id = hash[primaryKey];
1027
-
1028
- data[clientId] = hash;
1029
- set(model, 'data', hash);
1030
- }
1031
-
1032
1163
  clientId = get(model, 'clientId');
1033
1164
 
1034
- idToClientIdMap[id] = clientId;
1035
- idList.push(id);
1036
-
1037
- model.send('didCommit');
1165
+ this._didCreateRecord(model, hash, typeMap, clientId, primaryKey);
1038
1166
  },
1039
1167
 
1040
1168
  recordWasInvalid: function(record, errors) {
@@ -1046,15 +1174,15 @@ DS.Store = Ember.Object.extend({
1046
1174
  // ................
1047
1175
 
1048
1176
  registerModelArray: function(array, type, filter) {
1049
- var modelArrays = get(this, 'modelArrays');
1177
+ var modelArrays = this.typeMapFor(type).modelArrays;
1050
1178
 
1051
1179
  modelArrays.push(array);
1052
1180
 
1053
1181
  this.updateModelArrayFilter(array, type, filter);
1054
1182
  },
1055
1183
 
1056
- createModelArray: function(type, clientIds) {
1057
- var array = DS.ModelArray.create({ type: type, content: clientIds, store: this });
1184
+ createManyArray: function(type, clientIds) {
1185
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
1058
1186
 
1059
1187
  clientIds.forEach(function(clientId) {
1060
1188
  var modelArrays = this.modelArraysForClientId(clientId);
@@ -1065,41 +1193,46 @@ DS.Store = Ember.Object.extend({
1065
1193
  },
1066
1194
 
1067
1195
  updateModelArrayFilter: function(array, type, filter) {
1068
- var data = this.clientIdToHashMap(type);
1069
- var allClientIds = this.clientIdList(type), clientId, hash;
1196
+ var typeMap = this.typeMapFor(type),
1197
+ dataCache = typeMap.cidToHash,
1198
+ clientIds = typeMap.clientIds,
1199
+ clientId, hash, proxy;
1200
+
1201
+ var recordCache = get(this, 'recordCache'), record;
1070
1202
 
1071
- for (var i=0, l=allClientIds.length; i<l; i++) {
1072
- clientId = allClientIds[i];
1203
+ for (var i=0, l=clientIds.length; i<l; i++) {
1204
+ clientId = clientIds[i];
1073
1205
 
1074
- hash = data[clientId];
1206
+ if (hash = dataCache[clientId]) {
1207
+ if (record = recordCache[clientId]) {
1208
+ proxy = get(record, 'data');
1209
+ } else {
1210
+ DATA_PROXY.savedData = hash;
1211
+ proxy = DATA_PROXY;
1212
+ }
1075
1213
 
1076
- if (hash) {
1077
- this.updateModelArray(array, filter, type, clientId, hash);
1214
+ this.updateModelArray(array, filter, type, clientId, proxy);
1078
1215
  }
1079
1216
  }
1080
1217
  },
1081
1218
 
1082
- updateModelArrays: function(type, clientId, hash) {
1083
- var modelArrays = get(this, 'modelArrays'),
1219
+ updateModelArrays: function(type, clientId, dataProxy) {
1220
+ var modelArrays = this.typeMapFor(type).modelArrays,
1084
1221
  modelArrayType, filter;
1085
1222
 
1086
1223
  modelArrays.forEach(function(array) {
1087
- modelArrayType = get(array, 'type');
1088
1224
  filter = get(array, 'filterFunction');
1089
-
1090
- if (type !== modelArrayType) { return; }
1091
-
1092
- this.updateModelArray(array, filter, type, clientId, hash);
1225
+ this.updateModelArray(array, filter, type, clientId, dataProxy);
1093
1226
  }, this);
1094
1227
  },
1095
1228
 
1096
- updateModelArray: function(array, filter, type, clientId, hash) {
1229
+ updateModelArray: function(array, filter, type, clientId, dataProxy) {
1097
1230
  var shouldBeInArray;
1098
1231
 
1099
1232
  if (!filter) {
1100
1233
  shouldBeInArray = true;
1101
1234
  } else {
1102
- shouldBeInArray = filter(hash);
1235
+ shouldBeInArray = filter(dataProxy);
1103
1236
  }
1104
1237
 
1105
1238
  var content = get(array, 'content');
@@ -1127,44 +1260,39 @@ DS.Store = Ember.Object.extend({
1127
1260
  },
1128
1261
 
1129
1262
  // ............
1130
- // . TYPE MAP .
1263
+ // . INDEXING .
1131
1264
  // ............
1132
1265
 
1266
+ modelArraysForClientId: function(clientId) {
1267
+ var modelArrays = get(this, 'modelArraysByClientId');
1268
+ var ret = modelArrays[clientId];
1269
+
1270
+ if (!ret) {
1271
+ ret = modelArrays[clientId] = Ember.OrderedSet.create();
1272
+ }
1273
+
1274
+ return ret;
1275
+ },
1276
+
1133
1277
  typeMapFor: function(type) {
1134
- var ids = get(this, '_typeMap');
1278
+ var typeMaps = get(this, 'typeMaps');
1135
1279
  var guidForType = Ember.guidFor(type);
1136
1280
 
1137
- var typeMap = ids[guidForType];
1281
+ var typeMap = typeMaps[guidForType];
1138
1282
 
1139
1283
  if (typeMap) {
1140
1284
  return typeMap;
1141
1285
  } else {
1142
- return (ids[guidForType] =
1286
+ return (typeMaps[guidForType] =
1143
1287
  {
1144
1288
  idToCid: {},
1145
- idList: [],
1146
- cidList: [],
1147
- cidToHash: {}
1289
+ clientIds: [],
1290
+ cidToHash: {},
1291
+ modelArrays: []
1148
1292
  });
1149
1293
  }
1150
1294
  },
1151
1295
 
1152
- idToClientIdMap: function(type) {
1153
- return this.typeMapFor(type).idToCid;
1154
- },
1155
-
1156
- idList: function(type) {
1157
- return this.typeMapFor(type).idList;
1158
- },
1159
-
1160
- clientIdList: function(type) {
1161
- return this.typeMapFor(type).cidList;
1162
- },
1163
-
1164
- clientIdToHashMap: function(type) {
1165
- return this.typeMapFor(type).cidToHash;
1166
- },
1167
-
1168
1296
  /** @private
1169
1297
 
1170
1298
  For a given type and id combination, returns the client id used by the store.
@@ -1177,13 +1305,6 @@ DS.Store = Ember.Object.extend({
1177
1305
  return this.typeMapFor(type).idToCid[id];
1178
1306
  },
1179
1307
 
1180
- idForHash: function(type, hash) {
1181
- var primaryKey = getPath(type, 'proto.primaryKey');
1182
-
1183
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1184
- return hash[primaryKey];
1185
- },
1186
-
1187
1308
  // ................
1188
1309
  // . LOADING DATA .
1189
1310
  // ................
@@ -1203,28 +1324,29 @@ DS.Store = Ember.Object.extend({
1203
1324
  load: function(type, id, hash) {
1204
1325
  if (hash === undefined) {
1205
1326
  hash = id;
1206
- var primaryKey = getPath(type, 'proto.primaryKey');
1207
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1327
+ var primaryKey = type.proto().primaryKey;
1328
+ ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", primaryKey in hash);
1208
1329
  id = hash[primaryKey];
1209
1330
  }
1210
1331
 
1211
- var data = this.clientIdToHashMap(type);
1212
- var models = get(this, 'models');
1213
-
1214
- var clientId = this.clientIdForId(type, id);
1332
+ var typeMap = this.typeMapFor(type),
1333
+ dataCache = typeMap.cidToHash,
1334
+ clientId = typeMap.idToCid[id],
1335
+ recordCache = get(this, 'recordCache');
1215
1336
 
1216
1337
  if (clientId !== undefined) {
1217
- data[clientId] = hash;
1338
+ dataCache[clientId] = hash;
1218
1339
 
1219
- var model = models[clientId];
1340
+ var model = recordCache[clientId];
1220
1341
  if (model) {
1221
- model.send('setData', hash);
1342
+ model.send('didChangeData');
1222
1343
  }
1223
1344
  } else {
1224
1345
  clientId = this.pushHash(hash, id, type);
1225
1346
  }
1226
1347
 
1227
- this.updateModelArrays(type, clientId, hash);
1348
+ DATA_PROXY.savedData = hash;
1349
+ this.updateModelArrays(type, clientId, DATA_PROXY);
1228
1350
 
1229
1351
  return { id: id, clientId: clientId };
1230
1352
  },
@@ -1235,10 +1357,9 @@ DS.Store = Ember.Object.extend({
1235
1357
  if (hashes === undefined) {
1236
1358
  hashes = ids;
1237
1359
  ids = [];
1238
- var primaryKey = getPath(type, 'proto.primaryKey');
1360
+ var primaryKey = type.proto().primaryKey;
1239
1361
 
1240
- ids = hashes.map(function(hash) {
1241
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1362
+ ids = Ember.ArrayUtils.map(hashes, function(hash) {
1242
1363
  return hash[primaryKey];
1243
1364
  });
1244
1365
  }
@@ -1262,23 +1383,25 @@ DS.Store = Ember.Object.extend({
1262
1383
  @returns {Number}
1263
1384
  */
1264
1385
  pushHash: function(hash, id, type) {
1265
- var idToClientIdMap = this.idToClientIdMap(type);
1266
- var clientIdList = this.clientIdList(type);
1267
- var idList = this.idList(type);
1268
- var data = this.clientIdToHashMap(type);
1386
+ var typeMap = this.typeMapFor(type);
1387
+
1388
+ var idToClientIdMap = typeMap.idToCid,
1389
+ clientIdToIdMap = this.clientIdToId,
1390
+ clientIds = typeMap.clientIds,
1391
+ dataCache = typeMap.cidToHash;
1269
1392
 
1270
- var clientId = this.incrementProperty('clientIdCounter');
1393
+ var clientId = ++this.clientIdCounter;
1271
1394
 
1272
- data[clientId] = hash;
1395
+ dataCache[clientId] = hash;
1273
1396
 
1274
1397
  // if we're creating an item, this process will be done
1275
1398
  // later, once the object has been persisted.
1276
1399
  if (id) {
1277
1400
  idToClientIdMap[id] = clientId;
1278
- idList.push(id);
1401
+ clientIdToIdMap[clientId] = id;
1279
1402
  }
1280
1403
 
1281
- clientIdList.push(clientId);
1404
+ clientIds.push(clientId);
1282
1405
 
1283
1406
  return clientId;
1284
1407
  },
@@ -1290,8 +1413,13 @@ DS.Store = Ember.Object.extend({
1290
1413
  materializeRecord: function(type, clientId) {
1291
1414
  var model;
1292
1415
 
1293
- get(this, 'models')[clientId] = model = type._create({ store: this, clientId: clientId });
1294
- set(model, 'clientId', clientId);
1416
+ get(this, 'recordCache')[clientId] = model = type._create({
1417
+ store: this,
1418
+ clientId: clientId
1419
+ });
1420
+
1421
+ get(this, 'defaultTransaction').adoptRecord(model);
1422
+
1295
1423
  model.send('loadingData');
1296
1424
  return model;
1297
1425
  },
@@ -1305,7 +1433,6 @@ DS.Store = Ember.Object.extend({
1305
1433
  }
1306
1434
  });
1307
1435
 
1308
-
1309
1436
  })({});
1310
1437
 
1311
1438
 
@@ -1327,6 +1454,14 @@ var isEmptyObject = function(object) {
1327
1454
  return true;
1328
1455
  };
1329
1456
 
1457
+ var hasDefinedProperties = function(object) {
1458
+ for (var name in object) {
1459
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
1460
+ }
1461
+
1462
+ return false;
1463
+ };
1464
+
1330
1465
  DS.State = Ember.State.extend({
1331
1466
  isLoaded: stateProperty,
1332
1467
  isDirty: stateProperty,
@@ -1344,27 +1479,21 @@ DS.State = Ember.State.extend({
1344
1479
  dirtyType: stateProperty
1345
1480
  });
1346
1481
 
1347
- var isEmptyObject = function(obj) {
1348
- for (var prop in obj) {
1349
- if (!obj.hasOwnProperty(prop)) { continue; }
1350
- return false;
1351
- }
1352
-
1353
- return true;
1354
- };
1355
-
1356
1482
  var setProperty = function(manager, context) {
1357
1483
  var key = context.key, value = context.value;
1358
1484
 
1359
1485
  var model = get(manager, 'model'),
1360
1486
  data = get(model, 'data');
1361
1487
 
1362
- data[key] = value;
1488
+ set(data, key, value);
1489
+ };
1490
+
1491
+ var didChangeData = function(manager) {
1492
+ var model = get(manager, 'model'),
1493
+ data = get(model, 'data');
1363
1494
 
1364
- // At the end of the run loop, notify model arrays that
1365
- // this record has changed so they can re-evaluate its contents
1366
- // to determine membership.
1367
- Ember.run.once(model, model.notifyHashWasUpdated);
1495
+ data._savedData = null;
1496
+ model.notifyPropertyChange('data');
1368
1497
  };
1369
1498
 
1370
1499
  // The waitingOn event shares common functionality
@@ -1425,6 +1554,48 @@ var waitingOn = function(manager, object) {
1425
1554
  // `isPending` property on all children will become `false`
1426
1555
  // and the transaction will try to commit the records.
1427
1556
 
1557
+ // This mixin is mixed into various uncommitted states. Make
1558
+ // sure to mix it in *after* the class definition, so its
1559
+ // super points to the class definition.
1560
+ var Uncommitted = Ember.Mixin.create({
1561
+ setProperty: setProperty,
1562
+
1563
+ deleteRecord: function(manager) {
1564
+ this._super(manager);
1565
+
1566
+ var model = get(manager, 'model'),
1567
+ dirtyType = get(this, 'dirtyType');
1568
+
1569
+ model.withTransaction(function(t) {
1570
+ t.modelBecameClean(dirtyType, model);
1571
+ });
1572
+ }
1573
+ });
1574
+
1575
+ // These mixins are mixed into substates of the concrete
1576
+ // subclasses of DirtyState.
1577
+
1578
+ var CreatedUncommitted = Ember.Mixin.create({
1579
+ deleteRecord: function(manager) {
1580
+ this._super(manager);
1581
+
1582
+ manager.goToState('deleted.saved');
1583
+ }
1584
+ });
1585
+
1586
+ var UpdatedUncommitted = Ember.Mixin.create({
1587
+ deleteRecord: function(manager) {
1588
+ this._super(manager);
1589
+
1590
+ var model = get(manager, 'model');
1591
+
1592
+ model.withTransaction(function(t) {
1593
+ t.modelBecameClean('created', model);
1594
+ });
1595
+
1596
+ manager.goToState('deleted');
1597
+ }
1598
+ });
1428
1599
 
1429
1600
  // The dirty state is a abstract state whose functionality is
1430
1601
  // shared between the `created` and `updated` states.
@@ -1460,11 +1631,7 @@ var DirtyState = DS.State.extend({
1460
1631
  },
1461
1632
 
1462
1633
  // EVENTS
1463
- setProperty: setProperty,
1464
-
1465
- deleteRecord: function(manager) {
1466
- manager.goToState('deleted');
1467
- },
1634
+ deleteRecord: Ember.K,
1468
1635
 
1469
1636
  waitingOn: function(manager, object) {
1470
1637
  waitingOn(manager, object);
@@ -1474,7 +1641,7 @@ var DirtyState = DS.State.extend({
1474
1641
  willCommit: function(manager) {
1475
1642
  manager.goToState('inFlight');
1476
1643
  }
1477
- }),
1644
+ }, Uncommitted),
1478
1645
 
1479
1646
  // Once a record has been handed off to the adapter to be
1480
1647
  // saved, it is in the 'in flight' state. Changes to the
@@ -1505,10 +1672,7 @@ var DirtyState = DS.State.extend({
1505
1672
  manager.goToState('invalid');
1506
1673
  },
1507
1674
 
1508
- setData: function(manager, hash) {
1509
- var model = get(manager, 'model');
1510
- set(model, 'data', hash);
1511
- }
1675
+ didChangeData: didChangeData
1512
1676
  }),
1513
1677
 
1514
1678
  // If a record becomes associated with a newly created
@@ -1533,8 +1697,6 @@ var DirtyState = DS.State.extend({
1533
1697
  // started to commit is in this state.
1534
1698
  uncommitted: DS.State.extend({
1535
1699
  // EVENTS
1536
- setProperty: setProperty,
1537
-
1538
1700
  deleteRecord: function(manager) {
1539
1701
  var model = get(manager, 'model'),
1540
1702
  pendingQueue = get(model, 'pendingQueue'),
@@ -1548,8 +1710,6 @@ var DirtyState = DS.State.extend({
1548
1710
  tuple = pendingQueue[prop];
1549
1711
  Ember.removeObserver(tuple[0], 'id', tuple[1]);
1550
1712
  }
1551
-
1552
- manager.goToState('deleted');
1553
1713
  },
1554
1714
 
1555
1715
  willCommit: function(manager) {
@@ -1572,7 +1732,7 @@ var DirtyState = DS.State.extend({
1572
1732
  var dirtyType = get(this, 'dirtyType');
1573
1733
  manager.goToState(dirtyType + '.uncommitted');
1574
1734
  }
1575
- }),
1735
+ }, Uncommitted),
1576
1736
 
1577
1737
  // A pending record whose transaction has started
1578
1738
  // to commit is in this state. Since it has not yet
@@ -1596,6 +1756,15 @@ var DirtyState = DS.State.extend({
1596
1756
  },
1597
1757
 
1598
1758
  doneWaiting: function(manager) {
1759
+ var model = get(manager, 'model'),
1760
+ transaction = get(model, 'transaction');
1761
+
1762
+ // Now that the model is no longer pending, schedule
1763
+ // the transaction to commit.
1764
+ Ember.run.once(transaction, transaction.commit);
1765
+ },
1766
+
1767
+ willCommit: function(manager) {
1599
1768
  var dirtyType = get(this, 'dirtyType');
1600
1769
  manager.goToState(dirtyType + '.inFlight');
1601
1770
  }
@@ -1623,7 +1792,7 @@ var DirtyState = DS.State.extend({
1623
1792
 
1624
1793
  delete errors[key];
1625
1794
 
1626
- if (isEmptyObject(errors)) {
1795
+ if (!hasDefinedProperties(errors)) {
1627
1796
  manager.send('becameValid');
1628
1797
  }
1629
1798
  },
@@ -1634,6 +1803,41 @@ var DirtyState = DS.State.extend({
1634
1803
  })
1635
1804
  });
1636
1805
 
1806
+ // The created and updated states are created outside the state
1807
+ // chart so we can reopen their substates and add mixins as
1808
+ // necessary.
1809
+
1810
+ var createdState = DirtyState.create({
1811
+ dirtyType: 'created',
1812
+
1813
+ // FLAGS
1814
+ isNew: true,
1815
+
1816
+ // EVENTS
1817
+ invokeLifecycleCallbacks: function(manager, model) {
1818
+ model.didCreate();
1819
+ }
1820
+ });
1821
+
1822
+ var updatedState = DirtyState.create({
1823
+ dirtyType: 'updated',
1824
+
1825
+ // EVENTS
1826
+ invokeLifecycleCallbacks: function(manager, model) {
1827
+ model.didUpdate();
1828
+ }
1829
+ });
1830
+
1831
+ // The created.uncommitted state and created.pending.uncommitted share
1832
+ // some logic defined in CreatedUncommitted.
1833
+ createdState.states.uncommitted.reopen(CreatedUncommitted);
1834
+ createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
1835
+
1836
+ // The updated.uncommitted state and updated.pending.uncommitted share
1837
+ // some logic defined in UpdatedUncommitted.
1838
+ updatedState.states.uncommitted.reopen(UpdatedUncommitted);
1839
+ updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
1840
+
1637
1841
  var states = {
1638
1842
  rootState: Ember.State.create({
1639
1843
  // FLAGS
@@ -1659,9 +1863,9 @@ var states = {
1659
1863
  manager.goToState('loading');
1660
1864
  },
1661
1865
 
1662
- setData: function(manager, hash) {
1663
- var model = get(manager, 'model');
1664
- set(model, 'data', hash);
1866
+ didChangeData: function(manager) {
1867
+ didChangeData(manager);
1868
+
1665
1869
  manager.goToState('loaded.created');
1666
1870
  }
1667
1871
  }),
@@ -1680,17 +1884,9 @@ var states = {
1680
1884
  },
1681
1885
 
1682
1886
  // EVENTS
1683
- setData: function(manager, data) {
1684
- var model = get(manager, 'model');
1685
-
1686
- model.beginPropertyChanges();
1687
- set(model, 'data', data);
1688
-
1689
- if (data !== null) {
1690
- manager.send('loadedData');
1691
- }
1692
-
1693
- model.endPropertyChanges();
1887
+ didChangeData: function(manager, data) {
1888
+ didChangeData(manager);
1889
+ manager.send('loadedData');
1694
1890
  },
1695
1891
 
1696
1892
  loadedData: function(manager) {
@@ -1718,6 +1914,8 @@ var states = {
1718
1914
  manager.goToState('updated');
1719
1915
  },
1720
1916
 
1917
+ didChangeData: didChangeData,
1918
+
1721
1919
  deleteRecord: function(manager) {
1722
1920
  manager.goToState('deleted');
1723
1921
  },
@@ -1731,29 +1929,12 @@ var states = {
1731
1929
  // A record is in this state after it has been locally
1732
1930
  // created but before the adapter has indicated that
1733
1931
  // it has been saved.
1734
- created: DirtyState.create({
1735
- dirtyType: 'created',
1736
-
1737
- // FLAGS
1738
- isNew: true,
1739
-
1740
- // EVENTS
1741
- invokeLifecycleCallbacks: function(manager, model) {
1742
- model.didCreate();
1743
- }
1744
- }),
1932
+ created: createdState,
1745
1933
 
1746
1934
  // A record is in this state if it has already been
1747
1935
  // saved to the server, but there are new local changes
1748
1936
  // that have not yet been saved.
1749
- updated: DirtyState.create({
1750
- dirtyType: 'updated',
1751
-
1752
- // EVENTS
1753
- invokeLifecycleCallbacks: function(manager, model) {
1754
- model.didUpdate();
1755
- }
1756
- })
1937
+ updated: updatedState
1757
1938
  }),
1758
1939
 
1759
1940
  // A record is in this state if it was deleted from the store.
@@ -1763,26 +1944,27 @@ var states = {
1763
1944
  isLoaded: true,
1764
1945
  isDirty: true,
1765
1946
 
1766
- // TRANSITIONS
1767
- enter: function(manager) {
1768
- var model = get(manager, 'model');
1769
- var store = get(model, 'store');
1770
-
1771
- if (store) {
1772
- store.removeFromModelArrays(model);
1773
- }
1774
-
1775
- model.withTransaction(function(t) {
1776
- t.modelBecameDirty('deleted', model);
1777
- });
1778
- },
1779
-
1780
1947
  // SUBSTATES
1781
1948
 
1782
1949
  // When a record is deleted, it enters the `start`
1783
1950
  // state. It will exit this state when the record's
1784
1951
  // transaction starts to commit.
1785
1952
  start: DS.State.create({
1953
+ // TRANSITIONS
1954
+ enter: function(manager) {
1955
+ var model = get(manager, 'model');
1956
+ var store = get(model, 'store');
1957
+
1958
+ if (store) {
1959
+ store.removeFromModelArrays(model);
1960
+ }
1961
+
1962
+ model.withTransaction(function(t) {
1963
+ t.modelBecameDirty('deleted', model);
1964
+ });
1965
+ },
1966
+
1967
+ // EVENTS
1786
1968
  willCommit: function(manager) {
1787
1969
  manager.goToState('inFlight');
1788
1970
  }
@@ -1815,6 +1997,7 @@ var states = {
1815
1997
  // been saved, the record enters the `saved` substate
1816
1998
  // of `deleted`.
1817
1999
  saved: DS.State.create({
2000
+ // FLAGS
1818
2001
  isDirty: false
1819
2002
  })
1820
2003
  }),
@@ -1838,12 +2021,103 @@ DS.StateManager = Ember.StateManager.extend({
1838
2021
 
1839
2022
 
1840
2023
  (function(exports) {
1841
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
2024
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none;
1842
2025
 
1843
2026
  var retrieveFromCurrentState = Ember.computed(function(key) {
1844
2027
  return get(getPath(this, 'stateManager.currentState'), key);
1845
2028
  }).property('stateManager.currentState').cacheable();
1846
2029
 
2030
+ // This object is a regular JS object for performance. It is only
2031
+ // used internally for bookkeeping purposes.
2032
+ var DataProxy = function(record) {
2033
+ this.record = record;
2034
+ this.unsavedData = {};
2035
+ this.associations = {};
2036
+ };
2037
+
2038
+ DataProxy.prototype = {
2039
+ get: function(key) { return Ember.get(this, key); },
2040
+ set: function(key, value) { return Ember.set(this, key, value); },
2041
+
2042
+ setAssociation: function(key, value) {
2043
+ this.associations[key] = value;
2044
+ },
2045
+
2046
+ savedData: function() {
2047
+ var savedData = this._savedData;
2048
+ if (savedData) { return savedData; }
2049
+
2050
+ var record = this.record,
2051
+ clientId = get(record, 'clientId'),
2052
+ store = get(record, 'store');
2053
+
2054
+ if (store) {
2055
+ savedData = store.dataForRecord(record);
2056
+ this._savedData = savedData;
2057
+ return savedData;
2058
+ }
2059
+ },
2060
+
2061
+ unknownProperty: function(key) {
2062
+ var unsavedData = this.unsavedData,
2063
+ associations = this.associations,
2064
+ savedData = this.savedData(),
2065
+ store;
2066
+
2067
+ var value = unsavedData[key], association;
2068
+
2069
+ // if this is a belongsTo association, this will
2070
+ // be a clientId.
2071
+ association = associations[key];
2072
+
2073
+ if (association !== undefined) {
2074
+ store = get(this.record, 'store');
2075
+ return store.clientIdToId[association];
2076
+ }
2077
+
2078
+ if (savedData && value === undefined) {
2079
+ value = savedData[key];
2080
+ }
2081
+
2082
+ return value;
2083
+ },
2084
+
2085
+ setUnknownProperty: function(key, value) {
2086
+ var record = this.record,
2087
+ unsavedData = this.unsavedData;
2088
+
2089
+ unsavedData[key] = value;
2090
+
2091
+ record.hashWasUpdated();
2092
+
2093
+ return value;
2094
+ },
2095
+
2096
+ commit: function() {
2097
+ var record = this.record;
2098
+
2099
+ var unsavedData = this.unsavedData;
2100
+ var savedData = this.savedData();
2101
+
2102
+ for (var prop in unsavedData) {
2103
+ if (unsavedData.hasOwnProperty(prop)) {
2104
+ savedData[prop] = unsavedData[prop];
2105
+ delete unsavedData[prop];
2106
+ }
2107
+ }
2108
+
2109
+ record.notifyPropertyChange('data');
2110
+ },
2111
+
2112
+ rollback: function() {
2113
+ this.unsavedData = {};
2114
+ },
2115
+
2116
+ adapterDidUpdate: function(data) {
2117
+ this.unsavedData = {};
2118
+ }
2119
+ };
2120
+
1847
2121
  DS.Model = Ember.Object.extend({
1848
2122
  isLoaded: retrieveFromCurrentState,
1849
2123
  isDirty: retrieveFromCurrentState,
@@ -1855,6 +2129,10 @@ DS.Model = Ember.Object.extend({
1855
2129
  isValid: retrieveFromCurrentState,
1856
2130
 
1857
2131
  clientId: null,
2132
+ transaction: null,
2133
+ stateManager: null,
2134
+ pendingQueue: null,
2135
+ errors: null,
1858
2136
 
1859
2137
  // because unknownProperty is used, any internal property
1860
2138
  // must be initialized here.
@@ -1871,10 +2149,159 @@ DS.Model = Ember.Object.extend({
1871
2149
  return data && get(data, primaryKey);
1872
2150
  }).property('primaryKey', 'data'),
1873
2151
 
1874
- data: null,
1875
- pendingQueue: null,
1876
- transaction: null,
1877
- errors: null,
2152
+ // The following methods are callbacks invoked by `toJSON`. You
2153
+ // can override one of the callbacks to override specific behavior,
2154
+ // or toJSON itself.
2155
+ //
2156
+ // If you override toJSON, you can invoke these callbacks manually
2157
+ // to get the default behavior.
2158
+
2159
+ /**
2160
+ Add the record's primary key to the JSON hash.
2161
+
2162
+ The default implementation uses the record's specified `primaryKey`
2163
+ and the `id` computed property, which are passed in as parameters.
2164
+
2165
+ @param {Object} json the JSON hash being built
2166
+ @param {Number|String} id the record's id
2167
+ @param {String} key the primaryKey for the record
2168
+ */
2169
+ addIdToJSON: function(json, id, key) {
2170
+ if (id) { json[key] = id; }
2171
+ },
2172
+
2173
+ /**
2174
+ Add the attributes' current values to the JSON hash.
2175
+
2176
+ The default implementation gets the current value of each
2177
+ attribute from the `data`, and uses a `defaultValue` if
2178
+ specified in the `DS.attr` definition.
2179
+
2180
+ @param {Object} json the JSON hash being build
2181
+ @param {Ember.Map} attributes a Map of attributes
2182
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2183
+ */
2184
+ addAttributesToJSON: function(json, attributes, data) {
2185
+ attributes.forEach(function(name, meta) {
2186
+ var key = meta.key(this.constructor),
2187
+ value = get(data, key);
2188
+
2189
+ if (value === undefined) {
2190
+ value = meta.options.defaultValue;
2191
+ }
2192
+
2193
+ json[key] = value;
2194
+ }, this);
2195
+ },
2196
+
2197
+ /**
2198
+ Add the value of a `hasMany` association to the JSON hash.
2199
+
2200
+ The default implementation honors the `embedded` option
2201
+ passed to `DS.hasMany`. If embedded, `toJSON` is recursively
2202
+ called on the child records. If not, the `id` of each
2203
+ record is added.
2204
+
2205
+ Note that if a record is not embedded and does not
2206
+ yet have an `id` (usually provided by the server), it
2207
+ will not be included in the output.
2208
+
2209
+ @param {Object} json the JSON hash being built
2210
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2211
+ @param {Object} meta information about the association
2212
+ @param {Object} options options passed to `toJSON`
2213
+ */
2214
+ addHasManyToJSON: function(json, data, meta, options) {
2215
+ var key = meta.key,
2216
+ manyArray = get(this, key),
2217
+ records = [],
2218
+ clientId, id;
2219
+
2220
+ if (meta.options.embedded) {
2221
+ // TODO: Avoid materializing embedded hashes if possible
2222
+ manyArray.forEach(function(record) {
2223
+ records.push(record.toJSON(options));
2224
+ });
2225
+ } else {
2226
+ var clientIds = get(manyArray, 'content');
2227
+
2228
+ for (var i=0, l=clientIds.length; i<l; i++) {
2229
+ clientId = clientIds[i];
2230
+ id = get(this, 'store').clientIdToId[clientId];
2231
+
2232
+ if (id !== undefined) {
2233
+ records.push(id);
2234
+ }
2235
+ }
2236
+ }
2237
+
2238
+ json[key] = records;
2239
+ },
2240
+
2241
+ /**
2242
+ Add the value of a `belongsTo` association to the JSON hash.
2243
+
2244
+ The default implementation always includes the `id`.
2245
+
2246
+ @param {Object} json the JSON hash being built
2247
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2248
+ @param {Object} meta information about the association
2249
+ @param {Object} options options passed to `toJSON`
2250
+ */
2251
+ addBelongsToToJSON: function(json, data, meta, options) {
2252
+ var key = meta.key, value, id;
2253
+
2254
+ if (options.embedded) {
2255
+ key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
2256
+ value = get(data.record, key);
2257
+ json[key] = value ? value.toJSON(options) : null;
2258
+ } else {
2259
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
2260
+ id = data.get(key);
2261
+ json[key] = none(id) ? null : id;
2262
+ }
2263
+ },
2264
+ /**
2265
+ Create a JSON representation of the record, including its `id`,
2266
+ attributes and associations. Honor any settings defined on the
2267
+ attributes or associations (such as `embedded` or `key`).
2268
+ */
2269
+ toJSON: function(options) {
2270
+ var data = get(this, 'data'),
2271
+ result = {},
2272
+ type = this.constructor,
2273
+ attributes = get(type, 'attributes'),
2274
+ primaryKey = get(this, 'primaryKey'),
2275
+ id = get(this, 'id'),
2276
+ store = get(this, 'store'),
2277
+ associations;
2278
+
2279
+ options = options || {};
2280
+
2281
+ // delegate to `addIdToJSON` callback
2282
+ this.addIdToJSON(result, id, primaryKey);
2283
+
2284
+ // delegate to `addAttributesToJSON` callback
2285
+ this.addAttributesToJSON(result, attributes, data);
2286
+
2287
+ associations = get(type, 'associationsByName');
2288
+
2289
+ // add associations, delegating to `addHasManyToJSON` and
2290
+ // `addBelongsToToJSON`.
2291
+ associations.forEach(function(key, meta) {
2292
+ if (options.associations && meta.kind === 'hasMany') {
2293
+ this.addHasManyToJSON(result, data, meta, options);
2294
+ } else if (meta.kind === 'belongsTo') {
2295
+ this.addBelongsToToJSON(result, data, meta, options);
2296
+ }
2297
+ }, this);
2298
+
2299
+ return result;
2300
+ },
2301
+
2302
+ data: Ember.computed(function() {
2303
+ return new DataProxy(this);
2304
+ }).cacheable(),
1878
2305
 
1879
2306
  didLoad: Ember.K,
1880
2307
  didUpdate: Ember.K,
@@ -1886,6 +2313,7 @@ DS.Model = Ember.Object.extend({
1886
2313
  });
1887
2314
 
1888
2315
  set(this, 'pendingQueue', {});
2316
+
1889
2317
  set(this, 'stateManager', stateManager);
1890
2318
  stateManager.goToState('empty');
1891
2319
  },
@@ -1902,7 +2330,7 @@ DS.Model = Ember.Object.extend({
1902
2330
  },
1903
2331
 
1904
2332
  withTransaction: function(fn) {
1905
- var transaction = get(this, 'transaction') || getPath(this, 'store.defaultTransaction');
2333
+ var transaction = get(this, 'transaction');
1906
2334
  if (transaction) { fn(transaction); }
1907
2335
  },
1908
2336
 
@@ -1921,7 +2349,7 @@ DS.Model = Ember.Object.extend({
1921
2349
  notifyHashWasUpdated: function() {
1922
2350
  var store = get(this, 'store');
1923
2351
  if (store) {
1924
- store.hashWasUpdated(this.constructor, get(this, 'clientId'));
2352
+ store.hashWasUpdated(this.constructor, get(this, 'clientId'), this);
1925
2353
  }
1926
2354
  },
1927
2355
 
@@ -1941,6 +2369,25 @@ DS.Model = Ember.Object.extend({
1941
2369
  } else {
1942
2370
  return this._super(key, value);
1943
2371
  }
2372
+ },
2373
+
2374
+ namingConvention: {
2375
+ keyToJSONKey: function(key) {
2376
+ // TODO: Strip off `is` from the front. Example: `isHipster` becomes `hipster`
2377
+ return Ember.String.decamelize(key);
2378
+ },
2379
+
2380
+ foreignKey: function(key) {
2381
+ return Ember.String.decamelize(key) + '_id';
2382
+ }
2383
+ },
2384
+
2385
+ /** @private */
2386
+ hashWasUpdated: function() {
2387
+ // At the end of the run loop, notify model arrays that
2388
+ // this record has changed so they can re-evaluate its contents
2389
+ // to determine membership.
2390
+ Ember.run.once(this, this.notifyHashWasUpdated);
1944
2391
  }
1945
2392
  });
1946
2393
 
@@ -1976,6 +2423,30 @@ DS.Model.reopenClass({
1976
2423
 
1977
2424
  (function(exports) {
1978
2425
  var get = Ember.get, getPath = Ember.getPath;
2426
+ DS.Model.reopenClass({
2427
+ attributes: Ember.computed(function() {
2428
+ var map = Ember.Map.create();
2429
+
2430
+ this.eachComputedProperty(function(name, meta) {
2431
+ if (meta.isAttribute) { map.set(name, meta); }
2432
+ });
2433
+
2434
+ return map;
2435
+ }).cacheable(),
2436
+
2437
+ processAttributeKeys: function() {
2438
+ if (this.processedAttributeKeys) { return; }
2439
+
2440
+ var namingConvention = this.proto().namingConvention;
2441
+
2442
+ this.eachComputedProperty(function(name, meta) {
2443
+ if (meta.isAttribute && !meta.options.key) {
2444
+ meta.options.key = namingConvention.keyToJSONKey(name, this);
2445
+ }
2446
+ }, this);
2447
+ }
2448
+ });
2449
+
1979
2450
  DS.attr = function(type, options) {
1980
2451
  var transform = DS.attr.transforms[type];
1981
2452
  ember_assert("Could not find model attribute of type " + type, !!transform);
@@ -1983,24 +2454,45 @@ DS.attr = function(type, options) {
1983
2454
  var transformFrom = transform.from;
1984
2455
  var transformTo = transform.to;
1985
2456
 
1986
- return Ember.computed(function(key, value) {
1987
- var data = get(this, 'data');
2457
+ options = options || {};
1988
2458
 
1989
- key = (options && options.key) ? options.key : key;
2459
+ var meta = {
2460
+ type: type,
2461
+ isAttribute: true,
2462
+ options: options,
2463
+
2464
+ // this will ensure that the key always takes naming
2465
+ // conventions into consideration.
2466
+ key: function(recordType) {
2467
+ recordType.processAttributeKeys();
2468
+ return options.key;
2469
+ }
2470
+ };
1990
2471
 
1991
- if (value === undefined) {
1992
- if (!data) { return; }
2472
+ return Ember.computed(function(key, value) {
2473
+ var data;
1993
2474
 
1994
- return transformFrom(data[key]);
1995
- } else {
1996
- ember_assert("You cannot set a model attribute before its data is loaded.", !!data);
2475
+ key = meta.key(this.constructor);
1997
2476
 
2477
+ if (arguments.length === 2) {
1998
2478
  value = transformTo(value);
1999
2479
  this.setProperty(key, value);
2000
- return value;
2480
+ } else {
2481
+ data = get(this, 'data');
2482
+ value = get(data, key);
2483
+
2484
+ if (value === undefined) {
2485
+ value = options.defaultValue;
2486
+ }
2001
2487
  }
2002
- }).property('data');
2488
+
2489
+ return transformFrom(value);
2490
+ // `data` is never set directly. However, it may be
2491
+ // invalidated from the state manager's setData
2492
+ // event.
2493
+ }).property('data').cacheable().meta(meta);
2003
2494
  };
2495
+
2004
2496
  DS.attr.transforms = {
2005
2497
  string: {
2006
2498
  from: function(serialized) {
@@ -2012,7 +2504,7 @@ DS.attr.transforms = {
2012
2504
  }
2013
2505
  },
2014
2506
 
2015
- integer: {
2507
+ number: {
2016
2508
  from: function(serialized) {
2017
2509
  return Ember.none(serialized) ? null : Number(serialized);
2018
2510
  },
@@ -2022,7 +2514,7 @@ DS.attr.transforms = {
2022
2514
  }
2023
2515
  },
2024
2516
 
2025
- boolean: {
2517
+ 'boolean': {
2026
2518
  from: function(serialized) {
2027
2519
  return Boolean(serialized);
2028
2520
  },
@@ -2087,13 +2579,55 @@ DS.attr.transforms = {
2087
2579
  (function(exports) {
2088
2580
  var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
2089
2581
  DS.Model.reopenClass({
2090
- typeForAssociation: function(association) {
2091
- var type = this.metaForProperty(association).type;
2092
- if (typeof type === 'string') {
2093
- type = getPath(this, type, false) || getPath(window, type);
2094
- }
2095
- return type;
2096
- }
2582
+ typeForAssociation: function(name) {
2583
+ var association = get(this, 'associationsByName').get(name);
2584
+ return association && association.type;
2585
+ },
2586
+
2587
+ associations: Ember.computed(function() {
2588
+ var map = Ember.Map.create();
2589
+
2590
+ this.eachComputedProperty(function(name, meta) {
2591
+ if (meta.isAssociation) {
2592
+ var type = meta.type,
2593
+ typeList = map.get(type);
2594
+
2595
+ if (typeof type === 'string') {
2596
+ type = getPath(this, type, false) || getPath(window, type);
2597
+ meta.type = type;
2598
+ }
2599
+
2600
+ if (!typeList) {
2601
+ typeList = [];
2602
+ map.set(type, typeList);
2603
+ }
2604
+
2605
+ typeList.push({ name: name, kind: meta.kind });
2606
+ }
2607
+ });
2608
+
2609
+ return map;
2610
+ }).cacheable(),
2611
+
2612
+ associationsByName: Ember.computed(function() {
2613
+ var map = Ember.Map.create(), type;
2614
+
2615
+ this.eachComputedProperty(function(name, meta) {
2616
+ if (meta.isAssociation) {
2617
+ meta.key = name;
2618
+ type = meta.type;
2619
+
2620
+ if (typeof type === 'string') {
2621
+ type = getPath(this, type, false) || getPath(window, type);
2622
+ meta.type = type;
2623
+ }
2624
+
2625
+ map.set(name, meta);
2626
+ }
2627
+ });
2628
+
2629
+ return map;
2630
+ }).cacheable()
2097
2631
  });
2098
2632
 
2099
2633
 
@@ -2111,29 +2645,55 @@ var referencedFindRecord = function(store, type, data, key, one) {
2111
2645
  };
2112
2646
 
2113
2647
  var hasAssociation = function(type, options, one) {
2114
- var embedded = options && options.embedded,
2115
- findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2648
+ options = options || {};
2649
+
2650
+ var embedded = options.embedded,
2651
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2116
2652
 
2117
- return Ember.computed(function(key) {
2653
+ var meta = { type: type, isAssociation: true, options: options };
2654
+ if (one) {
2655
+ meta.kind = 'belongsTo';
2656
+ } else {
2657
+ meta.kind = 'hasMany';
2658
+ }
2659
+
2660
+ return Ember.computed(function(key, value) {
2118
2661
  var data = get(this, 'data'), ids, id, association,
2119
- store = get(this, 'store');
2662
+ store = get(this, 'store');
2120
2663
 
2121
2664
  if (typeof type === 'string') {
2122
2665
  type = getPath(this, type, false) || getPath(window, type);
2123
2666
  }
2124
2667
 
2125
- key = (options && options.key) ? options.key : key;
2126
2668
  if (one) {
2127
- id = findRecord(store, type, data, key, true);
2128
- association = id ? store.find(type, id) : null;
2669
+ if (arguments.length === 2) {
2670
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
2671
+ data.setAssociation(key, get(value, 'clientId'));
2672
+ // put the client id in `key` in the data hash
2673
+ return value;
2674
+ } else {
2675
+ // Embedded belongsTo associations should not look for
2676
+ // a foreign key.
2677
+ if (embedded) {
2678
+ key = options.key || key;
2679
+
2680
+ // Non-embedded associations should look for a foreign key.
2681
+ // For example, instead of person, we might look for person_id
2682
+ } else {
2683
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
2684
+ }
2685
+ id = findRecord(store, type, data, key, true);
2686
+ association = id ? store.find(type, id) : null;
2687
+ }
2129
2688
  } else {
2689
+ key = options.key || key;
2130
2690
  ids = findRecord(store, type, data, key);
2131
2691
  association = store.findMany(type, ids);
2132
2692
  set(association, 'parentRecord', this);
2133
2693
  }
2134
2694
 
2135
2695
  return association;
2136
- }).property('data').cacheable().meta({ type: type });
2696
+ }).property('data').cacheable().meta(meta);
2137
2697
  };
2138
2698
 
2139
2699
  DS.hasMany = function(type, options) {
@@ -2142,10 +2702,12 @@ DS.hasMany = function(type, options) {
2142
2702
  };
2143
2703
 
2144
2704
  DS.hasOne = function(type, options) {
2145
- ember_assert("The type passed to DS.hasOne must be defined", !!type);
2705
+ ember_assert("The type passed to DS.belongsTo must be defined", !!type);
2146
2706
  return hasAssociation(type, options, true);
2147
2707
  };
2148
2708
 
2709
+ DS.belongsTo = DS.hasOne;
2710
+
2149
2711
  })({});
2150
2712
 
2151
2713