rasputin 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- 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
- })({});