rasputin 0.14.1 → 0.15.0

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
- rasputin (0.14.1)
4
+ rasputin (0.15.0)
5
5
  actionpack (~> 3.1)
6
6
  jquery-rails (>= 1.0)
7
7
  railties (~> 3.1)
@@ -10,42 +10,42 @@ PATH
10
10
  GEM
11
11
  remote: http://rubygems.org/
12
12
  specs:
13
- actionpack (3.2.1)
14
- activemodel (= 3.2.1)
15
- activesupport (= 3.2.1)
13
+ actionpack (3.2.3)
14
+ activemodel (= 3.2.3)
15
+ activesupport (= 3.2.3)
16
16
  builder (~> 3.0.0)
17
17
  erubis (~> 2.7.0)
18
18
  journey (~> 1.0.1)
19
19
  rack (~> 1.4.0)
20
- rack-cache (~> 1.1)
20
+ rack-cache (~> 1.2)
21
21
  rack-test (~> 0.6.1)
22
22
  sprockets (~> 2.1.2)
23
- activemodel (3.2.1)
24
- activesupport (= 3.2.1)
23
+ activemodel (3.2.3)
24
+ activesupport (= 3.2.3)
25
25
  builder (~> 3.0.0)
26
- activesupport (3.2.1)
26
+ activesupport (3.2.3)
27
27
  i18n (~> 0.6)
28
28
  multi_json (~> 1.0)
29
29
  builder (3.0.0)
30
30
  erubis (2.7.0)
31
31
  hike (1.2.1)
32
32
  i18n (0.6.0)
33
- journey (1.0.1)
34
- jquery-rails (2.0.0)
35
- railties (>= 3.2.0.beta, < 5.0)
33
+ journey (1.0.3)
34
+ jquery-rails (2.0.2)
35
+ railties (>= 3.2.0, < 5.0)
36
36
  thor (~> 0.14)
37
- json (1.6.5)
38
- multi_json (1.0.4)
37
+ json (1.6.6)
38
+ multi_json (1.2.0)
39
39
  rack (1.4.1)
40
- rack-cache (1.1)
40
+ rack-cache (1.2)
41
41
  rack (>= 0.4)
42
42
  rack-ssl (1.3.2)
43
43
  rack
44
44
  rack-test (0.6.1)
45
45
  rack (>= 1.0)
46
- railties (3.2.1)
47
- actionpack (= 3.2.1)
48
- activesupport (= 3.2.1)
46
+ railties (3.2.3)
47
+ actionpack (= 3.2.3)
48
+ activesupport (= 3.2.3)
49
49
  rack-ssl (~> 1.3.2)
50
50
  rake (>= 0.8.7)
51
51
  rdoc (~> 3.4)
data/README.md CHANGED
@@ -74,6 +74,10 @@ And any of the following you want to include:
74
74
  ChangeLog
75
75
  ----------
76
76
 
77
+ 0.15.0
78
+
79
+ * Update to Ember.js 0.9.6
80
+
77
81
  0.14.1
78
82
 
79
83
  * Precompilation fix
@@ -1,3 +1,3 @@
1
1
  module Rasputin
2
- VERSION = "0.14.1"
2
+ VERSION = "0.15.0"
3
3
  end
