embient 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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