@@ -1,270 +1,39 @@
1
-
2
- (function(exports) {
3
- window.DS = Ember.Namespace.create();
4
-
5
- })({});
6
-
7
-
8
- (function(exports) {
9
- DS.Adapter = Ember.Object.extend({
10
- commit: function(store, commitDetails) {
11
- commitDetails.updated.eachType(function(type, array) {
12
- this.updateRecords(store, type, array.slice());
13
- }, this);
14
-
15
- commitDetails.created.eachType(function(type, array) {
16
- this.createRecords(store, type, array.slice());
17
- }, this);
18
-
19
- commitDetails.deleted.eachType(function(type, array) {
20
- this.deleteRecords(store, type, array.slice());
21
- }, this);
22
- },
23
-
24
- createRecords: function(store, type, models) {
25
- models.forEach(function(model) {
26
- this.createRecord(store, type, model);
27
- }, this);
28
- },
29
-
30
- updateRecords: function(store, type, models) {
31
- models.forEach(function(model) {
32
- this.updateRecord(store, type, model);
33
- }, this);
34
- },
35
-
36
- deleteRecords: function(store, type, models) {
37
- models.forEach(function(model) {
38
- this.deleteRecord(store, type, model);
39
- }, this);
40
- },
41
-
42
- findMany: function(store, type, ids) {
43
- ids.forEach(function(id) {
44
- this.find(store, type, id);
45
- }, this);
46
- }
47
- });
48
- })({});
49
-
50
-
51
- (function(exports) {
52
- DS.fixtureAdapter = DS.Adapter.create({
53
- find: function(store, type, id) {
54
- var fixtures = type.FIXTURES;
55
-
56
- ember_assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
57
- if (fixtures.hasLoaded) { return; }
58
-
59
- setTimeout(function() {
60
- store.loadMany(type, fixtures);
61
- fixtures.hasLoaded = true;
62
- }, 300);
63
- },
64
-
65
- findMany: function() {
66
- this.find.apply(this, arguments);
67
- }
1
+ (function() {
2
+ window.DS = Ember.Namespace.create({
3
+ CURRENT_API_REVISION: 4
68
4
  });
69
5
 
70
- })({});
71
-
72
-
73
- (function(exports) {
74
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
75
-
76
- DS.RESTAdapter = DS.Adapter.extend({
77
- createRecord: function(store, type, model) {
78
- var root = this.rootForType(type);
79
-
80
- var data = {};
81
- data[root] = get(model, 'data');
82
-
83
- this.ajax("/" + this.pluralize(root), "POST", {
84
- data: data,
85
- success: function(json) {
86
- store.didCreateRecord(model, json[root]);
87
- }
88
- });
89
- },
90
-
91
- createRecords: function(store, type, models) {
92
- if (get(this, 'bulkCommit') === false) {
93
- return this._super(store, type, models);
94
- }
95
-
96
- var root = this.rootForType(type),
97
- plural = this.pluralize(root);
98
-
99
- var data = {};
100
- data[plural] = models.map(function(model) {
101
- return get(model, 'data');
102
- });
103
-
104
- this.ajax("/" + this.pluralize(root), "POST", {
105
- data: data,
106
- success: function(json) {
107
- store.didCreateRecords(type, models, json[plural]);
108
- }
109
- });
110
- },
111
-
112
- updateRecord: function(store, type, model) {
113
- var id = get(model, 'id');
114
- var root = this.rootForType(type);
115
-
116
- var data = {};
117
- data[root] = get(model, 'data');
118
-
119
- var url = ["", this.pluralize(root), id].join("/");
120
-
121
- this.ajax(url, "PUT", {
122
- data: data,
123
- success: function(json) {
124
- store.didUpdateRecord(model, json[root]);
125
- }
126
- });
127
- },
128
-
129
- updateRecords: function(store, type, models) {
130
- if (get(this, 'bulkCommit') === false) {
131
- return this._super(store, type, models);
132
- }
133
-
134
- var root = this.rootForType(type),
135
- plural = this.pluralize(root);
136
-
137
- var data = {};
138
- data[plural] = models.map(function(model) {
139
- return get(model, 'data');
140
- });
141
-
142
- this.ajax("/" + this.pluralize(root), "POST", {
143
- data: data,
144
- success: function(json) {
145
- store.didUpdateRecords(models, json[plural]);
146
- }
147
- });
148
- },
149
-
150
- deleteRecord: function(store, type, model) {
151
- var id = get(model, 'id');
152
- var root = this.rootForType(type);
153
-
154
- var url = ["", this.pluralize(root), id].join("/");
155
-
156
- this.ajax(url, "DELETE", {
157
- success: function(json) {
158
- store.didDeleteRecord(model);
159
- }
160
- });
161
- },
162
-
163
- deleteRecords: function(store, type, models) {
164
- if (get(this, 'bulkCommit') === false) {
165
- return this._super(store, type, models);
166
- }
167
-
168
- var root = this.rootForType(type),
169
- plural = this.pluralize(root),
170
- primaryKey = getPath(type, 'proto.primaryKey');
171
-
172
- var data = {};
173
- data[plural] = models.map(function(model) {
174
- return get(model, primaryKey);
175
- });
176
-
177
- this.ajax("/" + this.pluralize(root) + "/delete", "POST", {
178
- data: data,
179
- success: function(json) {
180
- store.didDeleteRecords(models);
181
- }
182
- });
183
- },
184
-
185
- find: function(store, type, id) {
186
- var root = this.rootForType(type);
187
-
188
- var url = ["", this.pluralize(root), id].join("/");
189
-
190
- this.ajax(url, "GET", {
191
- success: function(json) {
192
- store.load(type, json[root]);
193
- }
194
- });
195
- },
196
-
197
- findMany: function(store, type, ids) {
198
- var root = this.rootForType(type), plural = this.pluralize(root);
199
-
200
- this.ajax("/" + plural, "GET", {
201
- data: { ids: ids },
202
- success: function(json) {
203
- store.loadMany(type, ids, json[plural]);
204
- }
205
- });
206
- var url = "/" + plural;
207
- },
208
-
209
- findAll: function(store, type) {
210
- var root = this.rootForType(type), plural = this.pluralize(root);
211
-
212
- this.ajax("/" + plural, "GET", {
213
- success: function(json) {
214
- store.loadMany(type, json[plural]);
215
- }
216
- });
217
- },
218
-
219
- findQuery: function(store, type, query, modelArray) {
220
- var root = this.rootForType(type), plural = this.pluralize(root);
221
-
222
- this.ajax("/" + plural, "GET", {
223
- data: query,
224
- success: function(json) {
225
- modelArray.load(json[plural]);
226
- }
227
- });
228
- },
229
-
230
- // HELPERS
231
-
232
- plurals: {},
233
-
234
- // define a plurals hash in your subclass to define
235
- // special-case pluralization
236
- pluralize: function(name) {
237
- return this.plurals[name] || name + "s";
238
- },
239
-
240
- rootForType: function(type) {
241
- if (type.url) { return type.url; }
6
+ })();
242
7
 
243
- // use the last part of the name as the URL
244
- var parts = type.toString().split(".");
245
- var name = parts[parts.length - 1];
246
- return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
247
- },
248
-
249
- ajax: function(url, type, hash) {
250
- hash.url = url;
251
- hash.type = type;
252
- hash.dataType = "json";
253
8
 
254
- jQuery.ajax(hash);
255
- }
256
- });
257
9
 
10
+ (function() {
11
+ var get = Ember.get, set = Ember.set;
258
12
 
259
- })({});
13
+ /**
14
+ A model array is an array that contains records of a certain type. The model
15
+ array materializes records as needed when they are retrieved for the first
16
+ time. You should not create model arrays yourself. Instead, an instance of
17
+ DS.ModelArray or its subclasses will be returned by your application's store
18
+ in response to queries.
19
+ */
260
20
 
21
+ DS.ModelArray = Ember.ArrayProxy.extend({
261
22
 
262
- (function(exports) {
263
- var get = Ember.get, set = Ember.set;
23
+ /**
24
+ The model type contained by this model array.
264
25
 
265
- DS.ModelArray = Ember.ArrayProxy.extend({
26
+ @type DS.Model
27
+ */
266
28
  type: null,
29
+
30
+ // The array of client ids backing the model array. When a
31
+ // record is requested from the model array, the record
32
+ // for the client id at the same index is materialized, if
33
+ // necessary, by the store.
267
34
  content: null,
35
+
36
+ // The store that created this model array.
268
37
  store: null,
269
38
 
270
39
  init: function() {
@@ -274,7 +43,7 @@ DS.ModelArray = Ember.ArrayProxy.extend({
274
43
 
275
44
  arrayDidChange: function(array, index, removed, added) {
276
45
  var modelCache = get(this, 'modelCache');
277
- modelCache.replace(index, 0, Array(added));
46
+ modelCache.replace(index, 0, new Array(added));
278
47
 
279
48
  this._super(array, index, removed, added);
280
49
  },
@@ -306,19 +75,43 @@ DS.ModelArray = Ember.ArrayProxy.extend({
306
75
  }
307
76
  });
308
77
 
78
+ })();
79
+
80
+
81
+
82
+ (function() {
83
+ var get = Ember.get;
84
+
309
85
  DS.FilteredModelArray = DS.ModelArray.extend({
310
86
  filterFunction: null,
311
87
 
88
+ replace: function() {
89
+ var type = get(this, 'type').toString();
90
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
91
+ },
92
+
312
93
  updateFilter: Ember.observer(function() {
313
94
  var store = get(this, 'store');
314
95
  store.updateModelArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
315
96
  }, 'filterFunction')
316
97
  });
317
98
 
99
+ })();
100
+
101
+
102
+
103
+ (function() {
104
+ var get = Ember.get, set = Ember.set;
105
+
318
106
  DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
319
107
  query: null,
320
108
  isLoaded: false,
321
109
 
110
+ replace: function() {
111
+ var type = get(this, 'type').toString();
112
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
113
+ },
114
+
322
115
  load: function(array) {
323
116
  var store = get(this, 'store'), type = get(this, 'type');
324
117
 
@@ -331,125 +124,225 @@ DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
331
124
  }
332
125
  });
333
126
 
334
- })({});
335
127
 
128
+ })();
336
129
 
337
- (function(exports) {
338
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
339
130
 
340
- var OrderedSet = Ember.Object.extend({
341
- init: function() {
342
- this.clear();
343
- },
344
131
 
345
- clear: function() {
346
- this.set('presenceSet', {});
347
- this.set('list', Ember.NativeArray.apply([]));
348
- },
132
+ (function() {
133
+ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
134
+
135
+ var Set = function() {
136
+ this.hash = {};
137
+ this.list = [];
138
+ };
349
139
 
350
- add: function(obj) {
351
- var guid = Ember.guidFor(obj),
352
- presenceSet = get(this, 'presenceSet'),
353
- list = get(this, 'list');
140
+ Set.prototype = {
141
+ add: function(item) {
142
+ var hash = this.hash,
143
+ guid = guidFor(item);
354
144
 
355
- if (guid in presenceSet) { return; }
145
+ if (hash.hasOwnProperty(guid)) { return; }
356
146
 
357
- presenceSet[guid] = true;
358
- list.pushObject(obj);
147
+ hash[guid] = true;
148
+ this.list.push(item);
359
149
  },
360
150
 
361
- remove: function(obj) {
362
- var guid = Ember.guidFor(obj),
363
- presenceSet = get(this, 'presenceSet'),
364
- list = get(this, 'list');
151
+ remove: function(item) {
152
+ var hash = this.hash,
153
+ guid = guidFor(item);
365
154
 
366
- delete presenceSet[guid];
367
- list.removeObject(obj);
368
- },
155
+ if (!hash.hasOwnProperty(guid)) { return; }
369
156
 
370
- isEmpty: function() {
371
- return getPath(this, 'list.length') === 0;
372
- },
157
+ delete hash[guid];
158
+ var list = this.list,
159
+ index = Ember.ArrayUtils.indexOf(this, item);
373
160
 
374
- forEach: function(fn, self) {
375
- get(this, 'list').forEach(function(item) {
376
- fn.call(self, item);
377
- });
161
+ list.splice(index, 1);
378
162
  },
379
163
 
380
- toArray: function() {
381
- return get(this, 'list').slice();
164
+ isEmpty: function() {
165
+ return this.list.length === 0;
382
166
  }
383
- });
167
+ };
384
168
 
385
- /**
386
- A Hash stores values indexed by keys. Unlike JavaScript's
387
- default Objects, the keys of a Hash can be any JavaScript
388
- object.
169
+ var ManyArrayState = Ember.State.extend({
170
+ recordWasAdded: function(manager, record) {
171
+ var dirty = manager.dirty, observer;
172
+ dirty.add(record);
173
+
174
+ observer = function() {
175
+ if (!get(record, 'isDirty')) {
176
+ record.removeObserver('isDirty', observer);
177
+ manager.send('childWasSaved', record);
178
+ }
179
+ };
389
180
 
390
- Internally, a Hash has two data structures:
181
+ record.addObserver('isDirty', observer);
182
+ },
391
183
 
392
- `keys`: an OrderedSet of all of the existing keys
393
- `values`: a JavaScript Object indexed by the
394
- Ember.guidFor(key)
184
+ recordWasRemoved: function(manager, record) {
185
+ var dirty = manager.dirty, observer;
186
+ dirty.add(record);
395
187
 
396
- When a key/value pair is added for the first time, we
397
- add the key to the `keys` OrderedSet, and create or
398
- replace an entry in `values`. When an entry is deleted,
399
- we delete its entry in `keys` and `values`.
400
- */
188
+ observer = function() {
189
+ record.removeObserver('isDirty', observer);
190
+ if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); }
191
+ };
192
+
193
+ record.addObserver('isDirty', observer);
194
+ }
195
+ });
196
+
197
+ var states = {
198
+ clean: ManyArrayState.create({
199
+ isDirty: false,
200
+
201
+ recordWasAdded: function(manager, record) {
202
+ this._super(manager, record);
203
+ manager.goToState('dirty');
204
+ },
205
+
206
+ update: function(manager, clientIds) {
207
+ var manyArray = manager.manyArray;
208
+ set(manyArray, 'content', clientIds);
209
+ }
210
+ }),
211
+
212
+ dirty: ManyArrayState.create({
213
+ isDirty: true,
214
+
215
+ childWasSaved: function(manager, child) {
216
+ var dirty = manager.dirty;
217
+ dirty.remove(child);
218
+
219
+ if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); }
220
+ },
221
+
222
+ arrayBecameSaved: function(manager) {
223
+ manager.goToState('clean');
224
+ }
225
+ })
226
+ };
227
+
228
+ DS.ManyArrayStateManager = Ember.StateManager.extend({
229
+ manyArray: null,
230
+ initialState: 'clean',
231
+ states: states,
401
232
 
402
- var Hash = Ember.Object.extend({
403
233
  init: function() {
404
- set(this, 'keys', OrderedSet.create());
405
- set(this, 'values', {});
406
- },
234
+ this._super();
235
+ this.dirty = new Set();
236
+ }
237
+ });
407
238
 
408
- add: function(key, value) {
409
- var keys = get(this, 'keys'), values = get(this, 'values');
410
- var guid = Ember.guidFor(key);
239
+ })();
411
240
 
412
- keys.add(key);
413
- values[guid] = value;
414
241
 
415
- return value;
242
+
243
+ (function() {
244
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
245
+
246
+ DS.ManyArray = DS.ModelArray.extend({
247
+ init: function() {
248
+ set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this }));
249
+
250
+ return this._super();
416
251
  },
417
252
 
418
- remove: function(key) {
419
- var keys = get(this, 'keys'), values = get(this, 'values');
420
- var guid = Ember.guidFor(key), value;
253
+ parentRecord: null,
421
254
 
422
- keys.remove(key);
255
+ isDirty: Ember.computed(function() {
256
+ return getPath(this, 'stateManager.currentState.isDirty');
257
+ }).property('stateManager.currentState').cacheable(),
423
258
 
424
- value = values[guid];
425
- delete values[guid];
259
+ fetch: function() {
260
+ var clientIds = get(this, 'content'),
261
+ store = get(this, 'store'),
262
+ type = get(this, 'type');
426
263
 
427
- return value;
264
+ var ids = clientIds.map(function(clientId) {
265
+ return store.clientIdToId[clientId];
266
+ });
267
+
268
+ store.fetchMany(type, ids);
428
269
  },
429
270
 
430
- fetch: function(key) {
431
- var values = get(this, 'values');
432
- var guid = Ember.guidFor(key);
271
+ // Overrides Ember.Array's replace method to implement
272
+ replace: function(index, removed, added) {
273
+ var parentRecord = get(this, 'parentRecord');
274
+ var pendingParent = parentRecord && !get(parentRecord, 'id');
275
+ var stateManager = get(this, 'stateManager');
276
+
277
+ added = added.map(function(record) {
278
+
279
+
280
+ if (pendingParent) {
281
+ record.send('waitingOn', parentRecord);
282
+ }
283
+
284
+ this.assignInverse(record, parentRecord);
285
+
286
+ stateManager.send('recordWasAdded', record);
287
+
288
+ return record.get('clientId');
289
+ }, this);
290
+
291
+ var store = this.store;
292
+
293
+ var len = index+removed, record;
294
+ for (var i = index; i < len; i++) {
295
+ // TODO: null out inverse FK
296
+ record = this.objectAt(i);
297
+ this.assignInverse(record, parentRecord, true);
298
+ stateManager.send('recordWasAdded', record);
299
+ }
433
300
 
434
- return values[guid];
301
+ this._super(index, removed, added);
435
302
  },
436
303
 
437
- forEach: function(fn, binding) {
438
- var keys = get(this, 'keys'), values = get(this, 'values');
304
+ assignInverse: function(record, parentRecord, remove) {
305
+ var associationMap = get(record.constructor, 'associations'),
306
+ possibleAssociations = associationMap.get(parentRecord.constructor),
307
+ possible, actual;
439
308
 
440
- keys.forEach(function(key) {
441
- var guid = Ember.guidFor(key);
442
- fn.call(binding, key, values[guid]);
443
- });
309
+ if (!possibleAssociations) { return; }
310
+
311
+ for (var i = 0, l = possibleAssociations.length; i < l; i++) {
312
+ possible = possibleAssociations[i];
313
+
314
+ if (possible.kind === 'belongsTo') {
315
+ actual = possible;
316
+ break;
317
+ }
318
+ }
319
+
320
+ if (actual) {
321
+ set(record, actual.name, remove ? null : parentRecord);
322
+ }
444
323
  }
445
324
  });
446
325
 
326
+ })();
327
+
328
+
329
+
330
+ (function() {
331
+
332
+ })();
333
+
334
+
335
+
336
+ (function() {
337
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
338
+
447
339
  DS.Transaction = Ember.Object.extend({
448
340
  init: function() {
449
- set(this, 'dirty', {
450
- created: Hash.create(),
451
- updated: Hash.create(),
452
- deleted: Hash.create()
341
+ set(this, 'buckets', {
342
+ clean: Ember.Map.create(),
343
+ created: Ember.Map.create(),
344
+ updated: Ember.Map.create(),
345
+ deleted: Ember.Map.create()
453
346
  });
454
347
  },
455
348
 
@@ -459,44 +352,102 @@ DS.Transaction = Ember.Object.extend({
459
352
  return store.createRecord(type, hash, this);
460
353
  },
461
354
 
462
- add: function(model) {
463
- var modelTransaction = get(model, 'transaction');
464
- ember_assert("Models cannot belong to more than one transaction at a time.", !modelTransaction);
355
+ add: function(record) {
356
+ // we could probably make this work if someone has a valid use case. Do you?
357
+
358
+
359
+ var modelTransaction = get(record, 'transaction'),
360
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
361
+
362
+
363
+ this.adoptRecord(record);
364
+ },
365
+
366
+ remove: function(record) {
367
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
368
+
369
+ defaultTransaction.adoptRecord(record);
370
+ },
371
+
372
+ /**
373
+ @private
374
+
375
+ This method moves a record into a different transaction without the normal
376
+ checks that ensure that the user is not doing something weird, like moving
377
+ a dirty record into a new transaction.
378
+
379
+ It is designed for internal use, such as when we are moving a clean record
380
+ into a new transaction when the transaction is committed.
465
381
 
466
- set(model, 'transaction', this);
382
+ This method must not be called unless the record is clean.
383
+ */
384
+ adoptRecord: function(record) {
385
+ var oldTransaction = get(record, 'transaction');
386
+
387
+ if (oldTransaction) {
388
+ oldTransaction.removeFromBucket('clean', record);
389
+ }
390
+
391
+ this.addToBucket('clean', record);
392
+ set(record, 'transaction', this);
467
393
  },
468
394
 
469
- modelBecameDirty: function(kind, model) {
470
- var dirty = get(get(this, 'dirty'), kind),
471
- type = model.constructor;
395
+ modelBecameDirty: function(kind, record) {
396
+ this.removeFromBucket('clean', record);
397
+ this.addToBucket(kind, record);
398
+ },
399
+
400
+ /** @private */
401
+ addToBucket: function(kind, record) {
402
+ var bucket = get(get(this, 'buckets'), kind),
403
+ type = record.constructor;
472
404
 
473
- var models = dirty.fetch(type);
405
+ var records = bucket.get(type);
406
+
407
+ if (!records) {
408
+ records = Ember.OrderedSet.create();
409
+ bucket.set(type, records);
410
+ }
474
411
 
475
- models = models || dirty.add(type, OrderedSet.create());
476
- models.add(model);
412
+ records.add(record);
477
413
  },
478
414
 
479
- modelBecameClean: function(kind, model) {
480
- var dirty = get(get(this, 'dirty'), kind),
481
- type = model.constructor;
415
+ /** @private */
416
+ removeFromBucket: function(kind, record) {
417
+ var bucket = get(get(this, 'buckets'), kind),
418
+ type = record.constructor;
482
419
 
483
- var models = dirty.fetch(type);
484
- models.remove(model);
420
+ var records = bucket.get(type);
421
+ records.remove(record);
422
+ },
423
+
424
+ modelBecameClean: function(kind, record) {
425
+ this.removeFromBucket(kind, record);
485
426
 
486
- set(model, 'transaction', null);
427
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
428
+ defaultTransaction.adoptRecord(record);
487
429
  },
488
430
 
489
431
  commit: function() {
490
- var dirtyMap = get(this, 'dirty');
432
+ var buckets = get(this, 'buckets');
491
433
 
492
434
  var iterate = function(kind, fn, binding) {
493
- var dirty = get(dirtyMap, kind);
435
+ var dirty = get(buckets, kind);
494
436
 
495
437
  dirty.forEach(function(type, models) {
496
438
  if (models.isEmpty()) { return; }
497
439
 
498
- models.forEach(function(model) { model.willCommit(); });
499
- fn.call(binding, type, models.toArray());
440
+ var array = [];
441
+
442
+ models.forEach(function(model) {
443
+ model.send('willCommit');
444
+
445
+ if (get(model, 'isPending') === false) {
446
+ array.push(model);
447
+ }
448
+ });
449
+
450
+ fn.call(binding, type, array);
500
451
  });
501
452
  };
502
453
 
@@ -516,57 +467,39 @@ DS.Transaction = Ember.Object.extend({
516
467
 
517
468
  var store = get(this, 'store');
518
469
  var adapter = get(store, '_adapter');
470
+
471
+ var clean = get(buckets, 'clean');
472
+ var defaultTransaction = get(store, 'defaultTransaction');
473
+
474
+ clean.forEach(function(type, records) {
475
+ records.forEach(function(record) {
476
+ this.remove(record);
477
+ }, this);
478
+ }, this);
479
+
519
480
  if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
520
481
  else { throw fmt("Adapter is either null or do not implement `commit` method", this); }
521
482
  }
522
483
  });
523
484
 
524
- })({});
525
-
526
-
527
- (function(exports) {
528
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
529
-
530
- var OrderedSet = Ember.Object.extend({
531
- init: function() {
532
- this.clear();
533
- },
534
-
535
- clear: function() {
536
- this.set('presenceSet', {});
537
- this.set('list', Ember.NativeArray.apply([]));
538
- },
539
-
540
- add: function(obj) {
541
- var guid = Ember.guidFor(obj),
542
- presenceSet = get(this, 'presenceSet'),
543
- list = get(this, 'list');
485
+ })();
544
486
 
545
- if (guid in presenceSet) { return; }
546
487
 
547
- presenceSet[guid] = true;
548
- list.pushObject(obj);
549
- },
550
-
551
- remove: function(obj) {
552
- var guid = Ember.guidFor(obj),
553
- presenceSet = get(this, 'presenceSet'),
554
- list = get(this, 'list');
555
-
556
- delete presenceSet[guid];
557
- list.removeObject(obj);
558
- },
559
488
 
560
- isEmpty: function() {
561
- return getPath(this, 'list.length') === 0;
562
- },
489
+ (function() {
490
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
563
491
 
564
- forEach: function(fn, self) {
565
- get(this, 'list').forEach(function(item) {
566
- fn.call(self, item);
567
- });
492
+ var DATA_PROXY = {
493
+ get: function(name) {
494
+ return this.savedData[name];
568
495
  }
569
- });
496
+ };
497
+
498
+ // These values are used in the data cache when clientIds are
499
+ // needed but the underlying data has not yet been loaded by
500
+ // the server.
501
+ var UNLOADED = 'unloaded';
502
+ var LOADING = 'loading';
570
503
 
571
504
  // Implementors Note:
572
505
  //
@@ -615,33 +548,49 @@ DS.Store = Ember.Object.extend({
615
548
  The init method registers this store as the default if none is specified.
616
549
  */
617
550
  init: function() {
551
+ // Enforce API revisioning. See BREAKING_CHANGES.md for more.
552
+ var revision = get(this, 'revision');
553
+
554
+ if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
555
+ 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);
556
+ }
557
+
618
558
  if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
619
559
  set(DS, 'defaultStore', this);
620
560
  }
621
561
 
622
- set(this, 'data', []);
623
- set(this, '_typeMap', {});
624
- set(this, 'models', []);
625
- set(this, 'modelArrays', []);
626
- set(this, 'modelArraysByClientId', {});
627
- set(this, 'defaultTransaction', DS.Transaction.create({ store: this }));
562
+ // internal bookkeeping; not observable
563
+ this.typeMaps = {};
564
+ this.recordCache = [];
565
+ this.clientIdToId = {};
566
+ this.modelArraysByClientId = {};
567
+
568
+ set(this, 'defaultTransaction', this.transaction());
628
569
 
629
570
  return this._super();
630
571
  },
631
572
 
573
+ /**
574
+ Returns a new transaction scoped to this store.
575
+
576
+ @see {DS.Transaction}
577
+ @returns DS.Transaction
578
+ */
632
579
  transaction: function() {
633
580
  return DS.Transaction.create({ store: this });
634
581
  },
635
582
 
636
- modelArraysForClientId: function(clientId) {
637
- var modelArrays = get(this, 'modelArraysByClientId');
638
- var ret = modelArrays[clientId];
583
+ /**
584
+ @private
639
585
 
640
- if (!ret) {
641
- ret = modelArrays[clientId] = OrderedSet.create();
642
- }
586
+ This is used only by the model's DataProxy. Do not use this directly.
587
+ */
588
+ dataForRecord: function(record) {
589
+ var type = record.constructor,
590
+ clientId = get(record, 'clientId'),
591
+ typeMap = this.typeMapFor(type);
643
592
 
644
- return ret;
593
+ return typeMap.cidToHash[clientId];
645
594
  },
646
595
 
647
596
  /**
@@ -654,53 +603,95 @@ DS.Store = Ember.Object.extend({
654
603
  */
655
604
  adapter: null,
656
605
 
606
+ /**
607
+ @private
608
+
609
+ This property returns the adapter, after resolving a possible String.
610
+
611
+ @returns DS.Adapter
612
+ */
657
613
  _adapter: Ember.computed(function() {
658
614
  var adapter = get(this, 'adapter');
659
615
  if (typeof adapter === 'string') {
660
- return getPath(this, adapter);
616
+ return getPath(this, adapter, false) || getPath(window, adapter);
661
617
  }
662
618
  return adapter;
663
619
  }).property('adapter').cacheable(),
664
620
 
665
- clientIdCounter: -1,
621
+ // A monotonically increasing number to be used to uniquely identify
622
+ // data hashes and records.
623
+ clientIdCounter: 1,
666
624
 
667
625
  // ....................
668
626
  // . CREATE NEW MODEL .
669
627
  // ....................
670
628
 
671
- createRecord: function(type, hash, transaction) {
672
- hash = hash || {};
673
-
674
- var id = hash[getPath(type, 'proto.primaryKey')] || null;
629
+ /**
630
+ Create a new record in the current store. The properties passed
631
+ to this method are set on the newly created record.
675
632
 
676
- var model = type.create({
677
- data: hash || {},
678
- store: this,
679
- transaction: transaction
633
+ @param {subclass of DS.Model} type
634
+ @param {Object} properties a hash of properties to set on the
635
+ newly created record.
636
+ @returns DS.Model
637
+ */
638
+ createRecord: function(type, properties, transaction) {
639
+ properties = properties || {};
640
+
641
+ // Create a new instance of the model `type` and put it
642
+ // into the specified `transaction`. If no transaction is
643
+ // specified, the default transaction will be used.
644
+ //
645
+ // NOTE: A `transaction` is specified when the
646
+ // `transaction.createRecord` API is used.
647
+ var record = type._create({
648
+ store: this
680
649
  });
681
650
 
682
- model.adapterDidCreate();
651
+ transaction = transaction || get(this, 'defaultTransaction');
652
+ transaction.adoptRecord(record);
683
653
 
684
- var data = this.clientIdToHashMap(type);
685
- var models = get(this, 'models');
654
+ // Extract the primary key from the `properties` hash,
655
+ // based on the `primaryKey` for the model type.
656
+ var id = properties[get(record, 'primaryKey')] || null;
686
657
 
687
- var clientId = this.pushHash(hash, id, type);
658
+ var hash = {}, clientId;
688
659
 
689
- set(model, 'clientId', clientId);
660
+ // Push the hash into the store. If present, associate the
661
+ // extracted `id` with the hash.
662
+ clientId = this.pushHash(hash, id, type);
690
663
 
691
- models[clientId] = model;
664
+ record.send('didChangeData');
692
665
 
693
- this.updateModelArrays(type, clientId, hash);
666
+ var recordCache = get(this, 'recordCache');
694
667
 
695
- return model;
668
+ // Now that we have a clientId, attach it to the record we
669
+ // just created.
670
+ set(record, 'clientId', clientId);
671
+
672
+ // Store the record we just created in the record cache for
673
+ // this clientId.
674
+ recordCache[clientId] = record;
675
+
676
+ // Set the properties specified on the record.
677
+ record.setProperties(properties);
678
+
679
+ this.updateModelArrays(type, clientId, get(record, 'data'));
680
+
681
+ return record;
696
682
  },
697
683
 
698
684
  // ................
699
685
  // . DELETE MODEL .
700
686
  // ................
701
687
 
702
- deleteRecord: function(model) {
703
- model.deleteRecord();
688
+ /**
689
+ For symmetry, a record can be deleted via the store.
690
+
691
+ @param {DS.Model} record
692
+ */
693
+ deleteRecord: function(record) {
694
+ record.send('deleteRecord');
704
695
  },
705
696
 
706
697
  // ...............
@@ -708,26 +699,64 @@ DS.Store = Ember.Object.extend({
708
699
  // ...............
709
700
 
710
701
  /**
711
- Finds a model by its id. If the data for that model has already been
712
- loaded, an instance of DS.Model with that data will be returned
713
- immediately. Otherwise, an empty DS.Model instance will be returned in
714
- the loading state. As soon as the requested data is available, the model
715
- will be moved into the loaded state and all of the information will be
716
- available.
702
+ This is the main entry point into finding records. The first
703
+ parameter to this method is always a subclass of `DS.Model`.
717
704
 
718
- Note that only one DS.Model instance is ever created per unique id for a
719
- given type.
705
+ You can use the `find` method on a subclass of `DS.Model`
706
+ directly if your application only has one store. For
707
+ example, instead of `store.find(App.Person, 1)`, you could
708
+ say `App.Person.find(1)`.
720
709
 
721
- Example:
710
+ ---
722
711
 
723
- var record = MyApp.store.find(MyApp.Person, 1234);
712
+ To find a record by ID, pass the `id` as the second parameter:
724
713
 
725
- @param {DS.Model} type
726
- @param {String|Number} id
714
+ store.find(App.Person, 1);
715
+ App.Person.find(1);
716
+
717
+ If the record with that `id` had not previously been loaded,
718
+ the store will return an empty record immediately and ask
719
+ the adapter to find the data by calling its `find` method.
720
+
721
+ The `find` method will always return the same object for a
722
+ given type and `id`. To check whether the adapter has populated
723
+ a record, you can check its `isLoaded` property.
724
+
725
+ ---
726
+
727
+ To find all records for a type, call `find` with no additional
728
+ parameters:
729
+
730
+ store.find(App.Person);
731
+ App.Person.find();
732
+
733
+ This will return a `ModelArray` representing all known records
734
+ for the given type and kick off a request to the adapter's
735
+ `findAll` method to load any additional records for the type.
736
+
737
+ The `ModelArray` returned by `find()` is live. If any more
738
+ records for the type are added at a later time through any
739
+ mechanism, it will automatically update to reflect the change.
740
+
741
+ ---
742
+
743
+ To find a record by a query, call `find` with a hash as the
744
+ second parameter:
745
+
746
+ store.find(App.Person, { page: 1 });
747
+ App.Person.find({ page: 1 });
748
+
749
+ This will return a `ModelArray` immediately, but it will always
750
+ be an empty `ModelArray` at first. It will call the adapter's
751
+ `findQuery` method, which will populate the `ModelArray` once
752
+ the server has returned results.
753
+
754
+ You can check whether a query results `ModelArray` has loaded
755
+ by checking its `isLoaded` property.
727
756
  */
728
757
  find: function(type, id, query) {
729
758
  if (id === undefined) {
730
- return this.findMany(type, null, null);
759
+ return this.findAll(type);
731
760
  }
732
761
 
733
762
  if (query !== undefined) {
@@ -740,16 +769,15 @@ DS.Store = Ember.Object.extend({
740
769
  return this.findMany(type, id);
741
770
  }
742
771
 
743
- var clientId = this.clientIdForId(type, id);
772
+ var clientId = this.typeMapFor(type).idToCid[id];
744
773
 
745
774
  return this.findByClientId(type, clientId, id);
746
775
  },
747
776
 
748
777
  findByClientId: function(type, clientId, id) {
749
- var model;
750
-
751
- var models = get(this, 'models');
752
- var data = this.clientIdToHashMap(type);
778
+ var recordCache = get(this, 'recordCache'),
779
+ dataCache = this.typeMapFor(type).cidToHash,
780
+ model;
753
781
 
754
782
  // If there is already a clientId assigned for this
755
783
  // type/id combination, try to find an existing
@@ -757,37 +785,55 @@ DS.Store = Ember.Object.extend({
757
785
  // materialize a new model and set its data to the
758
786
  // value we already have.
759
787
  if (clientId !== undefined) {
760
- model = models[clientId];
788
+ model = recordCache[clientId];
761
789
 
762
790
  if (!model) {
763
791
  // create a new instance of the model in the
764
792
  // 'isLoading' state
765
- model = this.createModel(type, clientId);
793
+ model = this.materializeRecord(type, clientId);
766
794
 
767
- // immediately set its data
768
- model.setData(data[clientId] || null);
795
+ if (typeof dataCache[clientId] === 'object') {
796
+ model.send('didChangeData');
797
+ }
769
798
  }
770
799
  } else {
771
- clientId = this.pushHash(null, id, type);
800
+ clientId = this.pushHash(LOADING, id, type);
772
801
 
773
802
  // create a new instance of the model in the
774
803
  // 'isLoading' state
775
- model = this.createModel(type, clientId);
804
+ model = this.materializeRecord(type, clientId);
776
805
 
777
806
  // let the adapter set the data, possibly async
778
807
  var adapter = get(this, '_adapter');
779
808
  if (adapter && adapter.find) { adapter.find(this, type, id); }
780
- else { throw fmt("Adapter is either null or do not implement `find` method", this); }
809
+ else { throw fmt("Adapter is either null or does not implement `find` method", this); }
781
810
  }
782
811
 
783
812
  return model;
784
813
  },
785
814
 
786
- /** @private
815
+ /**
816
+ @private
817
+
818
+ Ask the adapter to fetch IDs that are not already loaded.
819
+
820
+ This method will convert `id`s to `clientId`s, filter out
821
+ `clientId`s that already have a data hash present, and pass
822
+ the remaining `id`s to the adapter.
823
+
824
+ @param {Class} type A model class
825
+ @param {Array} ids An array of ids
826
+ @param {Object} query
827
+
828
+ @returns {Array} An Array of all clientIds for the
829
+ specified ids.
787
830
  */
788
- findMany: function(type, ids, query) {
789
- var idToClientIdMap = this.idToClientIdMap(type);
790
- var data = this.clientIdToHashMap(type), needed;
831
+ fetchMany: function(type, ids, query) {
832
+ var typeMap = this.typeMapFor(type),
833
+ idToClientIdMap = typeMap.idToCid,
834
+ dataCache = typeMap.cidToHash,
835
+ data = typeMap.cidToHash,
836
+ needed;
791
837
 
792
838
  var clientIds = Ember.A([]);
793
839
 
@@ -795,32 +841,59 @@ DS.Store = Ember.Object.extend({
795
841
  needed = [];
796
842
 
797
843
  ids.forEach(function(id) {
844
+ // Get the clientId for the given id
798
845
  var clientId = idToClientIdMap[id];
799
- if (clientId === undefined || data[clientId] === undefined) {
800
- clientId = this.pushHash(null, id, type);
846
+
847
+ // If there is no `clientId` yet
848
+ if (clientId === undefined) {
849
+ // Create a new `clientId`, marking its data hash
850
+ // as loading. Once the adapter returns the data
851
+ // hash, it will be updated
852
+ clientId = this.pushHash(LOADING, id, type);
853
+ needed.push(id);
854
+
855
+ // If there is a clientId, but its data hash is
856
+ // marked as unloaded (this happens when a
857
+ // hasMany association creates clientIds for its
858
+ // referenced ids before they were loaded)
859
+ } else if (clientId && data[clientId] === UNLOADED) {
860
+ // change the data hash marker to loading
861
+ dataCache[clientId] = LOADING;
801
862
  needed.push(id);
802
863
  }
803
864
 
865
+ // this method is expected to return a list of
866
+ // all of the clientIds for the specified ids,
867
+ // unconditionally add it.
804
868
  clientIds.push(clientId);
805
869
  }, this);
806
870
  } else {
807
871
  needed = null;
808
872
  }
809
873
 
874
+ // If there are any needed ids, ask the adapter to load them
810
875
  if ((needed && get(needed, 'length') > 0) || query) {
811
876
  var adapter = get(this, '_adapter');
812
877
  if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
813
- else { throw fmt("Adapter is either null or do not implement `findMany` method", this); }
878
+ else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
814
879
  }
815
880
 
816
- return this.createModelArray(type, clientIds);
881
+ return clientIds;
882
+ },
883
+
884
+ /** @private
885
+ */
886
+ findMany: function(type, ids, query) {
887
+ var clientIds = this.fetchMany(type, ids, query);
888
+
889
+ return this.createManyArray(type, clientIds);
817
890
  },
818
891
 
819
892
  findQuery: function(type, query) {
820
893
  var array = DS.AdapterPopulatedModelArray.create({ type: type, content: Ember.A([]), store: this });
821
894
  var adapter = get(this, '_adapter');
822
895
  if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
823
- else { throw fmt("Adapter is either null or do not implement `findQuery` method", this); }
896
+ else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); }
824
897
  return array;
825
898
  },
826
899
 
@@ -841,7 +914,14 @@ DS.Store = Ember.Object.extend({
841
914
  return array;
842
915
  },
843
916
 
844
- filter: function(type, filter) {
917
+ filter: function(type, query, filter) {
918
+ // allow an optional server query
919
+ if (arguments.length === 3) {
920
+ this.findQuery(type, query);
921
+ } else if (arguments.length === 2) {
922
+ filter = query;
923
+ }
924
+
845
925
  var array = DS.FilteredModelArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
846
926
 
847
927
  this.registerModelArray(array, type, filter);
@@ -853,11 +933,8 @@ DS.Store = Ember.Object.extend({
853
933
  // . UPDATING .
854
934
  // ............
855
935
 
856
- hashWasUpdated: function(type, clientId) {
857
- var clientIdToHashMap = this.clientIdToHashMap(type);
858
- var hash = clientIdToHashMap[clientId];
859
-
860
- this.updateModelArrays(type, clientId, hash);
936
+ hashWasUpdated: function(type, clientId, record) {
937
+ this.updateModelArrays(type, clientId, get(record, 'data'));
861
938
  },
862
939
 
863
940
  // ..............
@@ -865,11 +942,14 @@ DS.Store = Ember.Object.extend({
865
942
  // ..............
866
943
 
867
944
  commit: function() {
868
- get(this, 'defaultTransaction').commit();
945
+ var defaultTransaction = get(this, 'defaultTransaction');
946
+ set(this, 'defaultTransaction', this.transaction());
947
+
948
+ defaultTransaction.commit();
869
949
  },
870
950
 
871
951
  didUpdateRecords: function(array, hashes) {
872
- if (arguments.length === 2) {
952
+ if (hashes) {
873
953
  array.forEach(function(model, idx) {
874
954
  this.didUpdateRecord(model, hashes[idx]);
875
955
  }, this);
@@ -881,72 +961,91 @@ DS.Store = Ember.Object.extend({
881
961
  },
882
962
 
883
963
  didUpdateRecord: function(model, hash) {
884
- if (arguments.length === 2) {
885
- var clientId = get(model, 'clientId');
886
- var data = this.clientIdToHashMap(model.constructor);
964
+ if (hash) {
965
+ var clientId = get(model, 'clientId'),
966
+ dataCache = this.typeMapFor(model.constructor).cidToHash;
887
967
 
888
- data[clientId] = hash;
889
- model.set('data', hash);
968
+ dataCache[clientId] = hash;
969
+ model.send('didChangeData');
970
+ model.hashWasUpdated();
890
971
  }
891
972
 
892
- model.adapterDidUpdate();
973
+ model.send('didCommit');
893
974
  },
894
975
 
895
976
  didDeleteRecords: function(array) {
896
977
  array.forEach(function(model) {
897
- model.adapterDidDelete();
978
+ model.send('didCommit');
898
979
  });
899
980
  },
900
981
 
901
982
  didDeleteRecord: function(model) {
902
- model.adapterDidDelete();
983
+ model.send('didCommit');
903
984
  },
904
985
 
905
- didCreateRecords: function(type, array, hashes) {
906
- var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
986
+ _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) {
987
+ var recordData = get(record, 'data'), id, changes;
907
988
 
908
- var idToClientIdMap = this.idToClientIdMap(type);
909
- var data = this.clientIdToHashMap(type);
910
- var idList = this.idList(type);
989
+ if (hash) {
990
+ typeMap.cidToHash[clientId] = hash;
991
+
992
+ // If the server returns a hash, we assume that the server's version
993
+ // of the data supercedes the local changes.
994
+ record.beginPropertyChanges();
995
+ record.send('didChangeData');
996
+ recordData.adapterDidUpdate(hash);
997
+ record.hashWasUpdated();
998
+ record.endPropertyChanges();
911
999
 
912
- for (var i=0, l=get(array, 'length'); i<l; i++) {
913
- var model = array[i], hash = hashes[i];
914
1000
  id = hash[primaryKey];
915
- clientId = get(model, 'clientId');
916
1001
 
917
- data[clientId] = hash;
918
- set(model, 'data', hash);
1002
+ typeMap.idToCid[id] = clientId;
1003
+ this.clientIdToId[clientId] = id;
1004
+ } else {
1005
+ recordData.commit();
1006
+ }
1007
+
1008
+ record.send('didCommit');
1009
+ },
919
1010
 
920
- idToClientIdMap[id] = clientId;
921
- idList.push(id);
922
1011
 
923
- model.adapterDidUpdate();
1012
+ didCreateRecords: function(type, array, hashes) {
1013
+ var primaryKey = type.proto().primaryKey,
1014
+ typeMap = this.typeMapFor(type),
1015
+ id, clientId;
1016
+
1017
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
1018
+ var model = array[i], hash = hashes[i];
1019
+ clientId = get(model, 'clientId');
1020
+
1021
+ this._didCreateRecord(model, hash, typeMap, clientId, primaryKey);
924
1022
  }
925
1023
  },
926
1024
 
927
1025
  didCreateRecord: function(model, hash) {
928
- var type = model.constructor;
1026
+ var type = model.constructor,
1027
+ typeMap = this.typeMapFor(type),
1028
+ id, clientId, primaryKey;
929
1029
 
930
- var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
1030
+ // The hash is optional, but if it is not provided, the client must have
1031
+ // provided a primary key.
931
1032
 
932
- var idToClientIdMap = this.idToClientIdMap(type);
933
- var data = this.clientIdToHashMap(type);
934
- var idList = this.idList(type);
1033
+ primaryKey = type.proto().primaryKey;
935
1034
 
936
- id = hash[primaryKey];
1035
+ // TODO: Make ember_assert more flexible and convert this into an ember_assert
1036
+ if (hash) {
937
1037
 
938
- clientId = get(model, 'clientId');
939
- data[clientId] = hash;
940
- set(model, 'data', hash);
1038
+ } else {
941
1039
 
942
- idToClientIdMap[id] = clientId;
943
- idList.push(id);
1040
+ }
1041
+
1042
+ clientId = get(model, 'clientId');
944
1043
 
945
- model.adapterDidUpdate();
1044
+ this._didCreateRecord(model, hash, typeMap, clientId, primaryKey);
946
1045
  },
947
1046
 
948
1047
  recordWasInvalid: function(record, errors) {
949
- record.wasInvalid(errors);
1048
+ record.send('becameInvalid', errors);
950
1049
  },
951
1050
 
952
1051
  // ................
@@ -954,16 +1053,15 @@ DS.Store = Ember.Object.extend({
954
1053
  // ................
955
1054
 
956
1055
  registerModelArray: function(array, type, filter) {
957
- var modelArrays = get(this, 'modelArrays');
958
- var idToClientIdMap = this.idToClientIdMap(type);
1056
+ var modelArrays = this.typeMapFor(type).modelArrays;
959
1057
 
960
1058
  modelArrays.push(array);
961
1059
 
962
1060
  this.updateModelArrayFilter(array, type, filter);
963
1061
  },
964
1062
 
965
- createModelArray: function(type, clientIds) {
966
- var array = DS.ModelArray.create({ type: type, content: clientIds, store: this });
1063
+ createManyArray: function(type, clientIds) {
1064
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
967
1065
 
968
1066
  clientIds.forEach(function(clientId) {
969
1067
  var modelArrays = this.modelArraysForClientId(clientId);
@@ -974,40 +1072,47 @@ DS.Store = Ember.Object.extend({
974
1072
  },
975
1073
 
976
1074
  updateModelArrayFilter: function(array, type, filter) {
977
- var data = this.clientIdToHashMap(type);
978
- var allClientIds = this.clientIdList(type);
979
-
980
- for (var i=0, l=allClientIds.length; i<l; i++) {
981
- clientId = allClientIds[i];
982
-
983
- hash = data[clientId];
1075
+ var typeMap = this.typeMapFor(type),
1076
+ dataCache = typeMap.cidToHash,
1077
+ clientIds = typeMap.clientIds,
1078
+ clientId, hash, proxy;
1079
+
1080
+ var recordCache = get(this, 'recordCache'), record;
1081
+
1082
+ for (var i=0, l=clientIds.length; i<l; i++) {
1083
+ clientId = clientIds[i];
1084
+
1085
+ hash = dataCache[clientId];
1086
+ if (typeof hash === 'object') {
1087
+ if (record = recordCache[clientId]) {
1088
+ proxy = get(record, 'data');
1089
+ } else {
1090
+ DATA_PROXY.savedData = hash;
1091
+ proxy = DATA_PROXY;
1092
+ }
984
1093
 
985
- if (hash) {
986
- this.updateModelArray(array, filter, type, clientId, hash);
1094
+ this.updateModelArray(array, filter, type, clientId, proxy);
987
1095
  }
988
1096
  }
989
1097
  },
990
1098
 
991
- updateModelArrays: function(type, clientId, hash) {
992
- var modelArrays = get(this, 'modelArrays');
1099
+ updateModelArrays: function(type, clientId, dataProxy) {
1100
+ var modelArrays = this.typeMapFor(type).modelArrays,
1101
+ modelArrayType, filter;
993
1102
 
994
1103
  modelArrays.forEach(function(array) {
995
- modelArrayType = get(array, 'type');
996
- filter = get(array, 'filterFunction');
997
-
998
- if (type !== modelArrayType) { return; }
999
-
1000
- this.updateModelArray(array, filter, type, clientId, hash);
1104
+ filter = get(array, 'filterFunction');
1105
+ this.updateModelArray(array, filter, type, clientId, dataProxy);
1001
1106
  }, this);
1002
1107
  },
1003
1108
 
1004
- updateModelArray: function(array, filter, type, clientId, hash) {
1109
+ updateModelArray: function(array, filter, type, clientId, dataProxy) {
1005
1110
  var shouldBeInArray;
1006
1111
 
1007
1112
  if (!filter) {
1008
1113
  shouldBeInArray = true;
1009
1114
  } else {
1010
- shouldBeInArray = filter(hash);
1115
+ shouldBeInArray = filter(dataProxy);
1011
1116
  }
1012
1117
 
1013
1118
  var content = get(array, 'content');
@@ -1035,61 +1140,53 @@ DS.Store = Ember.Object.extend({
1035
1140
  },
1036
1141
 
1037
1142
  // ............
1038
- // . TYPE MAP .
1143
+ // . INDEXING .
1039
1144
  // ............
1040
1145
 
1146
+ modelArraysForClientId: function(clientId) {
1147
+ var modelArrays = get(this, 'modelArraysByClientId');
1148
+ var ret = modelArrays[clientId];
1149
+
1150
+ if (!ret) {
1151
+ ret = modelArrays[clientId] = Ember.OrderedSet.create();
1152
+ }
1153
+
1154
+ return ret;
1155
+ },
1156
+
1041
1157
  typeMapFor: function(type) {
1042
- var ids = get(this, '_typeMap');
1158
+ var typeMaps = get(this, 'typeMaps');
1043
1159
  var guidForType = Ember.guidFor(type);
1044
1160
 
1045
- var typeMap = ids[guidForType];
1161
+ var typeMap = typeMaps[guidForType];
1046
1162
 
1047
1163
  if (typeMap) {
1048
1164
  return typeMap;
1049
1165
  } else {
1050
- return (ids[guidForType] =
1166
+ return (typeMaps[guidForType] =
1051
1167
  {
1052
1168
  idToCid: {},
1053
- idList: [],
1054
- cidList: [],
1055
- cidToHash: {}
1169
+ clientIds: [],
1170
+ cidToHash: {},
1171
+ modelArrays: []
1056
1172
  });
1057
1173
  }
1058
1174
  },
1059
1175
 
1060
- idToClientIdMap: function(type) {
1061
- return this.typeMapFor(type).idToCid;
1062
- },
1063
-
1064
- idList: function(type) {
1065
- return this.typeMapFor(type).idList;
1066
- },
1067
-
1068
- clientIdList: function(type) {
1069
- return this.typeMapFor(type).cidList;
1070
- },
1071
-
1072
- clientIdToHashMap: function(type) {
1073
- return this.typeMapFor(type).cidToHash;
1074
- },
1075
-
1076
1176
  /** @private
1077
1177
 
1078
1178
  For a given type and id combination, returns the client id used by the store.
1079
- If no client id has been assigned yet, `undefined` is returned.
1179
+ If no client id has been assigned yet, one will be created and returned.
1080
1180
 
1081
1181
  @param {DS.Model} type
1082
1182
  @param {String|Number} id
1083
1183
  */
1084
1184
  clientIdForId: function(type, id) {
1085
- return this.typeMapFor(type).idToCid[id];
1086
- },
1185
+ var clientId = this.typeMapFor(type).idToCid[id];
1087
1186
 
1088
- idForHash: function(type, hash) {
1089
- var primaryKey = getPath(type, 'proto.primaryKey');
1187
+ if (clientId !== undefined) { return clientId; }
1090
1188
 
1091
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1092
- return hash[primaryKey];
1189
+ return this.pushHash(UNLOADED, id, type);
1093
1190
  },
1094
1191
 
1095
1192
  // ................
@@ -1111,29 +1208,29 @@ DS.Store = Ember.Object.extend({
1111
1208
  load: function(type, id, hash) {
1112
1209
  if (hash === undefined) {
1113
1210
  hash = id;
1114
- var primaryKey = getPath(type, 'proto.primaryKey');
1115
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1211
+ var primaryKey = type.proto().primaryKey;
1212
+
1116
1213
  id = hash[primaryKey];
1117
1214
  }
1118
1215
 
1119
- var data = this.clientIdToHashMap(type);
1120
- var models = get(this, 'models');
1121
-
1122
- var clientId = this.clientIdForId(type, id);
1216
+ var typeMap = this.typeMapFor(type),
1217
+ dataCache = typeMap.cidToHash,
1218
+ clientId = typeMap.idToCid[id],
1219
+ recordCache = get(this, 'recordCache');
1123
1220
 
1124
1221
  if (clientId !== undefined) {
1125
- data[clientId] = hash;
1222
+ dataCache[clientId] = hash;
1126
1223
 
1127
- var model = models[clientId];
1128
- if (model) {
1129
- model.willLoadData();
1130
- model.setData(hash);
1224
+ var record = recordCache[clientId];
1225
+ if (record) {
1226
+ record.send('didChangeData');
1131
1227
  }
1132
1228
  } else {
1133
1229
  clientId = this.pushHash(hash, id, type);
1134
1230
  }
1135
1231
 
1136
- this.updateModelArrays(type, clientId, hash);
1232
+ DATA_PROXY.savedData = hash;
1233
+ this.updateModelArrays(type, clientId, DATA_PROXY);
1137
1234
 
1138
1235
  return { id: id, clientId: clientId };
1139
1236
  },
@@ -1144,10 +1241,9 @@ DS.Store = Ember.Object.extend({
1144
1241
  if (hashes === undefined) {
1145
1242
  hashes = ids;
1146
1243
  ids = [];
1147
- var primaryKey = getPath(type, 'proto.primaryKey');
1244
+ var primaryKey = type.proto().primaryKey;
1148
1245
 
1149
- ids = hashes.map(function(hash) {
1150
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1246
+ ids = Ember.ArrayUtils.map(hashes, function(hash) {
1151
1247
  return hash[primaryKey];
1152
1248
  });
1153
1249
  }
@@ -1171,23 +1267,25 @@ DS.Store = Ember.Object.extend({
1171
1267
  @returns {Number}
1172
1268
  */
1173
1269
  pushHash: function(hash, id, type) {
1174
- var idToClientIdMap = this.idToClientIdMap(type);
1175
- var clientIdList = this.clientIdList(type);
1176
- var idList = this.idList(type);
1177
- var data = this.clientIdToHashMap(type);
1270
+ var typeMap = this.typeMapFor(type);
1271
+
1272
+ var idToClientIdMap = typeMap.idToCid,
1273
+ clientIdToIdMap = this.clientIdToId,
1274
+ clientIds = typeMap.clientIds,
1275
+ dataCache = typeMap.cidToHash;
1178
1276
 
1179
- var clientId = this.incrementProperty('clientIdCounter');
1277
+ var clientId = ++this.clientIdCounter;
1180
1278
 
1181
- data[clientId] = hash;
1279
+ dataCache[clientId] = hash;
1182
1280
 
1183
1281
  // if we're creating an item, this process will be done
1184
1282
  // later, once the object has been persisted.
1185
1283
  if (id) {
1186
1284
  idToClientIdMap[id] = clientId;
1187
- idList.push(id);
1285
+ clientIdToIdMap[clientId] = id;
1188
1286
  }
1189
1287
 
1190
- clientIdList.push(clientId);
1288
+ clientIds.push(clientId);
1191
1289
 
1192
1290
  return clientId;
1193
1291
  },
@@ -1196,22 +1294,35 @@ DS.Store = Ember.Object.extend({
1196
1294
  // . MODEL MATERIALIZATION .
1197
1295
  // .........................
1198
1296
 
1199
- createModel: function(type, clientId) {
1297
+ materializeRecord: function(type, clientId) {
1200
1298
  var model;
1201
1299
 
1202
- get(this, 'models')[clientId] = model = type.create({ store: this, clientId: clientId });
1203
- set(model, 'clientId', clientId);
1204
- model.loadingData();
1300
+ get(this, 'recordCache')[clientId] = model = type._create({
1301
+ store: this,
1302
+ clientId: clientId
1303
+ });
1304
+
1305
+ get(this, 'defaultTransaction').adoptRecord(model);
1306
+
1307
+ model.send('loadingData');
1205
1308
  return model;
1309
+ },
1310
+
1311
+ destroy: function() {
1312
+ if (get(DS, 'defaultStore') === this) {
1313
+ set(DS, 'defaultStore', null);
1314
+ }
1315
+
1316
+ return this._super();
1206
1317
  }
1207
1318
  });
1208
1319
 
1320
+ })();
1209
1321
 
1210
- })({});
1211
1322
 
1212
1323
 
1213
- (function(exports) {
1214
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
1324
+ (function() {
1325
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
1215
1326
 
1216
1327
  var stateProperty = Ember.computed(function(key) {
1217
1328
  var parent = get(this, 'parentState');
@@ -1220,116 +1331,413 @@ var stateProperty = Ember.computed(function(key) {
1220
1331
  }
1221
1332
  }).property();
1222
1333
 
1223
- DS.State = Ember.State.extend({
1224
- isLoaded: stateProperty,
1225
- isDirty: stateProperty,
1226
- isSaving: stateProperty,
1227
- isDeleted: stateProperty,
1228
- isError: stateProperty,
1229
- isNew: stateProperty,
1230
- isValid: stateProperty
1231
- });
1334
+ var isEmptyObject = function(object) {
1335
+ for (var name in object) {
1336
+ if (object.hasOwnProperty(name)) { return false; }
1337
+ }
1232
1338
 
1233
- var cantLoadData = function() {
1234
- // TODO: get the current state name
1235
- throw "You cannot load data into the store when its associated model is in its current state";
1339
+ return true;
1236
1340
  };
1237
1341
 
1238
- var isEmptyObject = function(obj) {
1239
- for (var prop in obj) {
1240
- if (!obj.hasOwnProperty(prop)) { continue; }
1241
- return false;
1342
+ var hasDefinedProperties = function(object) {
1343
+ for (var name in object) {
1344
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
1242
1345
  }
1243
1346
 
1244
- return true;
1347
+ return false;
1245
1348
  };
1246
1349
 
1350
+ DS.State = Ember.State.extend({
1351
+ isLoaded: stateProperty,
1352
+ isDirty: stateProperty,
1353
+ isSaving: stateProperty,
1354
+ isDeleted: stateProperty,
1355
+ isError: stateProperty,
1356
+ isNew: stateProperty,
1357
+ isValid: stateProperty,
1358
+ isPending: stateProperty,
1359
+
1360
+ // For states that are substates of a
1361
+ // DirtyState (updated or created), it is
1362
+ // useful to be able to determine which
1363
+ // type of dirty state it is.
1364
+ dirtyType: stateProperty
1365
+ });
1366
+
1247
1367
  var setProperty = function(manager, context) {
1248
1368
  var key = context.key, value = context.value;
1249
1369
 
1250
- var model = get(manager, 'model'), type = model.constructor;
1251
- var store = get(model, 'store');
1252
- var data = get(model, 'data');
1370
+ var model = get(manager, 'model'),
1371
+ data = get(model, 'data');
1372
+
1373
+ set(data, key, value);
1374
+ };
1375
+
1376
+ var setAssociation = function(manager, context) {
1377
+ var key = context.key, value = context.value;
1378
+
1379
+ var model = get(manager, 'model'),
1380
+ data = get(model, 'data');
1253
1381
 
1254
- data[key] = value;
1382
+ data.setAssociation(key, value);
1383
+ };
1384
+
1385
+ var didChangeData = function(manager) {
1386
+ var model = get(manager, 'model'),
1387
+ data = get(model, 'data');
1255
1388
 
1256
- if (store) { store.hashWasUpdated(type, get(model, 'clientId')); }
1389
+ data._savedData = null;
1390
+ model.notifyPropertyChange('data');
1257
1391
  };
1258
1392
 
1259
- // several states share extremely common functionality, so we are factoring
1260
- // them out into a common class.
1261
- var DirtyState = DS.State.extend({
1262
- // these states are virtually identical except that
1263
- // they (thrice) use their states name explicitly.
1264
- //
1265
- // child classes implement stateName.
1266
- stateName: null,
1267
- isDirty: true,
1268
- willLoadData: cantLoadData,
1393
+ // The waitingOn event shares common functionality
1394
+ // between the different dirty states, but each is
1395
+ // treated slightly differently. This method is exposed
1396
+ // so that each implementation can invoke the common
1397
+ // behavior, and then implement the behavior specific
1398
+ // to the state.
1399
+ var waitingOn = function(manager, object) {
1400
+ var model = get(manager, 'model'),
1401
+ pendingQueue = get(model, 'pendingQueue'),
1402
+ objectGuid = guidFor(object);
1403
+
1404
+ var observer = function() {
1405
+ if (get(object, 'id')) {
1406
+ manager.send('doneWaitingOn', object);
1407
+ Ember.removeObserver(object, 'id', observer);
1408
+ }
1409
+ };
1410
+
1411
+ pendingQueue[objectGuid] = [object, observer];
1412
+ Ember.addObserver(object, 'id', observer);
1413
+ };
1414
+
1415
+ // Implementation notes:
1416
+ //
1417
+ // Each state has a boolean value for all of the following flags:
1418
+ //
1419
+ // * isLoaded: The record has a populated `data` property. When a
1420
+ // record is loaded via `store.find`, `isLoaded` is false
1421
+ // until the adapter sets it. When a record is created locally,
1422
+ // its `isLoaded` property is always true.
1423
+ // * isDirty: The record has local changes that have not yet been
1424
+ // saved by the adapter. This includes records that have been
1425
+ // created (but not yet saved) or deleted.
1426
+ // * isSaving: The record's transaction has been committed, but
1427
+ // the adapter has not yet acknowledged that the changes have
1428
+ // been persisted to the backend.
1429
+ // * isDeleted: The record was marked for deletion. When `isDeleted`
1430
+ // is true and `isDirty` is true, the record is deleted locally
1431
+ // but the deletion was not yet persisted. When `isSaving` is
1432
+ // true, the change is in-flight. When both `isDirty` and
1433
+ // `isSaving` are false, the change has persisted.
1434
+ // * isError: The adapter reported that it was unable to save
1435
+ // local changes to the backend. This may also result in the
1436
+ // record having its `isValid` property become false if the
1437
+ // adapter reported that server-side validations failed.
1438
+ // * isNew: The record was created on the client and the adapter
1439
+ // did not yet report that it was successfully saved.
1440
+ // * isValid: No client-side validations have failed and the
1441
+ // adapter did not report any server-side validation failures.
1442
+ // * isPending: A record `isPending` when it belongs to an
1443
+ // association on another record and that record has not been
1444
+ // saved. A record in this state cannot be saved because it
1445
+ // lacks a "foreign key" that will be supplied by its parent
1446
+ // association when the parent record has been created. When
1447
+ // the adapter reports that the parent has saved, the
1448
+ // `isPending` property on all children will become `false`
1449
+ // and the transaction will try to commit the records.
1450
+
1451
+ // This mixin is mixed into various uncommitted states. Make
1452
+ // sure to mix it in *after* the class definition, so its
1453
+ // super points to the class definition.
1454
+ var Uncommitted = Ember.Mixin.create({
1455
+ setProperty: setProperty,
1456
+ setAssociation: setAssociation,
1269
1457
 
1270
- enter: function(manager) {
1271
- var stateName = get(this, 'stateName'),
1272
- model = get(manager, 'model');
1458
+ deleteRecord: function(manager) {
1459
+ this._super(manager);
1273
1460
 
1274
- model.withTransaction(function (t) {
1275
- t.modelBecameDirty(stateName, model);
1461
+ var model = get(manager, 'model'),
1462
+ dirtyType = get(this, 'dirtyType');
1463
+
1464
+ model.withTransaction(function(t) {
1465
+ t.modelBecameClean(dirtyType, model);
1276
1466
  });
1277
- },
1467
+ }
1468
+ });
1469
+
1470
+ // These mixins are mixed into substates of the concrete
1471
+ // subclasses of DirtyState.
1472
+
1473
+ var CreatedUncommitted = Ember.Mixin.create({
1474
+ deleteRecord: function(manager) {
1475
+ this._super(manager);
1476
+
1477
+ manager.goToState('deleted.saved');
1478
+ }
1479
+ });
1278
1480
 
1279
- exit: function(manager) {
1280
- var stateName = get(this, 'stateName'),
1281
- model = get(manager, 'model');
1481
+ var UpdatedUncommitted = Ember.Mixin.create({
1482
+ deleteRecord: function(manager) {
1483
+ this._super(manager);
1282
1484
 
1283
- this.notifyModel(model);
1485
+ var model = get(manager, 'model');
1284
1486
 
1285
- model.withTransaction(function (t) {
1286
- t.modelBecameClean(stateName, model);
1487
+ model.withTransaction(function(t) {
1488
+ t.modelBecameClean('created', model);
1287
1489
  });
1288
- },
1289
1490
 
1290
- setProperty: setProperty,
1491
+ manager.goToState('deleted');
1492
+ }
1493
+ });
1291
1494
 
1292
- willCommit: function(manager) {
1293
- manager.goToState('saving');
1294
- },
1495
+ // The dirty state is a abstract state whose functionality is
1496
+ // shared between the `created` and `updated` states.
1497
+ //
1498
+ // The deleted state shares the `isDirty` flag with the
1499
+ // subclasses of `DirtyState`, but with a very different
1500
+ // implementation.
1501
+ var DirtyState = DS.State.extend({
1502
+ initialState: 'uncommitted',
1295
1503
 
1296
- saving: DS.State.extend({
1504
+ // FLAGS
1505
+ isDirty: true,
1506
+
1507
+ // SUBSTATES
1508
+
1509
+ // When a record first becomes dirty, it is `uncommitted`.
1510
+ // This means that there are local pending changes,
1511
+ // but they have not yet begun to be saved.
1512
+ uncommitted: DS.State.extend({
1513
+ // TRANSITIONS
1514
+ enter: function(manager) {
1515
+ var dirtyType = get(this, 'dirtyType'),
1516
+ model = get(manager, 'model');
1517
+
1518
+ model.withTransaction(function (t) {
1519
+ t.modelBecameDirty(dirtyType, model);
1520
+ });
1521
+ },
1522
+
1523
+ exit: function(manager) {
1524
+ var model = get(manager, 'model');
1525
+ manager.send('invokeLifecycleCallbacks', model);
1526
+ },
1527
+
1528
+ // EVENTS
1529
+ deleteRecord: Ember.K,
1530
+
1531
+ waitingOn: function(manager, object) {
1532
+ waitingOn(manager, object);
1533
+ manager.goToState('pending');
1534
+ },
1535
+
1536
+ willCommit: function(manager) {
1537
+ manager.goToState('inFlight');
1538
+ }
1539
+ }, Uncommitted),
1540
+
1541
+ // Once a record has been handed off to the adapter to be
1542
+ // saved, it is in the 'in flight' state. Changes to the
1543
+ // record cannot be made during this window.
1544
+ inFlight: DS.State.extend({
1545
+ // FLAGS
1297
1546
  isSaving: true,
1298
1547
 
1299
- didUpdate: function(manager) {
1548
+ // TRANSITIONS
1549
+ enter: function(manager) {
1550
+ var dirtyType = get(this, 'dirtyType'),
1551
+ model = get(manager, 'model');
1552
+
1553
+ model.withTransaction(function (t) {
1554
+ t.modelBecameClean(dirtyType, model);
1555
+ });
1556
+ },
1557
+
1558
+ // EVENTS
1559
+ didCommit: function(manager) {
1300
1560
  manager.goToState('loaded');
1301
1561
  },
1302
1562
 
1303
- wasInvalid: function(manager, errors) {
1563
+ becameInvalid: function(manager, errors) {
1304
1564
  var model = get(manager, 'model');
1305
1565
 
1306
1566
  set(model, 'errors', errors);
1307
1567
  manager.goToState('invalid');
1308
- }
1568
+ },
1569
+
1570
+ didChangeData: didChangeData
1571
+ }),
1572
+
1573
+ // If a record becomes associated with a newly created
1574
+ // parent record, it will be `pending` until the parent
1575
+ // record has successfully persisted. Once this happens,
1576
+ // this record can use the parent's primary key as its
1577
+ // foreign key.
1578
+ //
1579
+ // If the record's transaction had already started to
1580
+ // commit, the record will transition to the `inFlight`
1581
+ // state. If it had not, the record will transition to
1582
+ // the `uncommitted` state.
1583
+ pending: DS.State.extend({
1584
+ initialState: 'uncommitted',
1585
+
1586
+ // FLAGS
1587
+ isPending: true,
1588
+
1589
+ // SUBSTATES
1590
+
1591
+ // A pending record whose transaction has not yet
1592
+ // started to commit is in this state.
1593
+ uncommitted: DS.State.extend({
1594
+ // EVENTS
1595
+ deleteRecord: function(manager) {
1596
+ var model = get(manager, 'model'),
1597
+ pendingQueue = get(model, 'pendingQueue'),
1598
+ tuple;
1599
+
1600
+ // since we are leaving the pending state, remove any
1601
+ // observers we have registered on other records.
1602
+ for (var prop in pendingQueue) {
1603
+ if (!pendingQueue.hasOwnProperty(prop)) { continue; }
1604
+
1605
+ tuple = pendingQueue[prop];
1606
+ Ember.removeObserver(tuple[0], 'id', tuple[1]);
1607
+ }
1608
+ },
1609
+
1610
+ willCommit: function(manager) {
1611
+ manager.goToState('committing');
1612
+ },
1613
+
1614
+ doneWaitingOn: function(manager, object) {
1615
+ var model = get(manager, 'model'),
1616
+ pendingQueue = get(model, 'pendingQueue'),
1617
+ objectGuid = guidFor(object);
1618
+
1619
+ delete pendingQueue[objectGuid];
1620
+
1621
+ if (isEmptyObject(pendingQueue)) {
1622
+ manager.send('doneWaiting');
1623
+ }
1624
+ },
1625
+
1626
+ doneWaiting: function(manager) {
1627
+ var dirtyType = get(this, 'dirtyType');
1628
+ manager.goToState(dirtyType + '.uncommitted');
1629
+ }
1630
+ }, Uncommitted),
1631
+
1632
+ // A pending record whose transaction has started
1633
+ // to commit is in this state. Since it has not yet
1634
+ // been sent to the adapter, it is not `inFlight`
1635
+ // until all of its dependencies have been committed.
1636
+ committing: DS.State.extend({
1637
+ // FLAGS
1638
+ isSaving: true,
1639
+
1640
+ // EVENTS
1641
+ doneWaitingOn: function(manager, object) {
1642
+ var model = get(manager, 'model'),
1643
+ pendingQueue = get(model, 'pendingQueue'),
1644
+ objectGuid = guidFor(object);
1645
+
1646
+ delete pendingQueue[objectGuid];
1647
+
1648
+ if (isEmptyObject(pendingQueue)) {
1649
+ manager.send('doneWaiting');
1650
+ }
1651
+ },
1652
+
1653
+ doneWaiting: function(manager) {
1654
+ var model = get(manager, 'model'),
1655
+ transaction = get(model, 'transaction');
1656
+
1657
+ // Now that the model is no longer pending, schedule
1658
+ // the transaction to commit.
1659
+ Ember.run.once(transaction, transaction.commit);
1660
+ },
1661
+
1662
+ willCommit: function(manager) {
1663
+ var dirtyType = get(this, 'dirtyType');
1664
+ manager.goToState(dirtyType + '.inFlight');
1665
+ }
1666
+ })
1309
1667
  }),
1310
1668
 
1669
+ // A record is in the `invalid` state when its client-side
1670
+ // invalidations have failed, or if the adapter has indicated
1671
+ // the the record failed server-side invalidations.
1311
1672
  invalid: DS.State.extend({
1673
+ // FLAGS
1312
1674
  isValid: false,
1313
1675
 
1676
+ // EVENTS
1677
+ deleteRecord: function(manager) {
1678
+ manager.goToState('deleted');
1679
+ },
1680
+
1681
+ setAssociation: setAssociation,
1682
+
1314
1683
  setProperty: function(manager, context) {
1315
1684
  setProperty(manager, context);
1316
1685
 
1317
- var stateName = getPath(this, 'parentState.stateName'),
1318
- model = get(manager, 'model'),
1686
+ var model = get(manager, 'model'),
1319
1687
  errors = get(model, 'errors'),
1320
1688
  key = context.key;
1321
1689
 
1322
1690
  delete errors[key];
1323
1691
 
1324
- if (isEmptyObject(errors)) {
1325
- manager.goToState(stateName);
1692
+ if (!hasDefinedProperties(errors)) {
1693
+ manager.send('becameValid');
1326
1694
  }
1695
+ },
1696
+
1697
+ becameValid: function(manager) {
1698
+ manager.goToState('uncommitted');
1327
1699
  }
1328
1700
  })
1329
1701
  });
1330
1702
 
1703
+ // The created and updated states are created outside the state
1704
+ // chart so we can reopen their substates and add mixins as
1705
+ // necessary.
1706
+
1707
+ var createdState = DirtyState.create({
1708
+ dirtyType: 'created',
1709
+
1710
+ // FLAGS
1711
+ isNew: true,
1712
+
1713
+ // EVENTS
1714
+ invokeLifecycleCallbacks: function(manager, model) {
1715
+ model.didCreate();
1716
+ }
1717
+ });
1718
+
1719
+ var updatedState = DirtyState.create({
1720
+ dirtyType: 'updated',
1721
+
1722
+ // EVENTS
1723
+ invokeLifecycleCallbacks: function(manager, model) {
1724
+ model.didUpdate();
1725
+ }
1726
+ });
1727
+
1728
+ // The created.uncommitted state and created.pending.uncommitted share
1729
+ // some logic defined in CreatedUncommitted.
1730
+ createdState.states.uncommitted.reopen(CreatedUncommitted);
1731
+ createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
1732
+
1733
+ // The updated.uncommitted state and updated.pending.uncommitted share
1734
+ // some logic defined in UpdatedUncommitted.
1735
+ updatedState.states.uncommitted.reopen(UpdatedUncommitted);
1736
+ updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
1737
+
1331
1738
  var states = {
1332
1739
  rootState: Ember.State.create({
1740
+ // FLAGS
1333
1741
  isLoaded: false,
1334
1742
  isDirty: false,
1335
1743
  isSaving: false,
@@ -1337,118 +1745,168 @@ var states = {
1337
1745
  isError: false,
1338
1746
  isNew: false,
1339
1747
  isValid: true,
1748
+ isPending: false,
1340
1749
 
1341
- willLoadData: cantLoadData,
1342
-
1343
- didCreate: function(manager) {
1344
- manager.goToState('loaded.created');
1345
- },
1750
+ // SUBSTATES
1346
1751
 
1752
+ // A record begins its lifecycle in the `empty` state.
1753
+ // If its data will come from the adapter, it will
1754
+ // transition into the `loading` state. Otherwise, if
1755
+ // the record is being created on the client, it will
1756
+ // transition into the `created` state.
1347
1757
  empty: DS.State.create({
1758
+ // EVENTS
1348
1759
  loadingData: function(manager) {
1349
1760
  manager.goToState('loading');
1761
+ },
1762
+
1763
+ didChangeData: function(manager) {
1764
+ didChangeData(manager);
1765
+
1766
+ manager.goToState('loaded.created');
1350
1767
  }
1351
1768
  }),
1352
1769
 
1770
+ // A record enters this state when the store askes
1771
+ // the adapter for its data. It remains in this state
1772
+ // until the adapter provides the requested data.
1773
+ //
1774
+ // Usually, this process is asynchronous, using an
1775
+ // XHR to retrieve the data.
1353
1776
  loading: DS.State.create({
1354
- willLoadData: Ember.K,
1355
-
1777
+ // TRANSITIONS
1356
1778
  exit: function(manager) {
1357
1779
  var model = get(manager, 'model');
1358
1780
  model.didLoad();
1359
1781
  },
1360
1782
 
1361
- setData: function(manager, data) {
1362
- var model = get(manager, 'model');
1363
-
1364
- model.beginPropertyChanges();
1365
- model.set('data', data);
1366
-
1367
- if (data !== null) {
1368
- manager.goToState('loaded');
1369
- }
1783
+ // EVENTS
1784
+ didChangeData: function(manager, data) {
1785
+ didChangeData(manager);
1786
+ manager.send('loadedData');
1787
+ },
1370
1788
 
1371
- model.endPropertyChanges();
1789
+ loadedData: function(manager) {
1790
+ manager.goToState('loaded');
1372
1791
  }
1373
1792
  }),
1374
1793
 
1794
+ // A record enters this state when its data is populated.
1795
+ // Most of a record's lifecycle is spent inside substates
1796
+ // of the `loaded` state.
1375
1797
  loaded: DS.State.create({
1798
+ initialState: 'saved',
1799
+
1800
+ // FLAGS
1376
1801
  isLoaded: true,
1377
1802
 
1378
- willLoadData: Ember.K,
1803
+ // SUBSTATES
1379
1804
 
1380
- setProperty: function(manager, context) {
1381
- setProperty(manager, context);
1382
- manager.goToState('updated');
1383
- },
1805
+ // If there are no local changes to a record, it remains
1806
+ // in the `saved` state.
1807
+ saved: DS.State.create({
1808
+ // EVENTS
1809
+ setProperty: function(manager, context) {
1810
+ setProperty(manager, context);
1811
+ manager.goToState('updated');
1812
+ },
1384
1813
 
1385
- 'delete': function(manager) {
1386
- manager.goToState('deleted');
1387
- },
1814
+ setAssociation: function(manager, context) {
1815
+ setAssociation(manager, context);
1816
+ manager.goToState('updated');
1817
+ },
1388
1818
 
1389
- created: DirtyState.create({
1390
- stateName: 'created',
1391
- isNew: true,
1819
+ didChangeData: didChangeData,
1392
1820
 
1393
- notifyModel: function(model) {
1394
- model.didCreate();
1821
+ deleteRecord: function(manager) {
1822
+ manager.goToState('deleted');
1823
+ },
1824
+
1825
+ waitingOn: function(manager, object) {
1826
+ waitingOn(manager, object);
1827
+ manager.goToState('updated.pending');
1395
1828
  }
1396
1829
  }),
1397
1830
 
1398
- updated: DirtyState.create({
1399
- stateName: 'updated',
1831
+ // A record is in this state after it has been locally
1832
+ // created but before the adapter has indicated that
1833
+ // it has been saved.
1834
+ created: createdState,
1400
1835
 
1401
- notifyModel: function(model) {
1402
- model.didUpdate();
1403
- }
1404
- })
1836
+ // A record is in this state if it has already been
1837
+ // saved to the server, but there are new local changes
1838
+ // that have not yet been saved.
1839
+ updated: updatedState
1405
1840
  }),
1406
1841
 
1842
+ // A record is in this state if it was deleted from the store.
1407
1843
  deleted: DS.State.create({
1844
+ // FLAGS
1408
1845
  isDeleted: true,
1409
1846
  isLoaded: true,
1410
1847
  isDirty: true,
1411
1848
 
1412
- willLoadData: cantLoadData,
1849
+ // SUBSTATES
1413
1850
 
1414
- enter: function(manager) {
1415
- var model = get(manager, 'model');
1416
- var store = get(model, 'store');
1851
+ // When a record is deleted, it enters the `start`
1852
+ // state. It will exit this state when the record's
1853
+ // transaction starts to commit.
1854
+ start: DS.State.create({
1855
+ // TRANSITIONS
1856
+ enter: function(manager) {
1857
+ var model = get(manager, 'model');
1858
+ var store = get(model, 'store');
1417
1859
 
1418
- if (store) {
1419
- store.removeFromModelArrays(model);
1420
- }
1860
+ if (store) {
1861
+ store.removeFromModelArrays(model);
1862
+ }
1421
1863
 
1422
- model.withTransaction(function(t) {
1423
- t.modelBecameDirty('deleted', model);
1424
- });
1425
- },
1864
+ model.withTransaction(function(t) {
1865
+ t.modelBecameDirty('deleted', model);
1866
+ });
1867
+ },
1426
1868
 
1427
- willCommit: function(manager) {
1428
- manager.goToState('saving');
1429
- },
1869
+ // EVENTS
1870
+ willCommit: function(manager) {
1871
+ manager.goToState('inFlight');
1872
+ }
1873
+ }),
1430
1874
 
1431
- saving: DS.State.create({
1875
+ // After a record's transaction is committing, but
1876
+ // before the adapter indicates that the deletion
1877
+ // has saved to the server, a record is in the
1878
+ // `inFlight` substate of `deleted`.
1879
+ inFlight: DS.State.create({
1880
+ // FLAGS
1432
1881
  isSaving: true,
1433
1882
 
1434
- didDelete: function(manager) {
1435
- manager.goToState('saved');
1436
- },
1437
-
1883
+ // TRANSITIONS
1438
1884
  exit: function(stateManager) {
1439
1885
  var model = get(stateManager, 'model');
1440
1886
 
1441
1887
  model.withTransaction(function(t) {
1442
1888
  t.modelBecameClean('deleted', model);
1443
1889
  });
1890
+ },
1891
+
1892
+ // EVENTS
1893
+ didCommit: function(manager) {
1894
+ manager.goToState('saved');
1444
1895
  }
1445
1896
  }),
1446
1897
 
1898
+ // Once the adapter indicates that the deletion has
1899
+ // been saved, the record enters the `saved` substate
1900
+ // of `deleted`.
1447
1901
  saved: DS.State.create({
1902
+ // FLAGS
1448
1903
  isDirty: false
1449
1904
  })
1450
1905
  }),
1451
1906
 
1907
+ // If the adapter indicates that there was an unknown
1908
+ // error saving a record, the record enters the `error`
1909
+ // state.
1452
1910
  error: DS.State.create({
1453
1911
  isError: true
1454
1912
  })
@@ -1461,210 +1919,536 @@ DS.StateManager = Ember.StateManager.extend({
1461
1919
  states: states
1462
1920
  });
1463
1921
 
1464
- var retrieveFromCurrentState = Ember.computed(function(key) {
1465
- return get(getPath(this, 'stateManager.currentState'), key);
1466
- }).property('stateManager.currentState').cacheable();
1922
+ })();
1467
1923
 
1468
- DS.Model = Ember.Object.extend({
1469
- isLoaded: retrieveFromCurrentState,
1470
- isDirty: retrieveFromCurrentState,
1471
- isSaving: retrieveFromCurrentState,
1472
- isDeleted: retrieveFromCurrentState,
1473
- isError: retrieveFromCurrentState,
1474
- isNew: retrieveFromCurrentState,
1475
- isValid: retrieveFromCurrentState,
1476
1924
 
1477
- clientId: null,
1478
1925
 
1479
- // because unknownProperty is used, any internal property
1480
- // must be initialized here.
1481
- primaryKey: 'id',
1482
- data: null,
1483
- transaction: null,
1926
+ (function() {
1927
+ var get = Ember.get, set = Ember.set;
1484
1928
 
1485
- didLoad: Ember.K,
1486
- didUpdate: Ember.K,
1487
- didCreate: Ember.K,
1929
+ // This object is a regular JS object for performance. It is only
1930
+ // used internally for bookkeeping purposes.
1931
+ var DataProxy = DS._DataProxy = function(record) {
1932
+ this.record = record;
1933
+ this.unsavedData = {};
1934
+ this.associations = {};
1935
+ };
1488
1936
 
1489
- init: function() {
1490
- var stateManager = DS.StateManager.create({
1491
- model: this
1492
- });
1937
+ DataProxy.prototype = {
1938
+ get: function(key) { return Ember.get(this, key); },
1939
+ set: function(key, value) { return Ember.set(this, key, value); },
1493
1940
 
1494
- set(this, 'stateManager', stateManager);
1495
- stateManager.goToState('empty');
1941
+ setAssociation: function(key, value) {
1942
+ this.associations[key] = value;
1496
1943
  },
1497
1944
 
1498
- withTransaction: function(fn) {
1499
- var transaction = get(this, 'transaction') || getPath(this, 'store.defaultTransaction');
1945
+ savedData: function() {
1946
+ var savedData = this._savedData;
1947
+ if (savedData) { return savedData; }
1500
1948
 
1501
- if (transaction) { fn(transaction); }
1502
- },
1949
+ var record = this.record,
1950
+ clientId = get(record, 'clientId'),
1951
+ store = get(record, 'store');
1503
1952
 
1504
- setData: function(data) {
1505
- var stateManager = get(this, 'stateManager');
1506
- stateManager.send('setData', data);
1953
+ if (store) {
1954
+ savedData = store.dataForRecord(record);
1955
+ this._savedData = savedData;
1956
+ return savedData;
1957
+ }
1507
1958
  },
1508
1959
 
1509
- setProperty: function(key, value) {
1510
- var stateManager = get(this, 'stateManager');
1511
- stateManager.send('setProperty', { key: key, value: value });
1512
- },
1960
+ unknownProperty: function(key) {
1961
+ var unsavedData = this.unsavedData,
1962
+ associations = this.associations,
1963
+ savedData = this.savedData(),
1964
+ store;
1513
1965
 
1514
- deleteRecord: function() {
1515
- var stateManager = get(this, 'stateManager');
1516
- stateManager.send('delete');
1517
- },
1966
+ var value = unsavedData[key], association;
1518
1967
 
1519
- destroy: function() {
1520
- this.deleteRecord();
1521
- this._super();
1522
- },
1968
+ // if this is a belongsTo association, this will
1969
+ // be a clientId.
1970
+ association = associations[key];
1523
1971
 
1524
- loadingData: function() {
1525
- var stateManager = get(this, 'stateManager');
1526
- stateManager.send('loadingData');
1527
- },
1972
+ if (association !== undefined) {
1973
+ store = get(this.record, 'store');
1974
+ return store.clientIdToId[association];
1975
+ }
1528
1976
 
1529
- willLoadData: function() {
1530
- var stateManager = get(this, 'stateManager');
1531
- stateManager.send('willLoadData');
1532
- },
1977
+ if (savedData && value === undefined) {
1978
+ value = savedData[key];
1979
+ }
1533
1980
 
1534
- willCommit: function() {
1535
- var stateManager = get(this, 'stateManager');
1536
- stateManager.send('willCommit');
1981
+ return value;
1537
1982
  },
1538
1983
 
1539
- adapterDidUpdate: function() {
1540
- var stateManager = get(this, 'stateManager');
1541
- stateManager.send('didUpdate');
1542
- },
1984
+ setUnknownProperty: function(key, value) {
1985
+ var record = this.record,
1986
+ unsavedData = this.unsavedData;
1543
1987
 
1544
- adapterDidCreate: function() {
1545
- var stateManager = get(this, 'stateManager');
1546
- stateManager.send('didCreate');
1547
- },
1988
+ unsavedData[key] = value;
1548
1989
 
1549
- adapterDidDelete: function() {
1550
- var stateManager = get(this, 'stateManager');
1551
- stateManager.send('didDelete');
1552
- },
1990
+ record.hashWasUpdated();
1553
1991
 
1554
- wasInvalid: function(errors) {
1555
- var stateManager = get(this, 'stateManager');
1556
- stateManager.send('wasInvalid', errors);
1992
+ return value;
1557
1993
  },
1558
1994
 
1559
- unknownProperty: function(key) {
1560
- var data = get(this, 'data');
1995
+ commit: function() {
1996
+ var record = this.record;
1997
+
1998
+ var unsavedData = this.unsavedData;
1999
+ var savedData = this.savedData();
1561
2000
 
1562
- if (data) {
1563
- return get(data, key);
2001
+ for (var prop in unsavedData) {
2002
+ if (unsavedData.hasOwnProperty(prop)) {
2003
+ savedData[prop] = unsavedData[prop];
2004
+ delete unsavedData[prop];
2005
+ }
1564
2006
  }
2007
+
2008
+ record.notifyPropertyChange('data');
1565
2009
  },
1566
2010
 
1567
- setUnknownProperty: function(key, value) {
1568
- var data = get(this, 'data');
1569
- ember_assert("You cannot set a model attribute before its data is loaded.", !!data);
2011
+ rollback: function() {
2012
+ this.unsavedData = {};
2013
+ },
1570
2014
 
1571
- this.setProperty(key, value);
1572
- return value;
2015
+ adapterDidUpdate: function(data) {
2016
+ this.unsavedData = {};
1573
2017
  }
1574
- });
2018
+ };
1575
2019
 
1576
- DS.attr = function(type, options) {
1577
- var transform = DS.attr.transforms[type];
1578
- var transformFrom = transform.from;
1579
- var transformTo = transform.to;
2020
+ })();
1580
2021
 
1581
- return Ember.computed(function(key, value) {
1582
- var data = get(this, 'data');
1583
2022
 
1584
- key = (options && options.key) ? options.key : key;
1585
2023
 
1586
- if (value === undefined) {
1587
- if (!data) { return; }
2024
+ (function() {
2025
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none;
1588
2026
 
1589
- return transformFrom(data[key]);
1590
- } else {
1591
- ember_assert("You cannot set a model attribute before its data is loaded.", !!data);
2027
+ var retrieveFromCurrentState = Ember.computed(function(key) {
2028
+ return get(getPath(this, 'stateManager.currentState'), key);
2029
+ }).property('stateManager.currentState').cacheable();
1592
2030
 
1593
- value = transformTo(value);
1594
- this.setProperty(key, value);
2031
+ DS.Model = Ember.Object.extend({
2032
+ isLoaded: retrieveFromCurrentState,
2033
+ isDirty: retrieveFromCurrentState,
2034
+ isSaving: retrieveFromCurrentState,
2035
+ isDeleted: retrieveFromCurrentState,
2036
+ isError: retrieveFromCurrentState,
2037
+ isNew: retrieveFromCurrentState,
2038
+ isPending: retrieveFromCurrentState,
2039
+ isValid: retrieveFromCurrentState,
2040
+
2041
+ clientId: null,
2042
+ transaction: null,
2043
+ stateManager: null,
2044
+ pendingQueue: null,
2045
+ errors: null,
2046
+
2047
+ // because unknownProperty is used, any internal property
2048
+ // must be initialized here.
2049
+ primaryKey: 'id',
2050
+ id: Ember.computed(function(key, value) {
2051
+ var primaryKey = get(this, 'primaryKey'),
2052
+ data = get(this, 'data');
2053
+
2054
+ if (arguments.length === 2) {
2055
+ set(data, primaryKey, value);
1595
2056
  return value;
1596
2057
  }
1597
- }).property('data');
1598
- };
1599
2058
 
1600
- var embeddedFindRecord = function(store, type, data, key, one) {
1601
- var association = data ? get(data, key) : one ? null : [];
1602
- if (one) {
1603
- return association ? store.load(type, association).id : null;
1604
- } else {
1605
- return association ? store.loadMany(type, association).ids : [];
1606
- }
1607
- };
2059
+ return data && get(data, primaryKey);
2060
+ }).property('primaryKey', 'data'),
1608
2061
 
1609
- var referencedFindRecord = function(store, type, data, key, one) {
1610
- return data ? get(data, key) : one ? null : [];
1611
- };
2062
+ // The following methods are callbacks invoked by `getJSON`. You
2063
+ // can override one of the callbacks to override specific behavior,
2064
+ // or getJSON itself.
2065
+ //
2066
+ // If you override getJSON, you can invoke these callbacks manually
2067
+ // to get the default behavior.
1612
2068
 
1613
- var hasAssociation = function(type, options, one) {
1614
- var embedded = options && options.embedded,
1615
- findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2069
+ /**
2070
+ Add the record's primary key to the JSON hash.
1616
2071
 
1617
- return Ember.computed(function(key) {
1618
- var data = get(this, 'data'), ids, id, association,
1619
- store = get(this, 'store');
2072
+ The default implementation uses the record's specified `primaryKey`
2073
+ and the `id` computed property, which are passed in as parameters.
1620
2074
 
1621
- if (typeof type === 'string') { type = getPath(this, type); }
2075
+ @param {Object} json the JSON hash being built
2076
+ @param {Number|String} id the record's id
2077
+ @param {String} key the primaryKey for the record
2078
+ */
2079
+ addIdToJSON: function(json, id, key) {
2080
+ if (id) { json[key] = id; }
2081
+ },
1622
2082
 
1623
- key = (options && options.key) ? options.key : key;
1624
- if (one) {
1625
- id = findRecord(store, type, data, key, true);
1626
- association = id ? store.find(type, id) : null;
2083
+ /**
2084
+ Add the attributes' current values to the JSON hash.
2085
+
2086
+ The default implementation gets the current value of each
2087
+ attribute from the `data`, and uses a `defaultValue` if
2088
+ specified in the `DS.attr` definition.
2089
+
2090
+ @param {Object} json the JSON hash being build
2091
+ @param {Ember.Map} attributes a Map of attributes
2092
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2093
+ */
2094
+ addAttributesToJSON: function(json, attributes, data) {
2095
+ attributes.forEach(function(name, meta) {
2096
+ var key = meta.key(this.constructor),
2097
+ value = get(data, key);
2098
+
2099
+ if (value === undefined) {
2100
+ value = meta.options.defaultValue;
2101
+ }
2102
+
2103
+ json[key] = value;
2104
+ }, this);
2105
+ },
2106
+
2107
+ /**
2108
+ Add the value of a `hasMany` association to the JSON hash.
2109
+
2110
+ The default implementation honors the `embedded` option
2111
+ passed to `DS.hasMany`. If embedded, `toJSON` is recursively
2112
+ called on the child records. If not, the `id` of each
2113
+ record is added.
2114
+
2115
+ Note that if a record is not embedded and does not
2116
+ yet have an `id` (usually provided by the server), it
2117
+ will not be included in the output.
2118
+
2119
+ @param {Object} json the JSON hash being built
2120
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2121
+ @param {Object} meta information about the association
2122
+ @param {Object} options options passed to `toJSON`
2123
+ */
2124
+ addHasManyToJSON: function(json, data, meta, options) {
2125
+ var key = meta.key,
2126
+ manyArray = get(this, key),
2127
+ records = [],
2128
+ clientId, id;
2129
+
2130
+ if (meta.options.embedded) {
2131
+ // TODO: Avoid materializing embedded hashes if possible
2132
+ manyArray.forEach(function(record) {
2133
+ records.push(record.toJSON(options));
2134
+ });
1627
2135
  } else {
1628
- ids = findRecord(store, type, data, key);
1629
- association = store.findMany(type, ids);
2136
+ var clientIds = get(manyArray, 'content');
2137
+
2138
+ for (var i=0, l=clientIds.length; i<l; i++) {
2139
+ clientId = clientIds[i];
2140
+ id = get(this, 'store').clientIdToId[clientId];
2141
+
2142
+ if (id !== undefined) {
2143
+ records.push(id);
2144
+ }
2145
+ }
1630
2146
  }
1631
2147
 
1632
- return association;
1633
- }).property('data').cacheable();
1634
- };
2148
+ json[key] = records;
2149
+ },
1635
2150
 
1636
- DS.hasMany = function(type, options) {
1637
- ember_assert("The type passed to DS.hasMany must be defined", !!type);
1638
- return hasAssociation(type, options);
2151
+ /**
2152
+ Add the value of a `belongsTo` association to the JSON hash.
2153
+
2154
+ The default implementation always includes the `id`.
2155
+
2156
+ @param {Object} json the JSON hash being built
2157
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2158
+ @param {Object} meta information about the association
2159
+ @param {Object} options options passed to `toJSON`
2160
+ */
2161
+ addBelongsToToJSON: function(json, data, meta, options) {
2162
+ var key = meta.key, value, id;
2163
+
2164
+ if (options.embedded) {
2165
+ key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
2166
+ value = get(data.record, key);
2167
+ json[key] = value ? value.toJSON(options) : null;
2168
+ } else {
2169
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
2170
+ id = data.get(key);
2171
+ json[key] = none(id) ? null : id;
2172
+ }
2173
+ },
2174
+ /**
2175
+ Create a JSON representation of the record, including its `id`,
2176
+ attributes and associations. Honor any settings defined on the
2177
+ attributes or associations (such as `embedded` or `key`).
2178
+ */
2179
+ toJSON: function(options) {
2180
+ var data = get(this, 'data'),
2181
+ result = {},
2182
+ type = this.constructor,
2183
+ attributes = get(type, 'attributes'),
2184
+ primaryKey = get(this, 'primaryKey'),
2185
+ id = get(this, 'id'),
2186
+ store = get(this, 'store'),
2187
+ associations;
2188
+
2189
+ options = options || {};
2190
+
2191
+ // delegate to `addIdToJSON` callback
2192
+ this.addIdToJSON(result, id, primaryKey);
2193
+
2194
+ // delegate to `addAttributesToJSON` callback
2195
+ this.addAttributesToJSON(result, attributes, data);
2196
+
2197
+ associations = get(type, 'associationsByName');
2198
+
2199
+ // add associations, delegating to `addHasManyToJSON` and
2200
+ // `addBelongsToToJSON`.
2201
+ associations.forEach(function(key, meta) {
2202
+ if (options.associations && meta.kind === 'hasMany') {
2203
+ this.addHasManyToJSON(result, data, meta, options);
2204
+ } else if (meta.kind === 'belongsTo') {
2205
+ this.addBelongsToToJSON(result, data, meta, options);
2206
+ }
2207
+ }, this);
2208
+
2209
+ return result;
2210
+ },
2211
+
2212
+ data: Ember.computed(function() {
2213
+ return new DS._DataProxy(this);
2214
+ }).cacheable(),
2215
+
2216
+ didLoad: Ember.K,
2217
+ didUpdate: Ember.K,
2218
+ didCreate: Ember.K,
2219
+
2220
+ init: function() {
2221
+ var stateManager = DS.StateManager.create({
2222
+ model: this
2223
+ });
2224
+
2225
+ set(this, 'pendingQueue', {});
2226
+
2227
+ set(this, 'stateManager', stateManager);
2228
+ stateManager.goToState('empty');
2229
+ },
2230
+
2231
+ destroy: function() {
2232
+ if (!get(this, 'isDeleted')) {
2233
+ this.deleteRecord();
2234
+ }
2235
+ this._super();
2236
+ },
2237
+
2238
+ send: function(name, context) {
2239
+ return get(this, 'stateManager').send(name, context);
2240
+ },
2241
+
2242
+ withTransaction: function(fn) {
2243
+ var transaction = get(this, 'transaction');
2244
+ if (transaction) { fn(transaction); }
2245
+ },
2246
+
2247
+ setProperty: function(key, value) {
2248
+ this.send('setProperty', { key: key, value: value });
2249
+ },
2250
+
2251
+ deleteRecord: function() {
2252
+ this.send('deleteRecord');
2253
+ },
2254
+
2255
+ waitingOn: function(record) {
2256
+ this.send('waitingOn', record);
2257
+ },
2258
+
2259
+ notifyHashWasUpdated: function() {
2260
+ var store = get(this, 'store');
2261
+ if (store) {
2262
+ store.hashWasUpdated(this.constructor, get(this, 'clientId'), this);
2263
+ }
2264
+ },
2265
+
2266
+ unknownProperty: function(key) {
2267
+ var data = get(this, 'data');
2268
+
2269
+ if (data && key in data) {
2270
+
2271
+ }
2272
+ },
2273
+
2274
+ setUnknownProperty: function(key, value) {
2275
+ var data = get(this, 'data');
2276
+
2277
+ if (data && key in data) {
2278
+
2279
+ } else {
2280
+ return this._super(key, value);
2281
+ }
2282
+ },
2283
+
2284
+ namingConvention: {
2285
+ keyToJSONKey: function(key) {
2286
+ // TODO: Strip off `is` from the front. Example: `isHipster` becomes `hipster`
2287
+ return Ember.String.decamelize(key);
2288
+ },
2289
+
2290
+ foreignKey: function(key) {
2291
+ return Ember.String.decamelize(key) + '_id';
2292
+ }
2293
+ },
2294
+
2295
+ /** @private */
2296
+ hashWasUpdated: function() {
2297
+ // At the end of the run loop, notify model arrays that
2298
+ // this record has changed so they can re-evaluate its contents
2299
+ // to determine membership.
2300
+ Ember.run.once(this, this.notifyHashWasUpdated);
2301
+ },
2302
+
2303
+ dataDidChange: Ember.observer(function() {
2304
+ var associations = get(this.constructor, 'associationsByName'),
2305
+ data = get(this, 'data'), store = get(this, 'store'),
2306
+ idToClientId = store.idToClientId,
2307
+ cachedValue;
2308
+
2309
+ associations.forEach(function(name, association) {
2310
+ if (association.kind === 'hasMany') {
2311
+ cachedValue = this.cacheFor(name);
2312
+
2313
+ if (cachedValue) {
2314
+ var ids = data.get(name) || [];
2315
+ var clientIds = Ember.ArrayUtils.map(ids, function(id) {
2316
+ return store.clientIdForId(association.type, id);
2317
+ });
2318
+
2319
+ set(cachedValue, 'content', Ember.A(clientIds));
2320
+ cachedValue.fetch();
2321
+ }
2322
+ }
2323
+ }, this);
2324
+ }, 'data')
2325
+ });
2326
+
2327
+ // Helper function to generate store aliases.
2328
+ // This returns a function that invokes the named alias
2329
+ // on the default store, but injects the class as the
2330
+ // first parameter.
2331
+ var storeAlias = function(methodName) {
2332
+ return function() {
2333
+ var store = get(DS, 'defaultStore'),
2334
+ args = [].slice.call(arguments);
2335
+
2336
+ args.unshift(this);
2337
+ return store[methodName].apply(store, args);
2338
+ };
1639
2339
  };
1640
2340
 
1641
- DS.hasOne = function(type, options) {
1642
- ember_assert("The type passed to DS.hasOne must be defined", !!type);
1643
- return hasAssociation(type, options, true);
2341
+ DS.Model.reopenClass({
2342
+ find: storeAlias('find'),
2343
+ filter: storeAlias('filter'),
2344
+
2345
+ _create: DS.Model.create,
2346
+
2347
+ create: function() {
2348
+ throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set.");
2349
+ },
2350
+
2351
+ createRecord: storeAlias('createRecord')
2352
+ });
2353
+
2354
+ })();
2355
+
2356
+
2357
+
2358
+ (function() {
2359
+ var get = Ember.get, getPath = Ember.getPath;
2360
+ DS.Model.reopenClass({
2361
+ attributes: Ember.computed(function() {
2362
+ var map = Ember.Map.create();
2363
+
2364
+ this.eachComputedProperty(function(name, meta) {
2365
+ if (meta.isAttribute) { map.set(name, meta); }
2366
+ });
2367
+
2368
+ return map;
2369
+ }).cacheable(),
2370
+
2371
+ processAttributeKeys: function() {
2372
+ if (this.processedAttributeKeys) { return; }
2373
+
2374
+ var namingConvention = this.proto().namingConvention;
2375
+
2376
+ this.eachComputedProperty(function(name, meta) {
2377
+ if (meta.isAttribute && !meta.options.key) {
2378
+ meta.options.key = namingConvention.keyToJSONKey(name, this);
2379
+ }
2380
+ }, this);
2381
+ }
2382
+ });
2383
+
2384
+ DS.attr = function(type, options) {
2385
+ var transform = DS.attr.transforms[type];
2386
+
2387
+
2388
+ var transformFrom = transform.from;
2389
+ var transformTo = transform.to;
2390
+
2391
+ options = options || {};
2392
+
2393
+ var meta = {
2394
+ type: type,
2395
+ isAttribute: true,
2396
+ options: options,
2397
+
2398
+ // this will ensure that the key always takes naming
2399
+ // conventions into consideration.
2400
+ key: function(recordType) {
2401
+ recordType.processAttributeKeys();
2402
+ return options.key;
2403
+ }
2404
+ };
2405
+
2406
+ return Ember.computed(function(key, value) {
2407
+ var data;
2408
+
2409
+ key = meta.key(this.constructor);
2410
+
2411
+ if (arguments.length === 2) {
2412
+ value = transformTo(value);
2413
+ this.setProperty(key, value);
2414
+ } else {
2415
+ data = get(this, 'data');
2416
+ value = get(data, key);
2417
+
2418
+ if (value === undefined) {
2419
+ value = options.defaultValue;
2420
+ }
2421
+ }
2422
+
2423
+ return transformFrom(value);
2424
+ // `data` is never set directly. However, it may be
2425
+ // invalidated from the state manager's setData
2426
+ // event.
2427
+ }).property('data').cacheable().meta(meta);
1644
2428
  };
1645
2429
 
1646
2430
  DS.attr.transforms = {
1647
2431
  string: {
1648
2432
  from: function(serialized) {
1649
- return Em.none(serialized) ? null : String(serialized);
2433
+ return Ember.none(serialized) ? null : String(serialized);
1650
2434
  },
1651
2435
 
1652
2436
  to: function(deserialized) {
1653
- return Em.none(deserialized) ? null : String(deserialized);
2437
+ return Ember.none(deserialized) ? null : String(deserialized);
1654
2438
  }
1655
2439
  },
1656
2440
 
1657
- integer: {
2441
+ number: {
1658
2442
  from: function(serialized) {
1659
- return Em.none(serialized) ? null : Number(serialized);
2443
+ return Ember.none(serialized) ? null : Number(serialized);
1660
2444
  },
1661
2445
 
1662
2446
  to: function(deserialized) {
1663
- return Em.none(deserialized) ? null : Number(deserialized);
2447
+ return Ember.none(deserialized) ? null : Number(deserialized);
1664
2448
  }
1665
2449
  },
1666
2450
 
1667
- boolean: {
2451
+ 'boolean': {
1668
2452
  from: function(serialized) {
1669
2453
  return Boolean(serialized);
1670
2454
  },
@@ -1722,27 +2506,532 @@ DS.attr.transforms = {
1722
2506
  }
1723
2507
  };
1724
2508
 
1725
- })({});
1726
2509
 
2510
+ })();
1727
2511
 
1728
- (function(exports) {
1729
- //Copyright (C) 2011 by Living Social, Inc.
1730
2512
 
1731
- //Permission is hereby granted, free of charge, to any person obtaining a copy of
1732
- //this software and associated documentation files (the "Software"), to deal in
1733
- //the Software without restriction, including without limitation the rights to
1734
- //use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
1735
- //of the Software, and to permit persons to whom the Software is furnished to do
1736
- //so, subject to the following conditions:
1737
2513
 
1738
- //The above copyright notice and this permission notice shall be included in all
1739
- //copies or substantial portions of the Software.
2514
+ (function() {
2515
+
2516
+ })();
2517
+
2518
+
2519
+
2520
+ (function() {
2521
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath,
2522
+ none = Ember.none;
2523
+
2524
+ var embeddedFindRecord = function(store, type, data, key, one) {
2525
+ var association = get(data, key);
2526
+ return none(association) ? undefined : store.load(type, association).id;
2527
+ };
2528
+
2529
+ var referencedFindRecord = function(store, type, data, key, one) {
2530
+ return get(data, key);
2531
+ };
2532
+
2533
+ var hasAssociation = function(type, options, one) {
2534
+ options = options || {};
2535
+
2536
+ var embedded = options.embedded,
2537
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2538
+
2539
+ var meta = { type: type, isAssociation: true, options: options, kind: 'belongsTo' };
2540
+
2541
+ return Ember.computed(function(key, value) {
2542
+ var data = get(this, 'data'), ids, id, association,
2543
+ store = get(this, 'store');
2544
+
2545
+ if (typeof type === 'string') {
2546
+ type = getPath(this, type, false) || getPath(window, type);
2547
+ }
2548
+
2549
+ if (arguments.length === 2) {
2550
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
2551
+ this.send('setAssociation', { key: key, value: value === null ? null : get(value, 'clientId') });
2552
+ //data.setAssociation(key, get(value, 'clientId'));
2553
+ // put the client id in `key` in the data hash
2554
+ return value;
2555
+ } else {
2556
+ // Embedded belongsTo associations should not look for
2557
+ // a foreign key.
2558
+ if (embedded) {
2559
+ key = options.key || key;
2560
+
2561
+ // Non-embedded associations should look for a foreign key.
2562
+ // For example, instead of person, we might look for person_id
2563
+ } else {
2564
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
2565
+ }
2566
+ id = findRecord(store, type, data, key, true);
2567
+ association = id ? store.find(type, id) : null;
2568
+ }
2569
+
2570
+ return association;
2571
+ }).property('data').cacheable().meta(meta);
2572
+ };
2573
+
2574
+ DS.belongsTo = function(type, options) {
2575
+
2576
+ return hasAssociation(type, options);
2577
+ };
2578
+
2579
+ })();
2580
+
2581
+
2582
+
2583
+ (function() {
2584
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
2585
+ var embeddedFindRecord = function(store, type, data, key) {
2586
+ var association = get(data, key);
2587
+ return association ? store.loadMany(type, association).ids : [];
2588
+ };
2589
+
2590
+ var referencedFindRecord = function(store, type, data, key, one) {
2591
+ return get(data, key);
2592
+ };
2593
+
2594
+ var hasAssociation = function(type, options) {
2595
+ options = options || {};
2596
+
2597
+ var embedded = options.embedded,
2598
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2599
+
2600
+ var meta = { type: type, isAssociation: true, options: options, kind: 'hasMany' };
2601
+
2602
+ return Ember.computed(function(key, value) {
2603
+ var data = get(this, 'data'),
2604
+ store = get(this, 'store'),
2605
+ ids, id, association;
2606
+
2607
+ if (typeof type === 'string') {
2608
+ type = getPath(this, type, false) || getPath(window, type);
2609
+ }
2610
+
2611
+ key = options.key || key;
2612
+ ids = findRecord(store, type, data, key);
2613
+ association = store.findMany(type, ids);
2614
+ set(association, 'parentRecord', this);
2615
+
2616
+ return association;
2617
+ }).property().cacheable().meta(meta);
2618
+ };
2619
+
2620
+ DS.hasMany = function(type, options) {
2621
+
2622
+ return hasAssociation(type, options);
2623
+ };
2624
+
2625
+ })();
2626
+
2627
+
2628
+
2629
+ (function() {
2630
+ var get = Ember.get, getPath = Ember.getPath;
2631
+
2632
+ DS.Model.reopenClass({
2633
+ typeForAssociation: function(name) {
2634
+ var association = get(this, 'associationsByName').get(name);
2635
+ return association && association.type;
2636
+ },
2637
+
2638
+ associations: Ember.computed(function() {
2639
+ var map = Ember.Map.create();
2640
+
2641
+ this.eachComputedProperty(function(name, meta) {
2642
+ if (meta.isAssociation) {
2643
+ var type = meta.type,
2644
+ typeList = map.get(type);
2645
+
2646
+ if (typeof type === 'string') {
2647
+ type = getPath(this, type, false) || getPath(window, type);
2648
+ meta.type = type;
2649
+ }
2650
+
2651
+ if (!typeList) {
2652
+ typeList = [];
2653
+ map.set(type, typeList);
2654
+ }
2655
+
2656
+ typeList.push({ name: name, kind: meta.kind });
2657
+ }
2658
+ });
2659
+
2660
+ return map;
2661
+ }).cacheable(),
2662
+
2663
+ associationsByName: Ember.computed(function() {
2664
+ var map = Ember.Map.create(), type;
2665
+
2666
+ this.eachComputedProperty(function(name, meta) {
2667
+ if (meta.isAssociation) {
2668
+ meta.key = name;
2669
+ type = meta.type;
2670
+
2671
+ if (typeof type === 'string') {
2672
+ type = getPath(this, type, false) || getPath(window, type);
2673
+ meta.type = type;
2674
+ }
2675
+
2676
+ map.set(name, meta);
2677
+ }
2678
+ });
2679
+
2680
+ return map;
2681
+ }).cacheable()
2682
+ });
2683
+
2684
+ })();
2685
+
2686
+
2687
+
2688
+ (function() {
2689
+
2690
+ })();
2691
+
2692
+
2693
+
2694
+ (function() {
2695
+ DS.Adapter = Ember.Object.extend({
2696
+ commit: function(store, commitDetails) {
2697
+ commitDetails.updated.eachType(function(type, array) {
2698
+ this.updateRecords(store, type, array.slice());
2699
+ }, this);
2700
+
2701
+ commitDetails.created.eachType(function(type, array) {
2702
+ this.createRecords(store, type, array.slice());
2703
+ }, this);
2704
+
2705
+ commitDetails.deleted.eachType(function(type, array) {
2706
+ this.deleteRecords(store, type, array.slice());
2707
+ }, this);
2708
+ },
2709
+
2710
+ createRecords: function(store, type, models) {
2711
+ models.forEach(function(model) {
2712
+ this.createRecord(store, type, model);
2713
+ }, this);
2714
+ },
2715
+
2716
+ updateRecords: function(store, type, models) {
2717
+ models.forEach(function(model) {
2718
+ this.updateRecord(store, type, model);
2719
+ }, this);
2720
+ },
2721
+
2722
+ deleteRecords: function(store, type, models) {
2723
+ models.forEach(function(model) {
2724
+ this.deleteRecord(store, type, model);
2725
+ }, this);
2726
+ },
2727
+
2728
+ findMany: function(store, type, ids) {
2729
+ ids.forEach(function(id) {
2730
+ this.find(store, type, id);
2731
+ }, this);
2732
+ }
2733
+ });
2734
+
2735
+ })();
2736
+
2737
+
2738
+
2739
+ (function() {
2740
+ DS.fixtureAdapter = DS.Adapter.create({
2741
+ find: function(store, type, id) {
2742
+ var fixtures = type.FIXTURES;
2743
+
2744
+ if (fixtures.hasLoaded) { return; }
2745
+
2746
+ setTimeout(function() {
2747
+ store.loadMany(type, fixtures);
2748
+ fixtures.hasLoaded = true;
2749
+ }, 300);
2750
+ },
2751
+
2752
+ findMany: function() {
2753
+ this.find.apply(this, arguments);
2754
+ },
2755
+
2756
+ findAll: function(store, type) {
2757
+ var fixtures = type.FIXTURES;
2758
+
2759
+
2760
+ var ids = fixtures.map(function(item, index, self){ return item.id; });
2761
+ store.loadMany(type, ids, fixtures);
2762
+ }
2763
+
2764
+ });
2765
+
2766
+ })();
2767
+
2768
+
2769
+
2770
+ (function() {
2771
+ /*global jQuery*/
2772
+
2773
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
2774
+
2775
+ DS.RESTAdapter = DS.Adapter.extend({
2776
+ createRecord: function(store, type, model) {
2777
+ var root = this.rootForType(type);
2778
+
2779
+ var data = {};
2780
+ data[root] = model.toJSON();
2781
+
2782
+ this.ajax(this.buildURL(root), "POST", {
2783
+ data: data,
2784
+ success: function(json) {
2785
+ this.sideload(store, type, json, root);
2786
+ store.didCreateRecord(model, json[root]);
2787
+ }
2788
+ });
2789
+ },
2790
+
2791
+ createRecords: function(store, type, models) {
2792
+ if (get(this, 'bulkCommit') === false) {
2793
+ return this._super(store, type, models);
2794
+ }
2795
+
2796
+ var root = this.rootForType(type),
2797
+ plural = this.pluralize(root);
2798
+
2799
+ var data = {};
2800
+ data[plural] = models.map(function(model) {
2801
+ return model.toJSON();
2802
+ });
2803
+
2804
+ this.ajax(this.buildURL(root), "POST", {
2805
+ data: data,
2806
+
2807
+ success: function(json) {
2808
+ this.sideload(store, type, json, plural);
2809
+ store.didCreateRecords(type, models, json[plural]);
2810
+ }
2811
+ });
2812
+ },
2813
+
2814
+ updateRecord: function(store, type, model) {
2815
+ var id = get(model, 'id');
2816
+ var root = this.rootForType(type);
2817
+
2818
+ var data = {};
2819
+ data[root] = model.toJSON();
2820
+
2821
+ this.ajax(this.buildURL(root, id), "PUT", {
2822
+ data: data,
2823
+ success: function(json) {
2824
+ this.sideload(store, type, json, root);
2825
+ store.didUpdateRecord(model, json && json[root]);
2826
+ }
2827
+ });
2828
+ },
2829
+
2830
+ updateRecords: function(store, type, models) {
2831
+ if (get(this, 'bulkCommit') === false) {
2832
+ return this._super(store, type, models);
2833
+ }
2834
+
2835
+ var root = this.rootForType(type),
2836
+ plural = this.pluralize(root);
2837
+
2838
+ var data = {};
2839
+ data[plural] = models.map(function(model) {
2840
+ return model.toJSON();
2841
+ });
2842
+
2843
+ this.ajax(this.buildURL(root, "bulk"), "PUT", {
2844
+ data: data,
2845
+ success: function(json) {
2846
+ this.sideload(store, type, json, plural);
2847
+ store.didUpdateRecords(models, json[plural]);
2848
+ }
2849
+ });
2850
+ },
2851
+
2852
+ deleteRecord: function(store, type, model) {
2853
+ var id = get(model, 'id');
2854
+ var root = this.rootForType(type);
2855
+
2856
+ this.ajax(this.buildURL(root, id), "DELETE", {
2857
+ success: function(json) {
2858
+ if (json) { this.sideload(store, type, json); }
2859
+ store.didDeleteRecord(model);
2860
+ }
2861
+ });
2862
+ },
2863
+
2864
+ deleteRecords: function(store, type, models) {
2865
+ if (get(this, 'bulkCommit') === false) {
2866
+ return this._super(store, type, models);
2867
+ }
2868
+
2869
+ var root = this.rootForType(type),
2870
+ plural = this.pluralize(root);
2871
+
2872
+ var data = {};
2873
+ data[plural] = models.map(function(model) {
2874
+ return get(model, 'id');
2875
+ });
2876
+
2877
+ this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
2878
+ data: data,
2879
+ success: function(json) {
2880
+ if (json) { this.sideload(store, type, json); }
2881
+ store.didDeleteRecords(models);
2882
+ }
2883
+ });
2884
+ },
2885
+
2886
+ find: function(store, type, id) {
2887
+ var root = this.rootForType(type);
2888
+
2889
+ this.ajax(this.buildURL(root, id), "GET", {
2890
+ success: function(json) {
2891
+ store.load(type, json[root]);
2892
+ this.sideload(store, type, json, root);
2893
+ }
2894
+ });
2895
+ },
2896
+
2897
+ findMany: function(store, type, ids) {
2898
+ var root = this.rootForType(type), plural = this.pluralize(root);
2899
+
2900
+ this.ajax(this.buildURL(root), "GET", {
2901
+ data: { ids: ids },
2902
+ success: function(json) {
2903
+ store.loadMany(type, ids, json[plural]);
2904
+ this.sideload(store, type, json, plural);
2905
+ }
2906
+ });
2907
+ },
2908
+
2909
+ findAll: function(store, type) {
2910
+ var root = this.rootForType(type), plural = this.pluralize(root);
2911
+
2912
+ this.ajax(this.buildURL(root), "GET", {
2913
+ success: function(json) {
2914
+ store.loadMany(type, json[plural]);
2915
+ this.sideload(store, type, json, plural);
2916
+ }
2917
+ });
2918
+ },
2919
+
2920
+ findQuery: function(store, type, query, modelArray) {
2921
+ var root = this.rootForType(type), plural = this.pluralize(root);
2922
+
2923
+ this.ajax(this.buildURL(root), "GET", {
2924
+ data: query,
2925
+ success: function(json) {
2926
+ modelArray.load(json[plural]);
2927
+ this.sideload(store, type, json, plural);
2928
+ }
2929
+ });
2930
+ },
2931
+
2932
+ // HELPERS
2933
+
2934
+ plurals: {},
2935
+
2936
+ // define a plurals hash in your subclass to define
2937
+ // special-case pluralization
2938
+ pluralize: function(name) {
2939
+ return this.plurals[name] || name + "s";
2940
+ },
2941
+
2942
+ rootForType: function(type) {
2943
+ if (type.url) { return type.url; }
2944
+
2945
+ // use the last part of the name as the URL
2946
+ var parts = type.toString().split(".");
2947
+ var name = parts[parts.length - 1];
2948
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
2949
+ },
2950
+
2951
+ ajax: function(url, type, hash) {
2952
+ hash.url = url;
2953
+ hash.type = type;
2954
+ hash.dataType = 'json';
2955
+ hash.contentType = 'application/json';
2956
+ hash.context = this;
2957
+
2958
+ if (hash.data && type !== 'GET') {
2959
+ hash.data = JSON.stringify(hash.data);
2960
+ }
2961
+
2962
+ jQuery.ajax(hash);
2963
+ },
2964
+
2965
+ sideload: function(store, type, json, root) {
2966
+ var sideloadedType, mappings;
2967
+
2968
+ for (var prop in json) {
2969
+ if (!json.hasOwnProperty(prop)) { continue; }
2970
+ if (prop === root) { continue; }
2971
+
2972
+ sideloadedType = type.typeForAssociation(prop);
2973
+
2974
+ if (!sideloadedType) {
2975
+ mappings = get(this, 'mappings');
2976
+
2977
+
2978
+ sideloadedType = get(get(this, 'mappings'), prop);
2979
+
2980
+ }
2981
+
2982
+ this.loadValue(store, sideloadedType, json[prop]);
2983
+ }
2984
+ },
2985
+
2986
+ loadValue: function(store, type, value) {
2987
+ if (value instanceof Array) {
2988
+ store.loadMany(type, value);
2989
+ } else {
2990
+ store.load(type, value);
2991
+ }
2992
+ },
2993
+
2994
+ buildURL: function(model, suffix) {
2995
+ var url = [""];
2996
+
2997
+ if (this.namespace !== undefined) {
2998
+ url.push(this.namespace);
2999
+ }
3000
+
3001
+ url.push(this.pluralize(model));
3002
+ if (suffix !== undefined) {
3003
+ url.push(suffix);
3004
+ }
3005
+
3006
+ return url.join("/");
3007
+ }
3008
+ });
3009
+
3010
+
3011
+ })();
3012
+
3013
+
3014
+
3015
+ (function() {
3016
+ //Copyright (C) 2011 by Living Social, Inc.
3017
+
3018
+ //Permission is hereby granted, free of charge, to any person obtaining a copy of
3019
+ //this software and associated documentation files (the "Software"), to deal in
3020
+ //the Software without restriction, including without limitation the rights to
3021
+ //use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
3022
+ //of the Software, and to permit persons to whom the Software is furnished to do
3023
+ //so, subject to the following conditions:
3024
+
3025
+ //The above copyright notice and this permission notice shall be included in all
3026
+ //copies or substantial portions of the Software.
3027
+
3028
+ //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3029
+ //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3030
+ //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3031
+ //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3032
+ //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3033
+ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3034
+ //SOFTWARE.
3035
+
3036
+ })();
1740
3037
 
1741
- //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1742
- //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1743
- //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1744
- //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1745
- //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1746
- //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1747
- //SOFTWARE.
1748
- })({});