rightnow_oms 0.1.4 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/CHANGELOG +12 -0
  2. data/README.md +145 -66
  3. data/app/assets/javascripts/rightnow_oms/app/app.js.coffee +12 -4
  4. data/app/assets/javascripts/rightnow_oms/app/controllers/cart.js.coffee +17 -32
  5. data/app/assets/javascripts/rightnow_oms/app/models/cart.js.coffee +50 -28
  6. data/app/assets/javascripts/rightnow_oms/app/models/cart_item.js.coffee +28 -49
  7. data/app/assets/javascripts/rightnow_oms/app/templates/cart_items/show.handlebars +30 -0
  8. data/app/assets/javascripts/rightnow_oms/app/templates/cart_items/{show_in_detail.hjs → show_in_detail.handlebars} +3 -3
  9. data/app/assets/javascripts/rightnow_oms/app/templates/carts/{show.hjs → show.handlebars} +4 -3
  10. data/app/assets/javascripts/rightnow_oms/app/templates/carts/show_cartable_count.handlebars +2 -0
  11. data/app/assets/javascripts/rightnow_oms/app/templates/carts/show_in_detail.handlebars +30 -0
  12. data/app/assets/javascripts/rightnow_oms/app/views/cart_items/show_in_detail.js.coffee +4 -1
  13. data/app/assets/javascripts/rightnow_oms/application.js.coffee +10 -0
  14. data/app/assets/javascripts/rightnow_oms/cart_items.js +10 -0
  15. data/app/assets/javascripts/rightnow_oms/config/app.js.coffee +5 -0
  16. data/app/assets/javascripts/rightnow_oms/config/locales/en.js.coffee +24 -0
  17. data/app/assets/javascripts/rightnow_oms/config/locales/zh_CN.js.coffee +24 -0
  18. data/app/assets/javascripts/rightnow_oms/lib/ember/data/my_rest_adapter.js.coffee +2 -167
  19. data/app/assets/javascripts/rightnow_oms/vendor/cldr.js +240 -0
  20. data/app/assets/javascripts/rightnow_oms/vendor/ember-data.js +1530 -587
  21. data/app/assets/javascripts/rightnow_oms/vendor/ember-data.min.js +1 -1
  22. data/app/assets/javascripts/rightnow_oms/vendor/ember-i18n.js +123 -0
  23. data/app/assets/javascripts/rightnow_oms/vendor/{ember-0.9.5.js → ember.js} +1584 -928
  24. data/app/assets/javascripts/rightnow_oms/vendor/ember.min.js +5 -5
  25. data/app/assets/stylesheets/rightnow_oms/carts.css.scss +93 -85
  26. data/app/controllers/rightnow_oms/cart_items_controller.rb +2 -2
  27. data/app/controllers/rightnow_oms/orders_controller.rb +27 -5
  28. data/app/models/rightnow_oms/cart.rb +1 -0
  29. data/app/models/rightnow_oms/cart_item.rb +1 -0
  30. data/app/models/rightnow_oms/order.rb +26 -2
  31. data/app/views/rightnow_oms/cart_items/_list.html.haml +24 -0
  32. data/app/views/rightnow_oms/orders/show.html.haml +2 -0
  33. data/config/initializers/rightnow_oms.rb +4 -0
  34. data/config/initializers/validates_timeliness.rb +40 -0
  35. data/config/locales/validates_timeliness.en.yml +16 -0
  36. data/config/locales/validates_timeliness.zh-CN.yml +16 -0
  37. data/db/migrate/20120224051751_add_required_arrival_time_to_rightnow_oms_orders.rb +6 -0
  38. data/db/migrate/20120302085200_add_tastes_to_rightnow_oms_order_items.rb +6 -0
  39. data/lib/rightnow_oms.rb +5 -1
  40. data/lib/rightnow_oms/controller_extension.rb +33 -0
  41. data/lib/rightnow_oms/controller_helpers.rb +4 -21
  42. data/lib/rightnow_oms/version.rb +1 -1
  43. metadata +71 -35
  44. data/app/assets/javascripts/rightnow_oms/app/templates/cart_items/show.hjs +0 -23
  45. data/app/assets/javascripts/rightnow_oms/app/templates/carts/show_cartable_count.hjs +0 -1
  46. data/app/assets/javascripts/rightnow_oms/app/templates/carts/show_in_detail.hjs +0 -27
  47. data/app/assets/javascripts/rightnow_oms/application.js +0 -8
@@ -0,0 +1,240 @@
1
+ (function(globals) {
2
+
3
+ // CLDR Pluralization Data
4
+ // see http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
5
+
6
+ // The six plural forms. Not all languages use all six forms.
7
+ var Zero = 'zero',
8
+ One = 'one',
9
+ Two = 'two',
10
+ Few = 'few',
11
+ Many = 'many',
12
+ Other = 'other',
13
+ Data = {};
14
+
15
+ function isInt(value) {
16
+ return value << 0 === value;
17
+ }
18
+
19
+ function isAmong(value, array) {
20
+ for ( var i = 0; i < array.length; ++i ) {
21
+ if (array[i] === value) { return true; }
22
+ }
23
+ return false;
24
+ }
25
+
26
+ function define(languages, rule) {
27
+ for ( var i = 0; i < languages.length; ++i ) {
28
+ Data[ languages[i] ] = rule;
29
+ }
30
+ }
31
+
32
+ define([
33
+ 'az', 'bm', 'my', 'zh', 'dz', 'ka', 'hu', 'ig', 'id', 'ja', 'jv', 'kea',
34
+ 'kn', 'km', 'ko', 'ses', 'lo', 'kde', 'ms', 'fa', 'root', 'sah', 'sg',
35
+ 'ii', 'th', 'bo', 'to', 'tr', 'vi', 'wo', 'yo'
36
+ ], function(n) {
37
+ return Other;
38
+ });
39
+
40
+ define([ 'gv' ], function(n) {
41
+ if ( isAmong(n % 10, [ 1, 2 ]) || n % 20 === 0 ) { return One; }
42
+ return Other;
43
+ });
44
+
45
+ define([ 'tzm' ], function(n) {
46
+ if ( n === 0 || n === 1 ) { return One; }
47
+ if ( isInt(n) && n >= 11 && n <= 99 ) { return One; }
48
+ return Other;
49
+ });
50
+
51
+ define([ 'mk' ], function(n) {
52
+ return n % 10 === 1 && n !== 11 ? One : Other;
53
+ });
54
+
55
+ define([ 'fr', 'ff', 'kab' ], function(n) {
56
+ return n >= 0 && n < 2 ? One : Other;
57
+ });
58
+
59
+ define([
60
+ 'ak', 'am', 'bh', 'fil', 'guw', 'hi', 'ln', 'mg', 'nso', 'tl', 'ti', 'wa'
61
+ ], function(n) {
62
+ return n === 0 || n === 1 ? One : Other;
63
+ });
64
+
65
+ define([
66
+ 'af', 'sq', 'eu', 'bem', 'bn', 'brx', 'bg', 'ca', 'chr', 'cgg', 'da', 'dv',
67
+ 'nl', 'en', 'eo', 'et', 'ee', 'fo', 'fi', 'fur', 'gl', 'lg', 'de', 'el',
68
+ 'gu', 'ha', 'haw', 'he', 'is', 'it', 'kl', 'kk', 'ku', 'lb', 'ml', 'mr',
69
+ 'mas', 'mn', 'nah', 'ne', 'no', 'nb', 'nn', 'nyn', 'or', 'om', 'pap', 'ps',
70
+ 'pt', 'pa', 'rm', 'ssy', 'saq', 'xog', 'so', 'es', 'sw', 'sv', 'gsw',
71
+ 'syr', 'ta', 'te', 'tk', 'ur', 'wae', 'fy', 'zu'
72
+ ], function(n) {
73
+ return n === 1 ? One : Other;
74
+ });
75
+
76
+ define([ 'lv' ], function(n) {
77
+ if (n === 0) { return Zero; }
78
+ if (n % 10 === 1 && n % 100 !== 11) { return One; }
79
+ return Other;
80
+ });
81
+
82
+ define([ 'ksh' ], function(n) {
83
+ if (n === 0) { return Zero; }
84
+ if (n === 1) { return One; }
85
+ return Other;
86
+ });
87
+
88
+ define([ 'lag' ], function(n) {
89
+ if (n === 0) { return Zero; }
90
+ if (n > 0 && n < 2) { return One; }
91
+ return Other;
92
+ });
93
+
94
+ define([
95
+ 'kw', 'smn', 'iu', 'ga', 'smj', 'se', 'smi', 'sms', 'sma'
96
+ ], function(n) {
97
+ if (n === 1) { return One; }
98
+ if (n === 2) { return Two; }
99
+ return Other;
100
+ });
101
+
102
+ define([
103
+ 'be', 'bs', 'hr', 'ru', 'sr', 'sh', 'uk'
104
+ ], function(n) {
105
+ var mod10 = n % 10,
106
+ mod100 = n % 100;
107
+
108
+ if ( mod10 === 1 && n % 100 !== 11 ) { return One; }
109
+
110
+ if ( isAmong(mod10, [ 2, 3, 4 ]) &&
111
+ !isAmong(mod100, [ 12, 13, 14 ]) ) { return Few; }
112
+
113
+ if ( isAmong(mod10, [ 0, 5, 6, 7, 8, 9 ]) ||
114
+ isAmong(mod100, [ 11, 12, 13, 14 ]) ) { return Many; }
115
+
116
+ return Other;
117
+ });
118
+
119
+ define([ 'pl' ], function(n) {
120
+ var mod10 = n % 10,
121
+ mod100 = n % 100;
122
+
123
+ if ( n === 1 ) { return One; }
124
+
125
+ if ( isAmong(mod10, [ 2, 3, 4 ]) &&
126
+ !isAmong(mod100, [ 12, 13, 14 ]) ) { return Few; }
127
+
128
+ if ( isAmong(mod10, [ 0, 1, 5, 6, 7, 8, 9 ]) ||
129
+ isAmong(mod100, [ 12, 13, 14 ]) ) { return Many; }
130
+
131
+ return Other;
132
+ });
133
+
134
+ define([ 'lt' ], function(n) {
135
+ var mod10 = n % 10,
136
+ mod100 = n % 100;
137
+
138
+ if ( mod10 === 1 && mod100 !== 11 ) { return One; }
139
+
140
+ if ( isInt(n) &&
141
+ mod10 >= 2 && mod10 <= 9 &&
142
+ mod100 >= 12 && mod100 <= 19 ) { return Few; }
143
+
144
+ return Other;
145
+ });
146
+
147
+ define([ 'shi' ], function(n) {
148
+ if ( n >= 0 && n <= 1 ) { return One; }
149
+ if ( isInt(n) && n >= 2 && n <= 9 ) { return Few; }
150
+ return Other;
151
+ });
152
+
153
+ define([ 'mo', 'ro' ], function(n) {
154
+ var mod100 = n % 100;
155
+
156
+ if ( n === 1 ) { return One; }
157
+
158
+ if ( n === 0 ||
159
+ (isInt(n) && mod100 >= 1 && mod100 <= 19) ) { return Few; }
160
+
161
+ return Other;
162
+ });
163
+
164
+ define([ 'cs', 'sk' ], function(n) {
165
+ if ( n === 1 ) { return One; }
166
+ if ( isAmong(n, [ 2, 3, 4 ]) ) { return Few; }
167
+ return Other;
168
+ });
169
+
170
+ define([ 'sl' ], function(n) {
171
+ var mod100 = n % 100;
172
+ if ( mod100 === 1 ) { return One; }
173
+ if ( mod100 === 2 ) { return Two; }
174
+ if ( mod100 === 3 || mod100 === 4 ) { return Few; }
175
+ return Other;
176
+ });
177
+
178
+ define([ 'mt' ], function(n) {
179
+ if ( n === 1 ) { return One; }
180
+ var mod100 = n % 100;
181
+ if ( isInt(mod100) && mod100 >= 2 && mod100 <= 10 ) { return Few; }
182
+ if ( isInt(mod100) && mod100 >= 11 && mod100 <= 19 ) { return Many; }
183
+ return Other;
184
+ });
185
+
186
+ define([ 'ar' ], function(n) {
187
+ if ( n === 0 ) { return Zero; }
188
+ if ( n === 1 ) { return One; }
189
+ if ( n === 2 ) { return Two; }
190
+ var mod100 = n % 100;
191
+ if ( isInt(mod100) && mod100 >= 3 && mod100 <= 10 ) { return Few; }
192
+ if ( isInt(mod100) && mod100 >= 11 && mod100 <= 99 ) { return Many; }
193
+ return Other;
194
+ });
195
+
196
+ define([ 'br', 'cy' ], function(n) {
197
+ switch ( n ) {
198
+ case 0: return Zero;
199
+ case 1: return One;
200
+ case 2: return Two;
201
+ case 3: return Few;
202
+ case 6: return Many;
203
+ default: return Other;
204
+ }
205
+ });
206
+
207
+ if ( globals.CLDR == null ) { globals.CLDR = {}; }
208
+
209
+ var CLDR = globals.CLDR;
210
+
211
+ // Look up the proper plural key for a count and language.
212
+ // If CLDR.defaultLanguage is set, language is optional.
213
+ //
214
+ // For example:
215
+ //
216
+ // CLDR.pluralForm(0, 'en'); // => 'other'
217
+ // CLDR.pluralForm(1, 'en-US'); // => 'one'
218
+ // CLDR.pluralForm(2.383, 'fr'); // => 'other'
219
+ // CLDR.pluralForm(1, 'zh'); // => 'other'
220
+ // CLDR.pluralForm(26, 'uk'); // => 'many'
221
+ //
222
+ // @return [String] the proper key (one of `CLDR.pluralForm.Zero`,
223
+ // `.One`, `.Two`, `.Few`, `.Many`, or `.Other`).
224
+ CLDR.pluralForm = function(count, language) {
225
+ if (count == null) { throw new Error("CLDR.pluralForm requires a count"); }
226
+ language = language || CLDR.defaultLanguage;
227
+ if (language == null) { throw new Error("CLDR.pluralForm requires a language"); }
228
+ language = language.replace(/^(\w\w\w?)-?.*/, "$1");
229
+ if (Data[language] == null) { throw new Error("No CLDR pluralization information for " + language); }
230
+ return Data[language].call(CLDR, +count);
231
+ };
232
+
233
+ CLDR.pluralForm.Zero = Zero;
234
+ CLDR.pluralForm.One = One;
235
+ CLDR.pluralForm.Two = Two;
236
+ CLDR.pluralForm.Few = Few;
237
+ CLDR.pluralForm.Many = Many;
238
+ CLDR.pluralForm.Other = Other;
239
+
240
+ }(this));
@@ -1,6 +1,8 @@
1
1
 
2
2
  (function(exports) {
3
- window.DS = Ember.Namespace.create();
3
+ window.DS = Ember.Namespace.create({
4
+ CURRENT_API_REVISION: 2
5
+ });
4
6
 
5
7
  })({});
6
8
 
@@ -71,7 +73,7 @@ DS.fixtureAdapter = DS.Adapter.create({
71
73
 
72
74
  ember_assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
73
75
 
74
- var ids = fixtures.map(function(item, index, self){ return item.id });
76
+ var ids = fixtures.map(function(item, index, self){ return item.id; });
75
77
  store.loadMany(type, ids, fixtures);
76
78
  }
77
79
 
@@ -81,6 +83,7 @@ DS.fixtureAdapter = DS.Adapter.create({
81
83
 
82
84
 
83
85
  (function(exports) {
86
+ /*global jQuery*/
84
87
  var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
85
88
 
86
89
  DS.RESTAdapter = DS.Adapter.extend({
@@ -88,11 +91,12 @@ DS.RESTAdapter = DS.Adapter.extend({
88
91
  var root = this.rootForType(type);
89
92
 
90
93
  var data = {};
91
- data[root] = get(model, 'data');
94
+ data[root] = model.toJSON();
92
95
 
93
- this.ajax("/" + this.pluralize(root), "POST", {
96
+ this.ajax(this.buildURL(root), "POST", {
94
97
  data: data,
95
98
  success: function(json) {
99
+ this.sideload(store, type, json, root);
96
100
  store.didCreateRecord(model, json[root]);
97
101
  }
98
102
  });
@@ -108,31 +112,31 @@ DS.RESTAdapter = DS.Adapter.extend({
108
112
 
109
113
  var data = {};
110
114
  data[plural] = models.map(function(model) {
111
- return get(model, 'data');
115
+ return model.toJSON();
112
116
  });
113
117
 
114
- this.ajax("/" + this.pluralize(root), "POST", {
118
+ this.ajax(this.buildURL(root), "POST", {
115
119
  data: data,
120
+
116
121
  success: function(json) {
122
+ this.sideload(store, type, json, plural);
117
123
  store.didCreateRecords(type, models, json[plural]);
118
124
  }
119
125
  });
120
126
  },
121
127
 
122
128
  updateRecord: function(store, type, model) {
123
- var primaryKey = getPath(type, 'proto.primaryKey'),
124
- id = get(model, primaryKey);
129
+ var id = get(model, 'id');
125
130
  var root = this.rootForType(type);
126
131
 
127
132
  var data = {};
128
- data[root] = get(model, 'data');
129
-
130
- var url = ["", this.pluralize(root), id].join("/");
133
+ data[root] = model.toJSON();
131
134
 
132
- this.ajax(url, "PUT", {
135
+ this.ajax(this.buildURL(root, id), "PUT", {
133
136
  data: data,
134
137
  success: function(json) {
135
- store.didUpdateRecord(model, json[root]);
138
+ this.sideload(store, type, json, root);
139
+ store.didUpdateRecord(model, json && json[root]);
136
140
  }
137
141
  });
138
142
  },
@@ -147,26 +151,25 @@ DS.RESTAdapter = DS.Adapter.extend({
147
151
 
148
152
  var data = {};
149
153
  data[plural] = models.map(function(model) {
150
- return get(model, 'data');
154
+ return model.toJSON();
151
155
  });
152
156
 
153
- this.ajax("/" + this.pluralize(root), "POST", {
157
+ this.ajax(this.buildURL(root, "bulk"), "PUT", {
154
158
  data: data,
155
159
  success: function(json) {
160
+ this.sideload(store, type, json, plural);
156
161
  store.didUpdateRecords(models, json[plural]);
157
162
  }
158
163
  });
159
164
  },
160
165
 
161
166
  deleteRecord: function(store, type, model) {
162
- var primaryKey = getPath(type, 'proto.primaryKey'),
163
- id = get(model, primaryKey);
167
+ var id = get(model, 'id');
164
168
  var root = this.rootForType(type);
165
169
 
166
- var url = ["", this.pluralize(root), id].join("/");
167
-
168
- this.ajax(url, "DELETE", {
170
+ this.ajax(this.buildURL(root, id), "DELETE", {
169
171
  success: function(json) {
172
+ if (json) { this.sideload(store, type, json); }
170
173
  store.didDeleteRecord(model);
171
174
  }
172
175
  });
@@ -178,17 +181,17 @@ DS.RESTAdapter = DS.Adapter.extend({
178
181
  }
179
182
 
180
183
  var root = this.rootForType(type),
181
- plural = this.pluralize(root),
182
- primaryKey = getPath(type, 'proto.primaryKey');
184
+ plural = this.pluralize(root);
183
185
 
184
186
  var data = {};
185
187
  data[plural] = models.map(function(model) {
186
- return get(model, primaryKey);
188
+ return get(model, 'id');
187
189
  });
188
190
 
189
- this.ajax("/" + this.pluralize(root) + "/delete", "POST", {
191
+ this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
190
192
  data: data,
191
193
  success: function(json) {
194
+ if (json) { this.sideload(store, type, json); }
192
195
  store.didDeleteRecords(models);
193
196
  }
194
197
  });
@@ -197,11 +200,10 @@ DS.RESTAdapter = DS.Adapter.extend({
197
200
  find: function(store, type, id) {
198
201
  var root = this.rootForType(type);
199
202
 
200
- var url = ["", this.pluralize(root), id].join("/");
201
-
202
- this.ajax(url, "GET", {
203
+ this.ajax(this.buildURL(root, id), "GET", {
203
204
  success: function(json) {
204
205
  store.load(type, json[root]);
206
+ this.sideload(store, type, json, root);
205
207
  }
206
208
  });
207
209
  },
@@ -209,21 +211,22 @@ DS.RESTAdapter = DS.Adapter.extend({
209
211
  findMany: function(store, type, ids) {
210
212
  var root = this.rootForType(type), plural = this.pluralize(root);
211
213
 
212
- this.ajax("/" + plural, "GET", {
214
+ this.ajax(this.buildURL(root), "GET", {
213
215
  data: { ids: ids },
214
216
  success: function(json) {
215
217
  store.loadMany(type, ids, json[plural]);
218
+ this.sideload(store, type, json, plural);
216
219
  }
217
220
  });
218
- var url = "/" + plural;
219
221
  },
220
222
 
221
223
  findAll: function(store, type) {
222
224
  var root = this.rootForType(type), plural = this.pluralize(root);
223
225
 
224
- this.ajax("/" + plural, "GET", {
226
+ this.ajax(this.buildURL(root), "GET", {
225
227
  success: function(json) {
226
228
  store.loadMany(type, json[plural]);
229
+ this.sideload(store, type, json, plural);
227
230
  }
228
231
  });
229
232
  },
@@ -231,10 +234,11 @@ DS.RESTAdapter = DS.Adapter.extend({
231
234
  findQuery: function(store, type, query, modelArray) {
232
235
  var root = this.rootForType(type), plural = this.pluralize(root);
233
236
 
234
- this.ajax("/" + plural, "GET", {
237
+ this.ajax(this.buildURL(root), "GET", {
235
238
  data: query,
236
239
  success: function(json) {
237
240
  modelArray.load(json[plural]);
241
+ this.sideload(store, type, json, plural);
238
242
  }
239
243
  });
240
244
  },
@@ -261,9 +265,61 @@ DS.RESTAdapter = DS.Adapter.extend({
261
265
  ajax: function(url, type, hash) {
262
266
  hash.url = url;
263
267
  hash.type = type;
264
- hash.dataType = "json";
268
+ hash.dataType = 'json';
269
+ hash.contentType = 'application/json';
270
+ hash.context = this;
271
+
272
+ if (hash.data && type !== 'GET') {
273
+ hash.data = JSON.stringify(hash.data);
274
+ }
265
275
 
266
276
  jQuery.ajax(hash);
277
+ },
278
+
279
+ sideload: function(store, type, json, root) {
280
+ var sideloadedType, mappings;
281
+
282
+ for (var prop in json) {
283
+ if (!json.hasOwnProperty(prop)) { continue; }
284
+ if (prop === root) { continue; }
285
+
286
+ sideloadedType = type.typeForAssociation(prop);
287
+
288
+ if (!sideloadedType) {
289
+ mappings = get(this, 'mappings');
290
+
291
+ ember_assert("Your server returned a hash with the key " + prop + " but you have no mappings", !!mappings);
292
+
293
+ sideloadedType = get(get(this, 'mappings'), prop);
294
+
295
+ ember_assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType);
296
+ }
297
+
298
+ this.loadValue(store, sideloadedType, json[prop]);
299
+ }
300
+ },
301
+
302
+ loadValue: function(store, type, value) {
303
+ if (value instanceof Array) {
304
+ store.loadMany(type, value);
305
+ } else {
306
+ store.load(type, value);
307
+ }
308
+ },
309
+
310
+ buildURL: function(model, suffix) {
311
+ var url = [""];
312
+
313
+ if (this.namespace !== undefined) {
314
+ url.push(this.namespace);
315
+ }
316
+
317
+ url.push(this.pluralize(model));
318
+ if (suffix !== undefined) {
319
+ url.push(suffix);
320
+ }
321
+
322
+ return url.join("/");
267
323
  }
268
324
  });
269
325
 
@@ -274,9 +330,30 @@ DS.RESTAdapter = DS.Adapter.extend({
274
330
  (function(exports) {
275
331
  var get = Ember.get, set = Ember.set;
276
332
 
333
+ /**
334
+ A model array is an array that contains records of a certain type. The model
335
+ array materializes records as needed when they are retrieved for the first
336
+ time. You should not create model arrays yourself. Instead, an instance of
337
+ DS.ModelArray or its subclasses will be returned by your application's store
338
+ in response to queries.
339
+ */
340
+
277
341
  DS.ModelArray = Ember.ArrayProxy.extend({
342
+
343
+ /**
344
+ The model type contained by this model array.
345
+
346
+ @type DS.Model
347
+ */
278
348
  type: null,
349
+
350
+ // The array of client ids backing the model array. When a
351
+ // record is requested from the model array, the record
352
+ // for the client id at the same index is materialized, if
353
+ // necessary, by the store.
279
354
  content: null,
355
+
356
+ // The store that created this model array.
280
357
  store: null,
281
358
 
282
359
  init: function() {
@@ -286,7 +363,7 @@ DS.ModelArray = Ember.ArrayProxy.extend({
286
363
 
287
364
  arrayDidChange: function(array, index, removed, added) {
288
365
  var modelCache = get(this, 'modelCache');
289
- modelCache.replace(index, 0, Array(added));
366
+ modelCache.replace(index, 0, new Array(added));
290
367
 
291
368
  this._super(array, index, removed, added);
292
369
  },
@@ -318,19 +395,41 @@ DS.ModelArray = Ember.ArrayProxy.extend({
318
395
  }
319
396
  });
320
397
 
398
+ })({});
399
+
400
+
401
+ (function(exports) {
402
+ var get = Ember.get;
403
+
321
404
  DS.FilteredModelArray = DS.ModelArray.extend({
322
405
  filterFunction: null,
323
406
 
407
+ replace: function() {
408
+ var type = get(this, 'type').toString();
409
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
410
+ },
411
+
324
412
  updateFilter: Ember.observer(function() {
325
413
  var store = get(this, 'store');
326
414
  store.updateModelArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
327
415
  }, 'filterFunction')
328
416
  });
329
417
 
418
+ })({});
419
+
420
+
421
+ (function(exports) {
422
+ var get = Ember.get, set = Ember.set;
423
+
330
424
  DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
331
425
  query: null,
332
426
  isLoaded: false,
333
427
 
428
+ replace: function() {
429
+ var type = get(this, 'type').toString();
430
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
431
+ },
432
+
334
433
  load: function(array) {
335
434
  var store = get(this, 'store'), type = get(this, 'type');
336
435
 
@@ -343,172 +442,181 @@ DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
343
442
  }
344
443
  });
345
444
 
445
+
346
446
  })({});
347
447
 
348
448
 
349
449
  (function(exports) {
350
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
450
+ var get = Ember.get, set = Ember.set;
351
451
 
352
- var OrderedSet = Ember.Object.extend({
353
- init: function() {
354
- this.clear();
355
- },
452
+ DS.ManyArray = DS.ModelArray.extend({
453
+ parentRecord: null,
356
454
 
357
- clear: function() {
358
- this.set('presenceSet', {});
359
- this.set('list', Ember.NativeArray.apply([]));
360
- },
455
+ // Overrides Ember.Array's replace method to implement
456
+ replace: function(index, removed, added) {
457
+ var parentRecord = get(this, 'parentRecord');
458
+ var pendingParent = parentRecord && !get(parentRecord, 'id');
361
459
 
362
- add: function(obj) {
363
- var guid = Ember.guidFor(obj),
364
- presenceSet = get(this, 'presenceSet'),
365
- list = get(this, 'list');
460
+ added = added.map(function(record) {
461
+ ember_assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === record.constructor));
366
462
 
367
- if (guid in presenceSet) { return; }
463
+ if (pendingParent) {
464
+ record.send('waitingOn', parentRecord);
465
+ }
368
466
 
369
- presenceSet[guid] = true;
370
- list.pushObject(obj);
371
- },
467
+ this.assignInverse(record, parentRecord);
372
468
 
373
- remove: function(obj) {
374
- var guid = Ember.guidFor(obj),
375
- presenceSet = get(this, 'presenceSet'),
376
- list = get(this, 'list');
469
+ return record.get('clientId');
470
+ }, this);
377
471
 
378
- delete presenceSet[guid];
379
- list.removeObject(obj);
472
+ this._super(index, removed, added);
380
473
  },
381
474
 
382
- isEmpty: function() {
383
- return getPath(this, 'list.length') === 0;
384
- },
475
+ assignInverse: function(record, parentRecord) {
476
+ var associationMap = get(record.constructor, 'associations'),
477
+ possibleAssociations = associationMap.get(record.constructor),
478
+ possible, actual;
385
479
 
386
- forEach: function(fn, self) {
387
- get(this, 'list').forEach(function(item) {
388
- fn.call(self, item);
389
- });
390
- },
480
+ if (!possibleAssociations) { return; }
481
+
482
+ for (var i = 0, l = possibleAssociations.length; i < l; i++) {
483
+ possible = possibleAssociations[i];
484
+
485
+ if (possible.kind === 'belongsTo') {
486
+ actual = possible;
487
+ break;
488
+ }
489
+ }
391
490
 
392
- toArray: function() {
393
- return get(this, 'list').slice();
491
+ if (actual) {
492
+ set(record, actual.name, parentRecord);
493
+ }
394
494
  }
395
495
  });
396
496
 
397
- /**
398
- A Hash stores values indexed by keys. Unlike JavaScript's
399
- default Objects, the keys of a Hash can be any JavaScript
400
- object.
497
+ })({});
498
+
401
499
 
402
- Internally, a Hash has two data structures:
500
+ (function(exports) {
501
+ })({});
403
502
 
404
- `keys`: an OrderedSet of all of the existing keys
405
- `values`: a JavaScript Object indexed by the
406
- Ember.guidFor(key)
407
503
 
408
- When a key/value pair is added for the first time, we
409
- add the key to the `keys` OrderedSet, and create or
410
- replace an entry in `values`. When an entry is deleted,
411
- we delete its entry in `keys` and `values`.
412
- */
504
+ (function(exports) {
505
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
413
506
 
414
- var Hash = Ember.Object.extend({
507
+ DS.Transaction = Ember.Object.extend({
415
508
  init: function() {
416
- set(this, 'keys', OrderedSet.create());
417
- set(this, 'values', {});
509
+ set(this, 'buckets', {
510
+ clean: Ember.Map.create(),
511
+ created: Ember.Map.create(),
512
+ updated: Ember.Map.create(),
513
+ deleted: Ember.Map.create()
514
+ });
418
515
  },
419
516
 
420
- add: function(key, value) {
421
- var keys = get(this, 'keys'), values = get(this, 'values');
422
- var guid = Ember.guidFor(key);
423
-
424
- keys.add(key);
425
- values[guid] = value;
517
+ createRecord: function(type, hash) {
518
+ var store = get(this, 'store');
426
519
 
427
- return value;
520
+ return store.createRecord(type, hash, this);
428
521
  },
429
522
 
430
- remove: function(key) {
431
- var keys = get(this, 'keys'), values = get(this, 'values');
432
- var guid = Ember.guidFor(key), value;
523
+ add: function(record) {
524
+ // we could probably make this work if someone has a valid use case. Do you?
525
+ ember_assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
433
526
 
434
- keys.remove(key);
527
+ var modelTransaction = get(record, 'transaction'),
528
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
435
529
 
436
- value = values[guid];
437
- delete values[guid];
530
+ ember_assert("Models cannot belong to more than one transaction at a time.", modelTransaction === defaultTransaction);
438
531
 
439
- return value;
532
+ this.adoptRecord(record);
440
533
  },
441
534
 
442
- fetch: function(key) {
443
- var values = get(this, 'values');
444
- var guid = Ember.guidFor(key);
535
+ remove: function(record) {
536
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
445
537
 
446
- return values[guid];
538
+ defaultTransaction.adoptRecord(record);
447
539
  },
448
540
 
449
- forEach: function(fn, binding) {
450
- var keys = get(this, 'keys'), values = get(this, 'values');
541
+ /**
542
+ @private
451
543
 
452
- keys.forEach(function(key) {
453
- var guid = Ember.guidFor(key);
454
- fn.call(binding, key, values[guid]);
455
- });
456
- }
457
- });
544
+ This method moves a record into a different transaction without the normal
545
+ checks that ensure that the user is not doing something weird, like moving
546
+ a dirty record into a new transaction.
458
547
 
459
- DS.Transaction = Ember.Object.extend({
460
- init: function() {
461
- set(this, 'dirty', {
462
- created: Hash.create(),
463
- updated: Hash.create(),
464
- deleted: Hash.create()
465
- });
466
- },
548
+ It is designed for internal use, such as when we are moving a clean record
549
+ into a new transaction when the transaction is committed.
467
550
 
468
- createRecord: function(type, hash) {
469
- var store = get(this, 'store');
551
+ This method must not be called unless the record is clean.
552
+ */
553
+ adoptRecord: function(record) {
554
+ var oldTransaction = get(record, 'transaction');
470
555
 
471
- return store.createRecord(type, hash, this);
472
- },
556
+ if (oldTransaction) {
557
+ oldTransaction.removeFromBucket('clean', record);
558
+ }
473
559
 
474
- add: function(model) {
475
- var modelTransaction = get(model, 'transaction');
476
- ember_assert("Models cannot belong to more than one transaction at a time.", !modelTransaction);
560
+ this.addToBucket('clean', record);
561
+ set(record, 'transaction', this);
562
+ },
477
563
 
478
- set(model, 'transaction', this);
564
+ modelBecameDirty: function(kind, record) {
565
+ this.removeFromBucket('clean', record);
566
+ this.addToBucket(kind, record);
479
567
  },
480
568
 
481
- modelBecameDirty: function(kind, model) {
482
- var dirty = get(get(this, 'dirty'), kind),
483
- type = model.constructor;
569
+ /** @private */
570
+ addToBucket: function(kind, record) {
571
+ var bucket = get(get(this, 'buckets'), kind),
572
+ type = record.constructor;
484
573
 
485
- var models = dirty.fetch(type);
574
+ var records = bucket.get(type);
486
575
 
487
- models = models || dirty.add(type, OrderedSet.create());
488
- models.add(model);
576
+ if (!records) {
577
+ records = Ember.OrderedSet.create();
578
+ bucket.set(type, records);
579
+ }
580
+
581
+ records.add(record);
489
582
  },
490
583
 
491
- modelBecameClean: function(kind, model) {
492
- var dirty = get(get(this, 'dirty'), kind),
493
- type = model.constructor;
584
+ /** @private */
585
+ removeFromBucket: function(kind, record) {
586
+ var bucket = get(get(this, 'buckets'), kind),
587
+ type = record.constructor;
588
+
589
+ var records = bucket.get(type);
590
+ records.remove(record);
591
+ },
494
592
 
495
- var models = dirty.fetch(type);
496
- models.remove(model);
593
+ modelBecameClean: function(kind, record) {
594
+ this.removeFromBucket(kind, record);
497
595
 
498
- set(model, 'transaction', null);
596
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
597
+ defaultTransaction.adoptRecord(record);
499
598
  },
500
599
 
501
600
  commit: function() {
502
- var dirtyMap = get(this, 'dirty');
601
+ var buckets = get(this, 'buckets');
503
602
 
504
603
  var iterate = function(kind, fn, binding) {
505
- var dirty = get(dirtyMap, kind);
604
+ var dirty = get(buckets, kind);
506
605
 
507
606
  dirty.forEach(function(type, models) {
508
607
  if (models.isEmpty()) { return; }
509
608
 
510
- models.forEach(function(model) { model.willCommit(); });
511
- fn.call(binding, type, models.toArray());
609
+ var array = [];
610
+
611
+ models.forEach(function(model) {
612
+ model.send('willCommit');
613
+
614
+ if (get(model, 'isPending') === false) {
615
+ array.push(model);
616
+ }
617
+ });
618
+
619
+ fn.call(binding, type, array);
512
620
  });
513
621
  };
514
622
 
@@ -528,6 +636,16 @@ DS.Transaction = Ember.Object.extend({
528
636
 
529
637
  var store = get(this, 'store');
530
638
  var adapter = get(store, '_adapter');
639
+
640
+ var clean = get(buckets, 'clean');
641
+ var defaultTransaction = get(store, 'defaultTransaction');
642
+
643
+ clean.forEach(function(type, records) {
644
+ records.forEach(function(record) {
645
+ this.remove(record);
646
+ }, this);
647
+ }, this);
648
+
531
649
  if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
532
650
  else { throw fmt("Adapter is either null or do not implement `commit` method", this); }
533
651
  }
@@ -539,46 +657,12 @@ DS.Transaction = Ember.Object.extend({
539
657
  (function(exports) {
540
658
  var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
541
659
 
542
- var OrderedSet = Ember.Object.extend({
543
- init: function() {
544
- this.clear();
545
- },
546
-
547
- clear: function() {
548
- this.set('presenceSet', {});
549
- this.set('list', Ember.NativeArray.apply([]));
550
- },
551
-
552
- add: function(obj) {
553
- var guid = Ember.guidFor(obj),
554
- presenceSet = get(this, 'presenceSet'),
555
- list = get(this, 'list');
556
-
557
- if (guid in presenceSet) { return; }
558
-
559
- presenceSet[guid] = true;
560
- list.pushObject(obj);
561
- },
562
-
563
- remove: function(obj) {
564
- var guid = Ember.guidFor(obj),
565
- presenceSet = get(this, 'presenceSet'),
566
- list = get(this, 'list');
567
-
568
- delete presenceSet[guid];
569
- list.removeObject(obj);
570
- },
571
-
572
- isEmpty: function() {
573
- return getPath(this, 'list.length') === 0;
574
- },
575
-
576
- forEach: function(fn, self) {
577
- get(this, 'list').forEach(function(item) {
578
- fn.call(self, item);
579
- });
660
+ var DATA_PROXY = {
661
+ get: function(name) {
662
+ return this.savedData[name];
580
663
  }
581
- });
664
+ };
665
+
582
666
 
583
667
  // Implementors Note:
584
668
  //
@@ -627,33 +711,49 @@ DS.Store = Ember.Object.extend({
627
711
  The init method registers this store as the default if none is specified.
628
712
  */
629
713
  init: function() {
714
+ // Enforce API revisioning. See BREAKING_CHANGES.md for more.
715
+ var revision = get(this, 'revision');
716
+
717
+ if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
718
+ throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION);
719
+ }
720
+
630
721
  if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
631
722
  set(DS, 'defaultStore', this);
632
723
  }
633
724
 
634
- set(this, 'data', []);
635
- set(this, '_typeMap', {});
636
- set(this, 'models', []);
637
- set(this, 'modelArrays', []);
638
- set(this, 'modelArraysByClientId', {});
639
- set(this, 'defaultTransaction', DS.Transaction.create({ store: this }));
725
+ // internal bookkeeping; not observable
726
+ this.typeMaps = {};
727
+ this.recordCache = [];
728
+ this.clientIdToId = {};
729
+ this.modelArraysByClientId = {};
730
+
731
+ set(this, 'defaultTransaction', this.transaction());
640
732
 
641
733
  return this._super();
642
734
  },
643
735
 
736
+ /**
737
+ Returns a new transaction scoped to this store.
738
+
739
+ @see {DS.Transaction}
740
+ @returns DS.Transaction
741
+ */
644
742
  transaction: function() {
645
743
  return DS.Transaction.create({ store: this });
646
744
  },
647
745
 
648
- modelArraysForClientId: function(clientId) {
649
- var modelArrays = get(this, 'modelArraysByClientId');
650
- var ret = modelArrays[clientId];
746
+ /**
747
+ @private
651
748
 
652
- if (!ret) {
653
- ret = modelArrays[clientId] = OrderedSet.create();
654
- }
749
+ This is used only by the model's DataProxy. Do not use this directly.
750
+ */
751
+ dataForRecord: function(record) {
752
+ var type = record.constructor,
753
+ clientId = get(record, 'clientId'),
754
+ typeMap = this.typeMapFor(type);
655
755
 
656
- return ret;
756
+ return typeMap.cidToHash[clientId];
657
757
  },
658
758
 
659
759
  /**
@@ -666,6 +766,13 @@ DS.Store = Ember.Object.extend({
666
766
  */
667
767
  adapter: null,
668
768
 
769
+ /**
770
+ @private
771
+
772
+ This property returns the adapter, after resolving a possible String.
773
+
774
+ @returns DS.Adapter
775
+ */
669
776
  _adapter: Ember.computed(function() {
670
777
  var adapter = get(this, 'adapter');
671
778
  if (typeof adapter === 'string') {
@@ -674,45 +781,80 @@ DS.Store = Ember.Object.extend({
674
781
  return adapter;
675
782
  }).property('adapter').cacheable(),
676
783
 
784
+ // A monotonically increasing number to be used to uniquely identify
785
+ // data hashes and records.
677
786
  clientIdCounter: -1,
678
787
 
679
788
  // ....................
680
789
  // . CREATE NEW MODEL .
681
790
  // ....................
682
791
 
683
- createRecord: function(type, hash, transaction) {
684
- hash = hash || {};
685
-
686
- var id = hash[getPath(type, 'proto.primaryKey')] || null;
792
+ /**
793
+ Create a new record in the current store. The properties passed
794
+ to this method are set on the newly created record.
687
795
 
688
- var model = type.create({
689
- data: hash || {},
690
- store: this,
691
- transaction: transaction
796
+ @param {subclass of DS.Model} type
797
+ @param {Object} properties a hash of properties to set on the
798
+ newly created record.
799
+ @returns DS.Model
800
+ */
801
+ createRecord: function(type, properties, transaction) {
802
+ properties = properties || {};
803
+
804
+ // Create a new instance of the model `type` and put it
805
+ // into the specified `transaction`. If no transaction is
806
+ // specified, the default transaction will be used.
807
+ //
808
+ // NOTE: A `transaction` is specified when the
809
+ // `transaction.createRecord` API is used.
810
+ var record = type._create({
811
+ store: this
692
812
  });
693
813
 
694
- model.adapterDidCreate();
814
+ transaction = transaction || get(this, 'defaultTransaction');
815
+ transaction.adoptRecord(record);
695
816
 
696
- var data = this.clientIdToHashMap(type);
697
- var models = get(this, 'models');
817
+ // Extract the primary key from the `properties` hash,
818
+ // based on the `primaryKey` for the model type.
819
+ var id = properties[get(record, 'primaryKey')] || null;
698
820
 
699
- var clientId = this.pushHash(hash, id, type);
821
+ var hash = {}, clientId;
700
822
 
701
- set(model, 'clientId', clientId);
823
+ // Push the hash into the store. If present, associate the
824
+ // extracted `id` with the hash.
825
+ clientId = this.pushHash(hash, id, type);
702
826
 
703
- models[clientId] = model;
827
+ record.send('didChangeData');
704
828
 
705
- this.updateModelArrays(type, clientId, hash);
829
+ var recordCache = get(this, 'recordCache');
706
830
 
707
- return model;
831
+ // Now that we have a clientId, attach it to the record we
832
+ // just created.
833
+ set(record, 'clientId', clientId);
834
+
835
+ // Store the record we just created in the record cache for
836
+ // this clientId.
837
+ recordCache[clientId] = record;
838
+
839
+ // Set the properties specified on the record.
840
+ record.setProperties(properties);
841
+
842
+ this.updateModelArrays(type, clientId, get(record, 'data'));
843
+
844
+ return record;
708
845
  },
709
846
 
710
847
  // ................
711
848
  // . DELETE MODEL .
712
849
  // ................
713
850
 
714
- deleteRecord: function(model) {
715
- model.deleteRecord();
851
+ /**
852
+ For symmetry, a record can be deleted via the store.
853
+
854
+ @param {DS.Model} record
855
+ */
856
+ deleteRecord: function(record) {
857
+ record.send('deleteRecord');
716
858
  },
717
859
 
718
860
  // ...............
@@ -720,26 +862,64 @@ DS.Store = Ember.Object.extend({
720
862
  // ...............
721
863
 
722
864
  /**
723
- Finds a model by its id. If the data for that model has already been
724
- loaded, an instance of DS.Model with that data will be returned
725
- immediately. Otherwise, an empty DS.Model instance will be returned in
726
- the loading state. As soon as the requested data is available, the model
727
- will be moved into the loaded state and all of the information will be
728
- available.
865
+ This is the main entry point into finding records. The first
866
+ parameter to this method is always a subclass of `DS.Model`.
729
867
 
730
- Note that only one DS.Model instance is ever created per unique id for a
731
- given type.
868
+ You can use the `find` method on a subclass of `DS.Model`
869
+ directly if your application only has one store. For
870
+ example, instead of `store.find(App.Person, 1)`, you could
871
+ say `App.Person.find(1)`.
732
872
 
733
- Example:
873
+ ---
734
874
 
735
- var record = MyApp.store.find(MyApp.Person, 1234);
875
+ To find a record by ID, pass the `id` as the second parameter:
736
876
 
737
- @param {DS.Model} type
738
- @param {String|Number} id
877
+ store.find(App.Person, 1);
878
+ App.Person.find(1);
879
+
880
+ If the record with that `id` had not previously been loaded,
881
+ the store will return an empty record immediately and ask
882
+ the adapter to find the data by calling its `find` method.
883
+
884
+ The `find` method will always return the same object for a
885
+ given type and `id`. To check whether the adapter has populated
886
+ a record, you can check its `isLoaded` property.
887
+
888
+ ---
889
+
890
+ To find all records for a type, call `find` with no additional
891
+ parameters:
892
+
893
+ store.find(App.Person);
894
+ App.Person.find();
895
+
896
+ This will return a `ModelArray` representing all known records
897
+ for the given type and kick off a request to the adapter's
898
+ `findAll` method to load any additional records for the type.
899
+
900
+ The `ModelArray` returned by `find()` is live. If any more
901
+ records for the type are added at a later time through any
902
+ mechanism, it will automatically update to reflect the change.
903
+
904
+ ---
905
+
906
+ To find a record by a query, call `find` with a hash as the
907
+ second parameter:
908
+
909
+ store.find(App.Person, { page: 1 });
910
+ App.Person.find({ page: 1 });
911
+
912
+ This will return a `ModelArray` immediately, but it will always
913
+ be an empty `ModelArray` at first. It will call the adapter's
914
+ `findQuery` method, which will populate the `ModelArray` once
915
+ the server has returned results.
916
+
917
+ You can check whether a query results `ModelArray` has loaded
918
+ by checking its `isLoaded` property.
739
919
  */
740
920
  find: function(type, id, query) {
741
921
  if (id === undefined) {
742
- return this.findMany(type, null, null);
922
+ return this.findAll(type);
743
923
  }
744
924
 
745
925
  if (query !== undefined) {
@@ -758,10 +938,9 @@ DS.Store = Ember.Object.extend({
758
938
  },
759
939
 
760
940
  findByClientId: function(type, clientId, id) {
761
- var model;
762
-
763
- var models = get(this, 'models');
764
- var data = this.clientIdToHashMap(type);
941
+ var recordCache = get(this, 'recordCache'),
942
+ dataCache = this.typeMapFor(type).cidToHash,
943
+ model;
765
944
 
766
945
  // If there is already a clientId assigned for this
767
946
  // type/id combination, try to find an existing
@@ -769,27 +948,28 @@ DS.Store = Ember.Object.extend({
769
948
  // materialize a new model and set its data to the
770
949
  // value we already have.
771
950
  if (clientId !== undefined) {
772
- model = models[clientId];
951
+ model = recordCache[clientId];
773
952
 
774
953
  if (!model) {
775
954
  // create a new instance of the model in the
776
955
  // 'isLoading' state
777
- model = this.createModel(type, clientId);
956
+ model = this.materializeRecord(type, clientId);
778
957
 
779
- // immediately set its data
780
- model.setData(data[clientId] || null);
958
+ if (dataCache[clientId]) {
959
+ model.send('didChangeData');
960
+ }
781
961
  }
782
962
  } else {
783
963
  clientId = this.pushHash(null, id, type);
784
964
 
785
965
  // create a new instance of the model in the
786
966
  // 'isLoading' state
787
- model = this.createModel(type, clientId);
967
+ model = this.materializeRecord(type, clientId);
788
968
 
789
969
  // let the adapter set the data, possibly async
790
970
  var adapter = get(this, '_adapter');
791
971
  if (adapter && adapter.find) { adapter.find(this, type, id); }
792
- else { throw fmt("Adapter is either null or do not implement `find` method", this); }
972
+ else { throw fmt("Adapter is either null or does not implement `find` method", this); }
793
973
  }
794
974
 
795
975
  return model;
@@ -798,8 +978,10 @@ DS.Store = Ember.Object.extend({
798
978
  /** @private
799
979
  */
800
980
  findMany: function(type, ids, query) {
801
- var idToClientIdMap = this.idToClientIdMap(type);
802
- var data = this.clientIdToHashMap(type), needed;
981
+ var typeMap = this.typeMapFor(type),
982
+ idToClientIdMap = typeMap.idToCid,
983
+ data = typeMap.cidToHash,
984
+ needed;
803
985
 
804
986
  var clientIds = Ember.A([]);
805
987
 
@@ -822,17 +1004,17 @@ DS.Store = Ember.Object.extend({
822
1004
  if ((needed && get(needed, 'length') > 0) || query) {
823
1005
  var adapter = get(this, '_adapter');
824
1006
  if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
825
- else { throw fmt("Adapter is either null or do not implement `findMany` method", this); }
1007
+ else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
826
1008
  }
827
1009
 
828
- return this.createModelArray(type, clientIds);
1010
+ return this.createManyArray(type, clientIds);
829
1011
  },
830
1012
 
831
1013
  findQuery: function(type, query) {
832
1014
  var array = DS.AdapterPopulatedModelArray.create({ type: type, content: Ember.A([]), store: this });
833
1015
  var adapter = get(this, '_adapter');
834
1016
  if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
835
- else { throw fmt("Adapter is either null or do not implement `findQuery` method", this); }
1017
+ else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); }
836
1018
  return array;
837
1019
  },
838
1020
 
@@ -853,7 +1035,14 @@ DS.Store = Ember.Object.extend({
853
1035
  return array;
854
1036
  },
855
1037
 
856
- filter: function(type, filter) {
1038
+ filter: function(type, query, filter) {
1039
+ // allow an optional server query
1040
+ if (arguments.length === 3) {
1041
+ this.findQuery(type, query);
1042
+ } else if (arguments.length === 2) {
1043
+ filter = query;
1044
+ }
1045
+
857
1046
  var array = DS.FilteredModelArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
858
1047
 
859
1048
  this.registerModelArray(array, type, filter);
@@ -865,11 +1054,8 @@ DS.Store = Ember.Object.extend({
865
1054
  // . UPDATING .
866
1055
  // ............
867
1056
 
868
- hashWasUpdated: function(type, clientId) {
869
- var clientIdToHashMap = this.clientIdToHashMap(type);
870
- var hash = clientIdToHashMap[clientId];
871
-
872
- this.updateModelArrays(type, clientId, hash);
1057
+ hashWasUpdated: function(type, clientId, record) {
1058
+ this.updateModelArrays(type, clientId, get(record, 'data'));
873
1059
  },
874
1060
 
875
1061
  // ..............
@@ -877,11 +1063,14 @@ DS.Store = Ember.Object.extend({
877
1063
  // ..............
878
1064
 
879
1065
  commit: function() {
880
- get(this, 'defaultTransaction').commit();
1066
+ var defaultTransaction = get(this, 'defaultTransaction');
1067
+ set(this, 'defaultTransaction', this.transaction());
1068
+
1069
+ defaultTransaction.commit();
881
1070
  },
882
1071
 
883
1072
  didUpdateRecords: function(array, hashes) {
884
- if (arguments.length === 2) {
1073
+ if (hashes) {
885
1074
  array.forEach(function(model, idx) {
886
1075
  this.didUpdateRecord(model, hashes[idx]);
887
1076
  }, this);
@@ -893,72 +1082,89 @@ DS.Store = Ember.Object.extend({
893
1082
  },
894
1083
 
895
1084
  didUpdateRecord: function(model, hash) {
896
- if (arguments.length === 2) {
897
- var clientId = get(model, 'clientId');
898
- var data = this.clientIdToHashMap(model.constructor);
1085
+ if (hash) {
1086
+ var clientId = get(model, 'clientId'),
1087
+ dataCache = this.typeMapFor(model.constructor).cidToHash;
899
1088
 
900
- data[clientId] = hash;
901
- model.set('data', hash);
1089
+ dataCache[clientId] = hash;
1090
+ model.send('didChangeData');
902
1091
  }
903
1092
 
904
- model.adapterDidUpdate();
1093
+ model.send('didCommit');
905
1094
  },
906
1095
 
907
1096
  didDeleteRecords: function(array) {
908
1097
  array.forEach(function(model) {
909
- model.adapterDidDelete();
1098
+ model.send('didCommit');
910
1099
  });
911
1100
  },
912
1101
 
913
1102
  didDeleteRecord: function(model) {
914
- model.adapterDidDelete();
1103
+ model.send('didCommit');
915
1104
  },
916
1105
 
917
- didCreateRecords: function(type, array, hashes) {
918
- var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
1106
+ _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) {
1107
+ var recordData = get(record, 'data'), id, changes;
919
1108
 
920
- var idToClientIdMap = this.idToClientIdMap(type);
921
- var data = this.clientIdToHashMap(type);
922
- var idList = this.idList(type);
1109
+ if (hash) {
1110
+ typeMap.cidToHash[clientId] = hash;
1111
+
1112
+ // If the server returns a hash, we assume that the server's version
1113
+ // of the data supercedes the local changes.
1114
+ record.beginPropertyChanges();
1115
+ record.send('didChangeData');
1116
+ recordData.adapterDidUpdate(hash);
1117
+ record.endPropertyChanges();
923
1118
 
924
- for (var i=0, l=get(array, 'length'); i<l; i++) {
925
- var model = array[i], hash = hashes[i];
926
1119
  id = hash[primaryKey];
927
- clientId = get(model, 'clientId');
928
1120
 
929
- data[clientId] = hash;
930
- set(model, 'data', hash);
1121
+ typeMap.idToCid[id] = clientId;
1122
+ this.clientIdToId[clientId] = id;
1123
+ } else {
1124
+ recordData.commit();
1125
+ }
1126
+
1127
+ record.send('didCommit');
1128
+ },
931
1129
 
932
- idToClientIdMap[id] = clientId;
933
- idList.push(id);
934
1130
 
935
- model.adapterDidUpdate();
1131
+ didCreateRecords: function(type, array, hashes) {
1132
+ var primaryKey = getPath(type, 'proto.primaryKey'),
1133
+ typeMap = this.typeMapFor(type),
1134
+ id, clientId;
1135
+
1136
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
1137
+ var model = array[i], hash = hashes[i];
1138
+ clientId = get(model, 'clientId');
1139
+
1140
+ this._didCreateRecord(model, hash, typeMap, clientId, primaryKey);
936
1141
  }
937
1142
  },
938
1143
 
939
1144
  didCreateRecord: function(model, hash) {
940
- var type = model.constructor;
1145
+ var type = model.constructor,
1146
+ typeMap = this.typeMapFor(type),
1147
+ id, clientId, primaryKey;
941
1148
 
942
- var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
1149
+ // The hash is optional, but if it is not provided, the client must have
1150
+ // provided a primary key.
943
1151
 
944
- var idToClientIdMap = this.idToClientIdMap(type);
945
- var data = this.clientIdToHashMap(type);
946
- var idList = this.idList(type);
1152
+ primaryKey = getPath(type, 'proto.primaryKey');
947
1153
 
948
- id = hash[primaryKey];
1154
+ // TODO: Make ember_assert more flexible and convert this into an ember_assert
1155
+ if (hash) {
1156
+ ember_assert("The server must provide a primary key: " + primaryKey, get(hash, primaryKey));
1157
+ } else {
1158
+ ember_assert("The server did not return data, and you did not create a primary key (" + primaryKey + ") on the client", get(get(model, 'data'), primaryKey));
1159
+ }
949
1160
 
950
1161
  clientId = get(model, 'clientId');
951
- data[clientId] = hash;
952
- set(model, 'data', hash);
953
-
954
- idToClientIdMap[id] = clientId;
955
- idList.push(id);
956
1162
 
957
- model.adapterDidUpdate();
1163
+ this._didCreateRecord(model, hash, typeMap, clientId, primaryKey);
958
1164
  },
959
1165
 
960
1166
  recordWasInvalid: function(record, errors) {
961
- record.wasInvalid(errors);
1167
+ record.send('becameInvalid', errors);
962
1168
  },
963
1169
 
964
1170
  // ................
@@ -966,16 +1172,15 @@ DS.Store = Ember.Object.extend({
966
1172
  // ................
967
1173
 
968
1174
  registerModelArray: function(array, type, filter) {
969
- var modelArrays = get(this, 'modelArrays');
970
- var idToClientIdMap = this.idToClientIdMap(type);
1175
+ var modelArrays = this.typeMapFor(type).modelArrays;
971
1176
 
972
1177
  modelArrays.push(array);
973
1178
 
974
1179
  this.updateModelArrayFilter(array, type, filter);
975
1180
  },
976
1181
 
977
- createModelArray: function(type, clientIds) {
978
- var array = DS.ModelArray.create({ type: type, content: clientIds, store: this });
1182
+ createManyArray: function(type, clientIds) {
1183
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
979
1184
 
980
1185
  clientIds.forEach(function(clientId) {
981
1186
  var modelArrays = this.modelArraysForClientId(clientId);
@@ -986,40 +1191,46 @@ DS.Store = Ember.Object.extend({
986
1191
  },
987
1192
 
988
1193
  updateModelArrayFilter: function(array, type, filter) {
989
- var data = this.clientIdToHashMap(type);
990
- var allClientIds = this.clientIdList(type);
1194
+ var typeMap = this.typeMapFor(type),
1195
+ dataCache = typeMap.cidToHash,
1196
+ clientIds = typeMap.clientIds,
1197
+ clientId, hash, proxy;
1198
+
1199
+ var recordCache = get(this, 'recordCache'), record;
991
1200
 
992
- for (var i=0, l=allClientIds.length; i<l; i++) {
993
- clientId = allClientIds[i];
1201
+ for (var i=0, l=clientIds.length; i<l; i++) {
1202
+ clientId = clientIds[i];
994
1203
 
995
- hash = data[clientId];
1204
+ if (hash = dataCache[clientId]) {
1205
+ if (record = recordCache[clientId]) {
1206
+ proxy = get(record, 'data');
1207
+ } else {
1208
+ DATA_PROXY.savedData = hash;
1209
+ proxy = DATA_PROXY;
1210
+ }
996
1211
 
997
- if (hash) {
998
- this.updateModelArray(array, filter, type, clientId, hash);
1212
+ this.updateModelArray(array, filter, type, clientId, proxy);
999
1213
  }
1000
1214
  }
1001
1215
  },
1002
1216
 
1003
- updateModelArrays: function(type, clientId, hash) {
1004
- var modelArrays = get(this, 'modelArrays');
1217
+ updateModelArrays: function(type, clientId, dataProxy) {
1218
+ var modelArrays = this.typeMapFor(type).modelArrays,
1219
+ modelArrayType, filter;
1005
1220
 
1006
1221
  modelArrays.forEach(function(array) {
1007
- modelArrayType = get(array, 'type');
1008
- filter = get(array, 'filterFunction');
1009
-
1010
- if (type !== modelArrayType) { return; }
1011
-
1012
- this.updateModelArray(array, filter, type, clientId, hash);
1222
+ filter = get(array, 'filterFunction');
1223
+ this.updateModelArray(array, filter, type, clientId, dataProxy);
1013
1224
  }, this);
1014
1225
  },
1015
1226
 
1016
- updateModelArray: function(array, filter, type, clientId, hash) {
1227
+ updateModelArray: function(array, filter, type, clientId, dataProxy) {
1017
1228
  var shouldBeInArray;
1018
1229
 
1019
1230
  if (!filter) {
1020
1231
  shouldBeInArray = true;
1021
1232
  } else {
1022
- shouldBeInArray = filter(hash);
1233
+ shouldBeInArray = filter(dataProxy);
1023
1234
  }
1024
1235
 
1025
1236
  var content = get(array, 'content');
@@ -1047,44 +1258,39 @@ DS.Store = Ember.Object.extend({
1047
1258
  },
1048
1259
 
1049
1260
  // ............
1050
- // . TYPE MAP .
1261
+ // . INDEXING .
1051
1262
  // ............
1052
1263
 
1264
+ modelArraysForClientId: function(clientId) {
1265
+ var modelArrays = get(this, 'modelArraysByClientId');
1266
+ var ret = modelArrays[clientId];
1267
+
1268
+ if (!ret) {
1269
+ ret = modelArrays[clientId] = Ember.OrderedSet.create();
1270
+ }
1271
+
1272
+ return ret;
1273
+ },
1274
+
1053
1275
  typeMapFor: function(type) {
1054
- var ids = get(this, '_typeMap');
1276
+ var typeMaps = get(this, 'typeMaps');
1055
1277
  var guidForType = Ember.guidFor(type);
1056
1278
 
1057
- var typeMap = ids[guidForType];
1279
+ var typeMap = typeMaps[guidForType];
1058
1280
 
1059
1281
  if (typeMap) {
1060
1282
  return typeMap;
1061
1283
  } else {
1062
- return (ids[guidForType] =
1284
+ return (typeMaps[guidForType] =
1063
1285
  {
1064
1286
  idToCid: {},
1065
- idList: [],
1066
- cidList: [],
1067
- cidToHash: {}
1287
+ clientIds: [],
1288
+ cidToHash: {},
1289
+ modelArrays: []
1068
1290
  });
1069
1291
  }
1070
1292
  },
1071
1293
 
1072
- idToClientIdMap: function(type) {
1073
- return this.typeMapFor(type).idToCid;
1074
- },
1075
-
1076
- idList: function(type) {
1077
- return this.typeMapFor(type).idList;
1078
- },
1079
-
1080
- clientIdList: function(type) {
1081
- return this.typeMapFor(type).cidList;
1082
- },
1083
-
1084
- clientIdToHashMap: function(type) {
1085
- return this.typeMapFor(type).cidToHash;
1086
- },
1087
-
1088
1294
  /** @private
1089
1295
 
1090
1296
  For a given type and id combination, returns the client id used by the store.
@@ -1097,13 +1303,6 @@ DS.Store = Ember.Object.extend({
1097
1303
  return this.typeMapFor(type).idToCid[id];
1098
1304
  },
1099
1305
 
1100
- idForHash: function(type, hash) {
1101
- var primaryKey = getPath(type, 'proto.primaryKey');
1102
-
1103
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1104
- return hash[primaryKey];
1105
- },
1106
-
1107
1306
  // ................
1108
1307
  // . LOADING DATA .
1109
1308
  // ................
@@ -1124,28 +1323,28 @@ DS.Store = Ember.Object.extend({
1124
1323
  if (hash === undefined) {
1125
1324
  hash = id;
1126
1325
  var primaryKey = getPath(type, 'proto.primaryKey');
1127
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1326
+ ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", primaryKey in hash);
1128
1327
  id = hash[primaryKey];
1129
1328
  }
1130
1329
 
1131
- var data = this.clientIdToHashMap(type);
1132
- var models = get(this, 'models');
1133
-
1134
- var clientId = this.clientIdForId(type, id);
1330
+ var typeMap = this.typeMapFor(type),
1331
+ dataCache = typeMap.cidToHash,
1332
+ clientId = typeMap.idToCid[id],
1333
+ recordCache = get(this, 'recordCache');
1135
1334
 
1136
1335
  if (clientId !== undefined) {
1137
- data[clientId] = hash;
1336
+ dataCache[clientId] = hash;
1138
1337
 
1139
- var model = models[clientId];
1338
+ var model = recordCache[clientId];
1140
1339
  if (model) {
1141
- model.willLoadData();
1142
- model.setData(hash);
1340
+ model.send('didChangeData');
1143
1341
  }
1144
1342
  } else {
1145
1343
  clientId = this.pushHash(hash, id, type);
1146
1344
  }
1147
1345
 
1148
- this.updateModelArrays(type, clientId, hash);
1346
+ DATA_PROXY.savedData = hash;
1347
+ this.updateModelArrays(type, clientId, DATA_PROXY);
1149
1348
 
1150
1349
  return { id: id, clientId: clientId };
1151
1350
  },
@@ -1158,8 +1357,7 @@ DS.Store = Ember.Object.extend({
1158
1357
  ids = [];
1159
1358
  var primaryKey = getPath(type, 'proto.primaryKey');
1160
1359
 
1161
- ids = hashes.map(function(hash) {
1162
- ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
1360
+ ids = Ember.ArrayUtils.map(hashes, function(hash) {
1163
1361
  return hash[primaryKey];
1164
1362
  });
1165
1363
  }
@@ -1183,23 +1381,25 @@ DS.Store = Ember.Object.extend({
1183
1381
  @returns {Number}
1184
1382
  */
1185
1383
  pushHash: function(hash, id, type) {
1186
- var idToClientIdMap = this.idToClientIdMap(type);
1187
- var clientIdList = this.clientIdList(type);
1188
- var idList = this.idList(type);
1189
- var data = this.clientIdToHashMap(type);
1384
+ var typeMap = this.typeMapFor(type);
1385
+
1386
+ var idToClientIdMap = typeMap.idToCid,
1387
+ clientIdToIdMap = this.clientIdToId,
1388
+ clientIds = typeMap.clientIds,
1389
+ dataCache = typeMap.cidToHash;
1190
1390
 
1191
- var clientId = this.incrementProperty('clientIdCounter');
1391
+ var clientId = ++this.clientIdCounter;
1192
1392
 
1193
- data[clientId] = hash;
1393
+ dataCache[clientId] = hash;
1194
1394
 
1195
1395
  // if we're creating an item, this process will be done
1196
1396
  // later, once the object has been persisted.
1197
1397
  if (id) {
1198
1398
  idToClientIdMap[id] = clientId;
1199
- idList.push(id);
1399
+ clientIdToIdMap[clientId] = id;
1200
1400
  }
1201
1401
 
1202
- clientIdList.push(clientId);
1402
+ clientIds.push(clientId);
1203
1403
 
1204
1404
  return clientId;
1205
1405
  },
@@ -1208,22 +1408,34 @@ DS.Store = Ember.Object.extend({
1208
1408
  // . MODEL MATERIALIZATION .
1209
1409
  // .........................
1210
1410
 
1211
- createModel: function(type, clientId) {
1411
+ materializeRecord: function(type, clientId) {
1212
1412
  var model;
1213
1413
 
1214
- get(this, 'models')[clientId] = model = type.create({ store: this, clientId: clientId });
1215
- set(model, 'clientId', clientId);
1216
- model.loadingData();
1414
+ get(this, 'recordCache')[clientId] = model = type._create({
1415
+ store: this,
1416
+ clientId: clientId
1417
+ });
1418
+
1419
+ get(this, 'defaultTransaction').adoptRecord(model);
1420
+
1421
+ model.send('loadingData');
1217
1422
  return model;
1423
+ },
1424
+
1425
+ destroy: function() {
1426
+ if (get(DS, 'defaultStore') === this) {
1427
+ set(DS, 'defaultStore', null);
1428
+ }
1429
+
1430
+ return this._super();
1218
1431
  }
1219
1432
  });
1220
1433
 
1221
-
1222
1434
  })({});
1223
1435
 
1224
1436
 
1225
1437
  (function(exports) {
1226
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
1438
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
1227
1439
 
1228
1440
  var stateProperty = Ember.computed(function(key) {
1229
1441
  var parent = get(this, 'parentState');
@@ -1232,116 +1444,401 @@ var stateProperty = Ember.computed(function(key) {
1232
1444
  }
1233
1445
  }).property();
1234
1446
 
1235
- DS.State = Ember.State.extend({
1236
- isLoaded: stateProperty,
1237
- isDirty: stateProperty,
1238
- isSaving: stateProperty,
1239
- isDeleted: stateProperty,
1240
- isError: stateProperty,
1241
- isNew: stateProperty,
1242
- isValid: stateProperty
1243
- });
1447
+ var isEmptyObject = function(object) {
1448
+ for (var name in object) {
1449
+ if (object.hasOwnProperty(name)) { return false; }
1450
+ }
1244
1451
 
1245
- var cantLoadData = function() {
1246
- // TODO: get the current state name
1247
- throw "You cannot load data into the store when its associated model is in its current state";
1452
+ return true;
1248
1453
  };
1249
1454
 
1250
- var isEmptyObject = function(obj) {
1251
- for (var prop in obj) {
1252
- if (!obj.hasOwnProperty(prop)) { continue; }
1253
- return false;
1455
+ var hasDefinedProperties = function(object) {
1456
+ for (var name in object) {
1457
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
1254
1458
  }
1255
1459
 
1256
- return true;
1460
+ return false;
1257
1461
  };
1258
1462
 
1463
+ DS.State = Ember.State.extend({
1464
+ isLoaded: stateProperty,
1465
+ isDirty: stateProperty,
1466
+ isSaving: stateProperty,
1467
+ isDeleted: stateProperty,
1468
+ isError: stateProperty,
1469
+ isNew: stateProperty,
1470
+ isValid: stateProperty,
1471
+ isPending: stateProperty,
1472
+
1473
+ // For states that are substates of a
1474
+ // DirtyState (updated or created), it is
1475
+ // useful to be able to determine which
1476
+ // type of dirty state it is.
1477
+ dirtyType: stateProperty
1478
+ });
1479
+
1259
1480
  var setProperty = function(manager, context) {
1260
1481
  var key = context.key, value = context.value;
1261
1482
 
1262
- var model = get(manager, 'model'), type = model.constructor;
1263
- var store = get(model, 'store');
1264
- var data = get(model, 'data');
1483
+ var model = get(manager, 'model'),
1484
+ data = get(model, 'data');
1485
+
1486
+ set(data, key, value);
1487
+ };
1265
1488
 
1266
- data[key] = value;
1489
+ var didChangeData = function(manager) {
1490
+ var model = get(manager, 'model'),
1491
+ data = get(model, 'data');
1267
1492
 
1268
- if (store) { store.hashWasUpdated(type, get(model, 'clientId')); }
1493
+ data._savedData = null;
1494
+ model.notifyPropertyChange('data');
1269
1495
  };
1270
1496
 
1271
- // several states share extremely common functionality, so we are factoring
1272
- // them out into a common class.
1273
- var DirtyState = DS.State.extend({
1274
- // these states are virtually identical except that
1275
- // they (thrice) use their states name explicitly.
1276
- //
1277
- // child classes implement stateName.
1278
- stateName: null,
1279
- isDirty: true,
1280
- willLoadData: cantLoadData,
1497
+ // The waitingOn event shares common functionality
1498
+ // between the different dirty states, but each is
1499
+ // treated slightly differently. This method is exposed
1500
+ // so that each implementation can invoke the common
1501
+ // behavior, and then implement the behavior specific
1502
+ // to the state.
1503
+ var waitingOn = function(manager, object) {
1504
+ var model = get(manager, 'model'),
1505
+ pendingQueue = get(model, 'pendingQueue'),
1506
+ objectGuid = guidFor(object);
1507
+
1508
+ var observer = function() {
1509
+ if (get(object, 'id')) {
1510
+ manager.send('doneWaitingOn', object);
1511
+ Ember.removeObserver(object, 'id', observer);
1512
+ }
1513
+ };
1514
+
1515
+ pendingQueue[objectGuid] = [object, observer];
1516
+ Ember.addObserver(object, 'id', observer);
1517
+ };
1518
+
1519
+ // Implementation notes:
1520
+ //
1521
+ // Each state has a boolean value for all of the following flags:
1522
+ //
1523
+ // * isLoaded: The record has a populated `data` property. When a
1524
+ // record is loaded via `store.find`, `isLoaded` is false
1525
+ // until the adapter sets it. When a record is created locally,
1526
+ // its `isLoaded` property is always true.
1527
+ // * isDirty: The record has local changes that have not yet been
1528
+ // saved by the adapter. This includes records that have been
1529
+ // created (but not yet saved) or deleted.
1530
+ // * isSaving: The record's transaction has been committed, but
1531
+ // the adapter has not yet acknowledged that the changes have
1532
+ // been persisted to the backend.
1533
+ // * isDeleted: The record was marked for deletion. When `isDeleted`
1534
+ // is true and `isDirty` is true, the record is deleted locally
1535
+ // but the deletion was not yet persisted. When `isSaving` is
1536
+ // true, the change is in-flight. When both `isDirty` and
1537
+ // `isSaving` are false, the change has persisted.
1538
+ // * isError: The adapter reported that it was unable to save
1539
+ // local changes to the backend. This may also result in the
1540
+ // record having its `isValid` property become false if the
1541
+ // adapter reported that server-side validations failed.
1542
+ // * isNew: The record was created on the client and the adapter
1543
+ // did not yet report that it was successfully saved.
1544
+ // * isValid: No client-side validations have failed and the
1545
+ // adapter did not report any server-side validation failures.
1546
+ // * isPending: A record `isPending` when it belongs to an
1547
+ // association on another record and that record has not been
1548
+ // saved. A record in this state cannot be saved because it
1549
+ // lacks a "foreign key" that will be supplied by its parent
1550
+ // association when the parent record has been created. When
1551
+ // the adapter reports that the parent has saved, the
1552
+ // `isPending` property on all children will become `false`
1553
+ // and the transaction will try to commit the records.
1554
+
1555
+ // This mixin is mixed into various uncommitted states. Make
1556
+ // sure to mix it in *after* the class definition, so its
1557
+ // super points to the class definition.
1558
+ var Uncommitted = Ember.Mixin.create({
1559
+ setProperty: setProperty,
1281
1560
 
1282
- enter: function(manager) {
1283
- var stateName = get(this, 'stateName'),
1284
- model = get(manager, 'model');
1561
+ deleteRecord: function(manager) {
1562
+ this._super(manager);
1285
1563
 
1286
- model.withTransaction(function (t) {
1287
- t.modelBecameDirty(stateName, model);
1564
+ var model = get(manager, 'model'),
1565
+ dirtyType = get(this, 'dirtyType');
1566
+
1567
+ model.withTransaction(function(t) {
1568
+ t.modelBecameClean(dirtyType, model);
1288
1569
  });
1289
- },
1570
+ }
1571
+ });
1572
+
1573
+ // These mixins are mixed into substates of the concrete
1574
+ // subclasses of DirtyState.
1575
+
1576
+ var CreatedUncommitted = Ember.Mixin.create({
1577
+ deleteRecord: function(manager) {
1578
+ this._super(manager);
1579
+
1580
+ manager.goToState('deleted.saved');
1581
+ }
1582
+ });
1290
1583
 
1291
- exit: function(manager) {
1292
- var stateName = get(this, 'stateName'),
1293
- model = get(manager, 'model');
1584
+ var UpdatedUncommitted = Ember.Mixin.create({
1585
+ deleteRecord: function(manager) {
1586
+ this._super(manager);
1294
1587
 
1295
- this.notifyModel(model);
1588
+ var model = get(manager, 'model');
1296
1589
 
1297
- model.withTransaction(function (t) {
1298
- t.modelBecameClean(stateName, model);
1590
+ model.withTransaction(function(t) {
1591
+ t.modelBecameClean('created', model);
1299
1592
  });
1300
- },
1301
1593
 
1302
- setProperty: setProperty,
1594
+ manager.goToState('deleted');
1595
+ }
1596
+ });
1303
1597
 
1304
- willCommit: function(manager) {
1305
- manager.goToState('saving');
1306
- },
1598
+ // The dirty state is a abstract state whose functionality is
1599
+ // shared between the `created` and `updated` states.
1600
+ //
1601
+ // The deleted state shares the `isDirty` flag with the
1602
+ // subclasses of `DirtyState`, but with a very different
1603
+ // implementation.
1604
+ var DirtyState = DS.State.extend({
1605
+ initialState: 'uncommitted',
1606
+
1607
+ // FLAGS
1608
+ isDirty: true,
1609
+
1610
+ // SUBSTATES
1611
+
1612
+ // When a record first becomes dirty, it is `uncommitted`.
1613
+ // This means that there are local pending changes,
1614
+ // but they have not yet begun to be saved.
1615
+ uncommitted: DS.State.extend({
1616
+ // TRANSITIONS
1617
+ enter: function(manager) {
1618
+ var dirtyType = get(this, 'dirtyType'),
1619
+ model = get(manager, 'model');
1620
+
1621
+ model.withTransaction(function (t) {
1622
+ t.modelBecameDirty(dirtyType, model);
1623
+ });
1624
+ },
1625
+
1626
+ exit: function(manager) {
1627
+ var model = get(manager, 'model');
1628
+ manager.send('invokeLifecycleCallbacks', model);
1629
+ },
1630
+
1631
+ // EVENTS
1632
+ deleteRecord: Ember.K,
1307
1633
 
1308
- saving: DS.State.extend({
1634
+ waitingOn: function(manager, object) {
1635
+ waitingOn(manager, object);
1636
+ manager.goToState('pending');
1637
+ },
1638
+
1639
+ willCommit: function(manager) {
1640
+ manager.goToState('inFlight');
1641
+ }
1642
+ }, Uncommitted),
1643
+
1644
+ // Once a record has been handed off to the adapter to be
1645
+ // saved, it is in the 'in flight' state. Changes to the
1646
+ // record cannot be made during this window.
1647
+ inFlight: DS.State.extend({
1648
+ // FLAGS
1309
1649
  isSaving: true,
1310
1650
 
1311
- didUpdate: function(manager) {
1651
+ // TRANSITIONS
1652
+ enter: function(manager) {
1653
+ var dirtyType = get(this, 'dirtyType'),
1654
+ model = get(manager, 'model');
1655
+
1656
+ model.withTransaction(function (t) {
1657
+ t.modelBecameClean(dirtyType, model);
1658
+ });
1659
+ },
1660
+
1661
+ // EVENTS
1662
+ didCommit: function(manager) {
1312
1663
  manager.goToState('loaded');
1313
1664
  },
1314
1665
 
1315
- wasInvalid: function(manager, errors) {
1666
+ becameInvalid: function(manager, errors) {
1316
1667
  var model = get(manager, 'model');
1317
1668
 
1318
1669
  set(model, 'errors', errors);
1319
1670
  manager.goToState('invalid');
1320
- }
1671
+ },
1672
+
1673
+ didChangeData: didChangeData
1674
+ }),
1675
+
1676
+ // If a record becomes associated with a newly created
1677
+ // parent record, it will be `pending` until the parent
1678
+ // record has successfully persisted. Once this happens,
1679
+ // this record can use the parent's primary key as its
1680
+ // foreign key.
1681
+ //
1682
+ // If the record's transaction had already started to
1683
+ // commit, the record will transition to the `inFlight`
1684
+ // state. If it had not, the record will transition to
1685
+ // the `uncommitted` state.
1686
+ pending: DS.State.extend({
1687
+ initialState: 'uncommitted',
1688
+
1689
+ // FLAGS
1690
+ isPending: true,
1691
+
1692
+ // SUBSTATES
1693
+
1694
+ // A pending record whose transaction has not yet
1695
+ // started to commit is in this state.
1696
+ uncommitted: DS.State.extend({
1697
+ // EVENTS
1698
+ deleteRecord: function(manager) {
1699
+ var model = get(manager, 'model'),
1700
+ pendingQueue = get(model, 'pendingQueue'),
1701
+ tuple;
1702
+
1703
+ // since we are leaving the pending state, remove any
1704
+ // observers we have registered on other records.
1705
+ for (var prop in pendingQueue) {
1706
+ if (!pendingQueue.hasOwnProperty(prop)) { continue; }
1707
+
1708
+ tuple = pendingQueue[prop];
1709
+ Ember.removeObserver(tuple[0], 'id', tuple[1]);
1710
+ }
1711
+ },
1712
+
1713
+ willCommit: function(manager) {
1714
+ manager.goToState('committing');
1715
+ },
1716
+
1717
+ doneWaitingOn: function(manager, object) {
1718
+ var model = get(manager, 'model'),
1719
+ pendingQueue = get(model, 'pendingQueue'),
1720
+ objectGuid = guidFor(object);
1721
+
1722
+ delete pendingQueue[objectGuid];
1723
+
1724
+ if (isEmptyObject(pendingQueue)) {
1725
+ manager.send('doneWaiting');
1726
+ }
1727
+ },
1728
+
1729
+ doneWaiting: function(manager) {
1730
+ var dirtyType = get(this, 'dirtyType');
1731
+ manager.goToState(dirtyType + '.uncommitted');
1732
+ }
1733
+ }, Uncommitted),
1734
+
1735
+ // A pending record whose transaction has started
1736
+ // to commit is in this state. Since it has not yet
1737
+ // been sent to the adapter, it is not `inFlight`
1738
+ // until all of its dependencies have been committed.
1739
+ committing: DS.State.extend({
1740
+ // FLAGS
1741
+ isSaving: true,
1742
+
1743
+ // EVENTS
1744
+ doneWaitingOn: function(manager, object) {
1745
+ var model = get(manager, 'model'),
1746
+ pendingQueue = get(model, 'pendingQueue'),
1747
+ objectGuid = guidFor(object);
1748
+
1749
+ delete pendingQueue[objectGuid];
1750
+
1751
+ if (isEmptyObject(pendingQueue)) {
1752
+ manager.send('doneWaiting');
1753
+ }
1754
+ },
1755
+
1756
+ doneWaiting: function(manager) {
1757
+ var model = get(manager, 'model'),
1758
+ transaction = get(model, 'transaction');
1759
+
1760
+ // Now that the model is no longer pending, schedule
1761
+ // the transaction to commit.
1762
+ Ember.run.once(transaction, transaction.commit);
1763
+ },
1764
+
1765
+ willCommit: function(manager) {
1766
+ var dirtyType = get(this, 'dirtyType');
1767
+ manager.goToState(dirtyType + '.inFlight');
1768
+ }
1769
+ })
1321
1770
  }),
1322
1771
 
1772
+ // A record is in the `invalid` state when its client-side
1773
+ // invalidations have failed, or if the adapter has indicated
1774
+ // the the record failed server-side invalidations.
1323
1775
  invalid: DS.State.extend({
1776
+ // FLAGS
1324
1777
  isValid: false,
1325
1778
 
1779
+ // EVENTS
1780
+ deleteRecord: function(manager) {
1781
+ manager.goToState('deleted');
1782
+ },
1783
+
1326
1784
  setProperty: function(manager, context) {
1327
1785
  setProperty(manager, context);
1328
1786
 
1329
- var stateName = getPath(this, 'parentState.stateName'),
1330
- model = get(manager, 'model'),
1787
+ var model = get(manager, 'model'),
1331
1788
  errors = get(model, 'errors'),
1332
1789
  key = context.key;
1333
1790
 
1334
1791
  delete errors[key];
1335
1792
 
1336
- if (isEmptyObject(errors)) {
1337
- manager.goToState(stateName);
1793
+ if (!hasDefinedProperties(errors)) {
1794
+ manager.send('becameValid');
1338
1795
  }
1796
+ },
1797
+
1798
+ becameValid: function(manager) {
1799
+ manager.goToState('uncommitted');
1339
1800
  }
1340
1801
  })
1341
1802
  });
1342
1803
 
1804
+ // The created and updated states are created outside the state
1805
+ // chart so we can reopen their substates and add mixins as
1806
+ // necessary.
1807
+
1808
+ var createdState = DirtyState.create({
1809
+ dirtyType: 'created',
1810
+
1811
+ // FLAGS
1812
+ isNew: true,
1813
+
1814
+ // EVENTS
1815
+ invokeLifecycleCallbacks: function(manager, model) {
1816
+ model.didCreate();
1817
+ }
1818
+ });
1819
+
1820
+ var updatedState = DirtyState.create({
1821
+ dirtyType: 'updated',
1822
+
1823
+ // EVENTS
1824
+ invokeLifecycleCallbacks: function(manager, model) {
1825
+ model.didUpdate();
1826
+ }
1827
+ });
1828
+
1829
+ // The created.uncommitted state and created.pending.uncommitted share
1830
+ // some logic defined in CreatedUncommitted.
1831
+ createdState.states.uncommitted.reopen(CreatedUncommitted);
1832
+ createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
1833
+
1834
+ // The updated.uncommitted state and updated.pending.uncommitted share
1835
+ // some logic defined in UpdatedUncommitted.
1836
+ updatedState.states.uncommitted.reopen(UpdatedUncommitted);
1837
+ updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
1838
+
1343
1839
  var states = {
1344
1840
  rootState: Ember.State.create({
1841
+ // FLAGS
1345
1842
  isLoaded: false,
1346
1843
  isDirty: false,
1347
1844
  isSaving: false,
@@ -1349,118 +1846,163 @@ var states = {
1349
1846
  isError: false,
1350
1847
  isNew: false,
1351
1848
  isValid: true,
1849
+ isPending: false,
1352
1850
 
1353
- willLoadData: cantLoadData,
1354
-
1355
- didCreate: function(manager) {
1356
- manager.goToState('loaded.created');
1357
- },
1851
+ // SUBSTATES
1358
1852
 
1853
+ // A record begins its lifecycle in the `empty` state.
1854
+ // If its data will come from the adapter, it will
1855
+ // transition into the `loading` state. Otherwise, if
1856
+ // the record is being created on the client, it will
1857
+ // transition into the `created` state.
1359
1858
  empty: DS.State.create({
1859
+ // EVENTS
1360
1860
  loadingData: function(manager) {
1361
1861
  manager.goToState('loading');
1862
+ },
1863
+
1864
+ didChangeData: function(manager) {
1865
+ didChangeData(manager);
1866
+
1867
+ manager.goToState('loaded.created');
1362
1868
  }
1363
1869
  }),
1364
1870
 
1871
+ // A record enters this state when the store askes
1872
+ // the adapter for its data. It remains in this state
1873
+ // until the adapter provides the requested data.
1874
+ //
1875
+ // Usually, this process is asynchronous, using an
1876
+ // XHR to retrieve the data.
1365
1877
  loading: DS.State.create({
1366
- willLoadData: Ember.K,
1367
-
1878
+ // TRANSITIONS
1368
1879
  exit: function(manager) {
1369
1880
  var model = get(manager, 'model');
1370
1881
  model.didLoad();
1371
1882
  },
1372
1883
 
1373
- setData: function(manager, data) {
1374
- var model = get(manager, 'model');
1375
-
1376
- model.beginPropertyChanges();
1377
- model.set('data', data);
1378
-
1379
- if (data !== null) {
1380
- manager.goToState('loaded');
1381
- }
1884
+ // EVENTS
1885
+ didChangeData: function(manager, data) {
1886
+ didChangeData(manager);
1887
+ manager.send('loadedData');
1888
+ },
1382
1889
 
1383
- model.endPropertyChanges();
1890
+ loadedData: function(manager) {
1891
+ manager.goToState('loaded');
1384
1892
  }
1385
1893
  }),
1386
1894
 
1895
+ // A record enters this state when its data is populated.
1896
+ // Most of a record's lifecycle is spent inside substates
1897
+ // of the `loaded` state.
1387
1898
  loaded: DS.State.create({
1899
+ initialState: 'saved',
1900
+
1901
+ // FLAGS
1388
1902
  isLoaded: true,
1389
1903
 
1390
- willLoadData: Ember.K,
1904
+ // SUBSTATES
1391
1905
 
1392
- setProperty: function(manager, context) {
1393
- setProperty(manager, context);
1394
- manager.goToState('updated');
1395
- },
1906
+ // If there are no local changes to a record, it remains
1907
+ // in the `saved` state.
1908
+ saved: DS.State.create({
1909
+ // EVENTS
1910
+ setProperty: function(manager, context) {
1911
+ setProperty(manager, context);
1912
+ manager.goToState('updated');
1913
+ },
1396
1914
 
1397
- 'delete': function(manager) {
1398
- manager.goToState('deleted');
1399
- },
1915
+ didChangeData: didChangeData,
1400
1916
 
1401
- created: DirtyState.create({
1402
- stateName: 'created',
1403
- isNew: true,
1917
+ deleteRecord: function(manager) {
1918
+ manager.goToState('deleted');
1919
+ },
1404
1920
 
1405
- notifyModel: function(model) {
1406
- model.didCreate();
1921
+ waitingOn: function(manager, object) {
1922
+ waitingOn(manager, object);
1923
+ manager.goToState('updated.pending');
1407
1924
  }
1408
1925
  }),
1409
1926
 
1410
- updated: DirtyState.create({
1411
- stateName: 'updated',
1927
+ // A record is in this state after it has been locally
1928
+ // created but before the adapter has indicated that
1929
+ // it has been saved.
1930
+ created: createdState,
1412
1931
 
1413
- notifyModel: function(model) {
1414
- model.didUpdate();
1415
- }
1416
- })
1932
+ // A record is in this state if it has already been
1933
+ // saved to the server, but there are new local changes
1934
+ // that have not yet been saved.
1935
+ updated: updatedState,
1417
1936
  }),
1418
1937
 
1938
+ // A record is in this state if it was deleted from the store.
1419
1939
  deleted: DS.State.create({
1940
+ // FLAGS
1420
1941
  isDeleted: true,
1421
1942
  isLoaded: true,
1422
1943
  isDirty: true,
1423
1944
 
1424
- willLoadData: cantLoadData,
1945
+ // SUBSTATES
1425
1946
 
1426
- enter: function(manager) {
1427
- var model = get(manager, 'model');
1428
- var store = get(model, 'store');
1947
+ // When a record is deleted, it enters the `start`
1948
+ // state. It will exit this state when the record's
1949
+ // transaction starts to commit.
1950
+ start: DS.State.create({
1951
+ // TRANSITIONS
1952
+ enter: function(manager) {
1953
+ var model = get(manager, 'model');
1954
+ var store = get(model, 'store');
1429
1955
 
1430
- if (store) {
1431
- store.removeFromModelArrays(model);
1432
- }
1956
+ if (store) {
1957
+ store.removeFromModelArrays(model);
1958
+ }
1433
1959
 
1434
- model.withTransaction(function(t) {
1435
- t.modelBecameDirty('deleted', model);
1436
- });
1437
- },
1960
+ model.withTransaction(function(t) {
1961
+ t.modelBecameDirty('deleted', model);
1962
+ });
1963
+ },
1438
1964
 
1439
- willCommit: function(manager) {
1440
- manager.goToState('saving');
1441
- },
1965
+ // EVENTS
1966
+ willCommit: function(manager) {
1967
+ manager.goToState('inFlight');
1968
+ }
1969
+ }),
1442
1970
 
1443
- saving: DS.State.create({
1971
+ // After a record's transaction is committing, but
1972
+ // before the adapter indicates that the deletion
1973
+ // has saved to the server, a record is in the
1974
+ // `inFlight` substate of `deleted`.
1975
+ inFlight: DS.State.create({
1976
+ // FLAGS
1444
1977
  isSaving: true,
1445
1978
 
1446
- didDelete: function(manager) {
1447
- manager.goToState('saved');
1448
- },
1449
-
1979
+ // TRANSITIONS
1450
1980
  exit: function(stateManager) {
1451
1981
  var model = get(stateManager, 'model');
1452
1982
 
1453
1983
  model.withTransaction(function(t) {
1454
1984
  t.modelBecameClean('deleted', model);
1455
1985
  });
1986
+ },
1987
+
1988
+ // EVENTS
1989
+ didCommit: function(manager) {
1990
+ manager.goToState('saved');
1456
1991
  }
1457
1992
  }),
1458
1993
 
1994
+ // Once the adapter indicates that the deletion has
1995
+ // been saved, the record enters the `saved` substate
1996
+ // of `deleted`.
1459
1997
  saved: DS.State.create({
1998
+ // FLAGS
1460
1999
  isDirty: false
1461
2000
  })
1462
2001
  }),
1463
2002
 
2003
+ // If the adapter indicates that there was an unknown
2004
+ // error saving a record, the record enters the `error`
2005
+ // state.
1464
2006
  error: DS.State.create({
1465
2007
  isError: true
1466
2008
  })
@@ -1473,10 +2015,110 @@ DS.StateManager = Ember.StateManager.extend({
1473
2015
  states: states
1474
2016
  });
1475
2017
 
2018
+ })({});
2019
+
2020
+
2021
+ (function(exports) {
2022
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
2023
+
1476
2024
  var retrieveFromCurrentState = Ember.computed(function(key) {
1477
2025
  return get(getPath(this, 'stateManager.currentState'), key);
1478
2026
  }).property('stateManager.currentState').cacheable();
1479
2027
 
2028
+ // This object is a regular JS object for performance. It is only
2029
+ // used internally for bookkeeping purposes.
2030
+ var DataProxy = function(record) {
2031
+ this.record = record;
2032
+ this.unsavedData = {};
2033
+ this.associations = {};
2034
+ };
2035
+
2036
+ DataProxy.prototype = {
2037
+ get: function(key) { return Ember.get(this, key); },
2038
+ set: function(key, value) { return Ember.set(this, key, value); },
2039
+
2040
+ setAssociation: function(key, value) {
2041
+ this.associations[key] = value;
2042
+ },
2043
+
2044
+ savedData: function() {
2045
+ var savedData = this._savedData;
2046
+ if (savedData) { return savedData; }
2047
+
2048
+ var record = this.record,
2049
+ clientId = get(record, 'clientId'),
2050
+ store = get(record, 'store');
2051
+
2052
+ if (store) {
2053
+ savedData = store.dataForRecord(record);
2054
+ this._savedData = savedData;
2055
+ return savedData;
2056
+ }
2057
+ },
2058
+
2059
+ unknownProperty: function(key) {
2060
+ var unsavedData = this.unsavedData,
2061
+ associations = this.associations,
2062
+ savedData = this.savedData(),
2063
+ store;
2064
+
2065
+ var value = unsavedData[key], association;
2066
+
2067
+ // if this is a belongsTo association, this will
2068
+ // be a clientId.
2069
+ association = associations[key];
2070
+
2071
+ if (association !== undefined) {
2072
+ store = get(this.record, 'store');
2073
+ return store.clientIdToId[association];
2074
+ }
2075
+
2076
+ if (savedData && value === undefined) {
2077
+ value = savedData[key];
2078
+ }
2079
+
2080
+ return value;
2081
+ },
2082
+
2083
+ setUnknownProperty: function(key, value) {
2084
+ var record = this.record,
2085
+ unsavedData = this.unsavedData;
2086
+
2087
+ unsavedData[key] = value;
2088
+
2089
+ // At the end of the run loop, notify model arrays that
2090
+ // this record has changed so they can re-evaluate its contents
2091
+ // to determine membership.
2092
+ Ember.run.once(record, record.notifyHashWasUpdated);
2093
+
2094
+ return value;
2095
+ },
2096
+
2097
+ commit: function() {
2098
+ var record = this.record;
2099
+
2100
+ var unsavedData = this.unsavedData;
2101
+ var savedData = this.savedData();
2102
+
2103
+ for (var prop in unsavedData) {
2104
+ if (unsavedData.hasOwnProperty(prop)) {
2105
+ savedData[prop] = unsavedData[prop];
2106
+ delete unsavedData[prop];
2107
+ }
2108
+ }
2109
+
2110
+ record.notifyPropertyChange('data');
2111
+ },
2112
+
2113
+ rollback: function() {
2114
+ this.unsavedData = {};
2115
+ },
2116
+
2117
+ adapterDidUpdate: function(data) {
2118
+ this.unsavedData = {};
2119
+ }
2120
+ };
2121
+
1480
2122
  DS.Model = Ember.Object.extend({
1481
2123
  isLoaded: retrieveFromCurrentState,
1482
2124
  isDirty: retrieveFromCurrentState,
@@ -1484,207 +2126,379 @@ DS.Model = Ember.Object.extend({
1484
2126
  isDeleted: retrieveFromCurrentState,
1485
2127
  isError: retrieveFromCurrentState,
1486
2128
  isNew: retrieveFromCurrentState,
2129
+ isPending: retrieveFromCurrentState,
1487
2130
  isValid: retrieveFromCurrentState,
1488
2131
 
1489
2132
  clientId: null,
2133
+ transaction: null,
2134
+ stateManager: null,
2135
+ pendingQueue: null,
2136
+ errors: null,
1490
2137
 
1491
2138
  // because unknownProperty is used, any internal property
1492
2139
  // must be initialized here.
1493
2140
  primaryKey: 'id',
1494
- data: null,
1495
- transaction: null,
2141
+ id: Ember.computed(function(key, value) {
2142
+ var primaryKey = get(this, 'primaryKey'),
2143
+ data = get(this, 'data');
1496
2144
 
1497
- didLoad: Ember.K,
1498
- didUpdate: Ember.K,
1499
- didCreate: Ember.K,
2145
+ if (arguments.length === 2) {
2146
+ set(data, primaryKey, value);
2147
+ return value;
2148
+ }
1500
2149
 
1501
- init: function() {
1502
- var stateManager = DS.StateManager.create({
1503
- model: this
1504
- });
2150
+ return data && get(data, primaryKey);
2151
+ }).property('primaryKey', 'data'),
1505
2152
 
1506
- set(this, 'stateManager', stateManager);
1507
- stateManager.goToState('empty');
2153
+ // The following methods are callbacks invoked by `getJSON`. You
2154
+ // can override one of the callbacks to override specific behavior,
2155
+ // or getJSON itself.
2156
+ //
2157
+ // If you override getJSON, you can invoke these callbacks manually
2158
+ // to get the default behavior.
2159
+
2160
+ /**
2161
+ Add the record's primary key to the JSON hash.
2162
+
2163
+ The default implementation uses the record's specified `primaryKey`
2164
+ and the `id` computed property, which are passed in as parameters.
2165
+
2166
+ @param {Object} json the JSON hash being built
2167
+ @param {Number|String} id the record's id
2168
+ @param {String} key the primaryKey for the record
2169
+ */
2170
+ addIdToJSON: function(json, id, key) {
2171
+ if (id) { json[key] = id; }
1508
2172
  },
1509
2173
 
1510
- withTransaction: function(fn) {
1511
- var transaction = get(this, 'transaction') || getPath(this, 'store.defaultTransaction');
2174
+ /**
2175
+ Add the attributes' current values to the JSON hash.
1512
2176
 
1513
- if (transaction) { fn(transaction); }
2177
+ The default implementation gets the current value of each
2178
+ attribute from the `data`, and uses a `defaultValue` if
2179
+ specified in the `DS.attr` definition.
2180
+
2181
+ @param {Object} json the JSON hash being build
2182
+ @param {Ember.Map} attributes a Map of attributes
2183
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2184
+ */
2185
+ addAttributesToJSON: function(json, attributes, data) {
2186
+ attributes.forEach(function(name, meta) {
2187
+ var key = meta.key(this.constructor),
2188
+ value = get(data, key);
2189
+
2190
+ if (value === undefined) {
2191
+ value = meta.options.defaultValue;
2192
+ }
2193
+
2194
+ json[key] = value;
2195
+ }, this);
1514
2196
  },
1515
2197
 
1516
- setData: function(data) {
1517
- var stateManager = get(this, 'stateManager');
1518
- stateManager.send('setData', data);
2198
+ /**
2199
+ Add the value of a `hasMany` association to the JSON hash.
2200
+
2201
+ The default implementation honors the `embedded` option
2202
+ passed to `DS.hasMany`. If embedded, `toJSON` is recursively
2203
+ called on the child records. If not, the `id` of each
2204
+ record is added.
2205
+
2206
+ Note that if a record is not embedded and does not
2207
+ yet have an `id` (usually provided by the server), it
2208
+ will not be included in the output.
2209
+
2210
+ @param {Object} json the JSON hash being built
2211
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2212
+ @param {Object} meta information about the association
2213
+ @param {Object} options options passed to `toJSON`
2214
+ */
2215
+ addHasManyToJSON: function(json, data, meta, options) {
2216
+ var key = meta.key,
2217
+ manyArray = get(this, key),
2218
+ records = [],
2219
+ clientId, id;
2220
+
2221
+ if (meta.options.embedded) {
2222
+ // TODO: Avoid materializing embedded hashes if possible
2223
+ manyArray.forEach(function(record) {
2224
+ records.push(record.toJSON(options));
2225
+ });
2226
+ } else {
2227
+ var clientIds = get(manyArray, 'content');
2228
+
2229
+ for (var i=0, l=clientIds.length; i<l; i++) {
2230
+ clientId = clientIds[i];
2231
+ id = get(this, 'store').clientIdToId[clientId];
2232
+
2233
+ if (id !== undefined) {
2234
+ records.push(id);
2235
+ }
2236
+ }
2237
+ }
2238
+
2239
+ json[key] = records;
1519
2240
  },
1520
2241
 
1521
- setProperty: function(key, value) {
1522
- var stateManager = get(this, 'stateManager');
1523
- stateManager.send('setProperty', { key: key, value: value });
2242
+ /**
2243
+ Add the value of a `belongsTo` association to the JSON hash.
2244
+
2245
+ The default implementation always includes the `id`.
2246
+
2247
+ @param {Object} json the JSON hash being built
2248
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2249
+ @param {Object} meta information about the association
2250
+ @param {Object} options options passed to `toJSON`
2251
+ */
2252
+ addBelongsToToJSON: function(json, data, meta, options) {
2253
+ var key = meta.key, id;
2254
+
2255
+ if (id = data.get(key)) {
2256
+ json[key] = id;
2257
+ }
1524
2258
  },
1525
2259
 
1526
- deleteRecord: function() {
1527
- var stateManager = get(this, 'stateManager');
1528
- stateManager.send('delete');
2260
+ /**
2261
+ Create a JSON representation of the record, including its `id`,
2262
+ attributes and associations. Honor any settings defined on the
2263
+ attributes or associations (such as `embedded` or `key`).
2264
+ */
2265
+ toJSON: function(options) {
2266
+ var data = get(this, 'data'),
2267
+ result = {},
2268
+ type = this.constructor,
2269
+ attributes = get(type, 'attributes'),
2270
+ primaryKey = get(this, 'primaryKey'),
2271
+ id = get(this, 'id'),
2272
+ store = get(this, 'store'),
2273
+ associations;
2274
+
2275
+ options = options || {};
2276
+
2277
+ // delegate to `addIdToJSON` callback
2278
+ this.addIdToJSON(result, id, primaryKey);
2279
+
2280
+ // delegate to `addAttributesToJSON` callback
2281
+ this.addAttributesToJSON(result, attributes, data);
2282
+
2283
+ associations = get(type, 'associationsByName');
2284
+
2285
+ // add associations, delegating to `addHasManyToJSON` and
2286
+ // `addBelongsToToJSON`.
2287
+ associations.forEach(function(key, meta) {
2288
+ if (options.associations && meta.kind === 'hasMany') {
2289
+ this.addHasManyToJSON(result, data, meta, options);
2290
+ } else if (meta.kind === 'belongsTo') {
2291
+ this.addBelongsToToJSON(result, data, meta, options);
2292
+ }
2293
+ }, this);
2294
+
2295
+ return result;
1529
2296
  },
1530
2297
 
1531
- destroy: function() {
1532
- this.deleteRecord();
1533
- this._super();
2298
+ data: Ember.computed(function() {
2299
+ return new DataProxy(this);
2300
+ }).cacheable(),
2301
+
2302
+ didLoad: Ember.K,
2303
+ didUpdate: Ember.K,
2304
+ didCreate: Ember.K,
2305
+
2306
+ init: function() {
2307
+ var stateManager = DS.StateManager.create({
2308
+ model: this
2309
+ });
2310
+
2311
+ set(this, 'pendingQueue', {});
2312
+
2313
+ set(this, 'stateManager', stateManager);
2314
+ stateManager.goToState('empty');
1534
2315
  },
1535
2316
 
1536
- loadingData: function() {
1537
- var stateManager = get(this, 'stateManager');
1538
- stateManager.send('loadingData');
2317
+ destroy: function() {
2318
+ if (!get(this, 'isDeleted')) {
2319
+ this.deleteRecord();
2320
+ }
2321
+ this._super();
1539
2322
  },
1540
2323
 
1541
- willLoadData: function() {
1542
- var stateManager = get(this, 'stateManager');
1543
- stateManager.send('willLoadData');
2324
+ send: function(name, context) {
2325
+ return get(this, 'stateManager').send(name, context);
1544
2326
  },
1545
2327
 
1546
- willCommit: function() {
1547
- var stateManager = get(this, 'stateManager');
1548
- stateManager.send('willCommit');
2328
+ withTransaction: function(fn) {
2329
+ var transaction = get(this, 'transaction');
2330
+ if (transaction) { fn(transaction); }
1549
2331
  },
1550
2332
 
1551
- adapterDidUpdate: function() {
1552
- var stateManager = get(this, 'stateManager');
1553
- stateManager.send('didUpdate');
2333
+ setProperty: function(key, value) {
2334
+ this.send('setProperty', { key: key, value: value });
1554
2335
  },
1555
2336
 
1556
- adapterDidCreate: function() {
1557
- var stateManager = get(this, 'stateManager');
1558
- stateManager.send('didCreate');
2337
+ deleteRecord: function() {
2338
+ this.send('deleteRecord');
1559
2339
  },
1560
2340
 
1561
- adapterDidDelete: function() {
1562
- var stateManager = get(this, 'stateManager');
1563
- stateManager.send('didDelete');
2341
+ waitingOn: function(record) {
2342
+ this.send('waitingOn', record);
1564
2343
  },
1565
2344
 
1566
- wasInvalid: function(errors) {
1567
- var stateManager = get(this, 'stateManager');
1568
- stateManager.send('wasInvalid', errors);
2345
+ notifyHashWasUpdated: function() {
2346
+ var store = get(this, 'store');
2347
+ if (store) {
2348
+ store.hashWasUpdated(this.constructor, get(this, 'clientId'), this);
2349
+ }
1569
2350
  },
1570
2351
 
1571
2352
  unknownProperty: function(key) {
1572
2353
  var data = get(this, 'data');
1573
2354
 
1574
- if (data) {
1575
- return get(data, key);
2355
+ if (data && key in data) {
2356
+ ember_assert("You attempted to access the " + key + " property on a model without defining an attribute.", false);
1576
2357
  }
1577
2358
  },
1578
2359
 
1579
2360
  setUnknownProperty: function(key, value) {
1580
2361
  var data = get(this, 'data');
1581
- ember_assert("You cannot set a model attribute before its data is loaded.", !!data);
1582
2362
 
1583
- this.setProperty(key, value);
1584
- return value;
2363
+ if (data && key in data) {
2364
+ ember_assert("You attempted to set the " + key + " property on a model without defining an attribute.", false);
2365
+ } else {
2366
+ return this._super(key, value);
2367
+ }
2368
+ },
2369
+
2370
+ namingConvention: {
2371
+ keyToJSONKey: function(key) {
2372
+ // TODO: Strip off `is` from the front. Example: `isHipster` becomes `hipster`
2373
+ return Ember.String.decamelize(key);
2374
+ },
2375
+
2376
+ foreignKey: function(key) {
2377
+ return key + '_id';
2378
+ }
1585
2379
  }
1586
2380
  });
1587
2381
 
2382
+ // Helper function to generate store aliases.
2383
+ // This returns a function that invokes the named alias
2384
+ // on the default store, but injects the class as the
2385
+ // first parameter.
2386
+ var storeAlias = function(methodName) {
2387
+ return function() {
2388
+ var store = get(DS, 'defaultStore'),
2389
+ args = [].slice.call(arguments);
2390
+
2391
+ args.unshift(this);
2392
+ return store[methodName].apply(store, args);
2393
+ };
2394
+ };
2395
+
1588
2396
  DS.Model.reopenClass({
1589
- typeForAssociation: function(association) {
1590
- var type = this.metaForProperty(association).type;
1591
- if (typeof type === 'string') {
1592
- type = getPath(this, type, false) || getPath(window, type);
1593
- }
1594
- return type;
1595
- }
2397
+ find: storeAlias('find'),
2398
+ filter: storeAlias('filter'),
2399
+
2400
+ _create: DS.Model.create,
2401
+
2402
+ create: function() {
2403
+ throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set.");
2404
+ },
2405
+
2406
+ createRecord: storeAlias('createRecord')
1596
2407
  });
1597
2408
 
1598
- DS.attr = function(type, options) {
1599
- var transform = DS.attr.transforms[type];
1600
- var transformFrom = transform.from;
1601
- var transformTo = transform.to;
2409
+ })({});
1602
2410
 
1603
- return Ember.computed(function(key, value) {
1604
- var data = get(this, 'data');
1605
2411
 
1606
- key = (options && options.key) ? options.key : key;
2412
+ (function(exports) {
2413
+ var get = Ember.get, getPath = Ember.getPath;
2414
+ DS.Model.reopenClass({
2415
+ attributes: Ember.computed(function() {
2416
+ var map = Ember.Map.create();
1607
2417
 
1608
- if (value === undefined) {
1609
- if (!data) { return; }
2418
+ this.eachComputedProperty(function(name, meta) {
2419
+ if (meta.isAttribute) { map.set(name, meta); }
2420
+ });
1610
2421
 
1611
- return transformFrom(data[key]);
1612
- } else {
1613
- ember_assert("You cannot set a model attribute before its data is loaded.", !!data);
2422
+ return map;
2423
+ }).cacheable(),
1614
2424
 
1615
- value = transformTo(value);
1616
- this.setProperty(key, value);
1617
- return value;
1618
- }
1619
- }).property('data');
1620
- };
2425
+ processAttributeKeys: function() {
2426
+ if (this.processedAttributeKeys) { return; }
1621
2427
 
1622
- var embeddedFindRecord = function(store, type, data, key, one) {
1623
- var association = data ? get(data, key) : one ? null : [];
1624
- if (one) {
1625
- return association ? store.load(type, association).id : null;
1626
- } else {
1627
- return association ? store.loadMany(type, association).ids : [];
2428
+ var namingConvention = getPath(this, 'proto.namingConvention');
2429
+
2430
+ this.eachComputedProperty(function(name, meta) {
2431
+ if (meta.isAttribute && !meta.options.key) {
2432
+ meta.options.key = namingConvention.keyToJSONKey(name, this);
2433
+ }
2434
+ }, this);
1628
2435
  }
1629
- };
2436
+ });
1630
2437
 
1631
- var referencedFindRecord = function(store, type, data, key, one) {
1632
- return data ? get(data, key) : one ? null : [];
1633
- };
2438
+ DS.attr = function(type, options) {
2439
+ var transform = DS.attr.transforms[type];
2440
+ ember_assert("Could not find model attribute of type " + type, !!transform);
1634
2441
 
1635
- var hasAssociation = function(type, options, one) {
1636
- var embedded = options && options.embedded,
1637
- findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2442
+ var transformFrom = transform.from;
2443
+ var transformTo = transform.to;
1638
2444
 
1639
- return Ember.computed(function(key) {
1640
- var data = get(this, 'data'), ids, id, association,
1641
- store = get(this, 'store');
2445
+ options = options || {};
1642
2446
 
1643
- if (typeof type === 'string') {
1644
- type = getPath(this, type, false) || getPath(window, type);
1645
- }
2447
+ var meta = {
2448
+ type: type,
2449
+ isAttribute: true,
2450
+ options: options,
1646
2451
 
1647
- key = (options && options.key) ? options.key : key;
1648
- if (one) {
1649
- id = findRecord(store, type, data, key, true);
1650
- association = id ? store.find(type, id) : null;
1651
- } else {
1652
- ids = findRecord(store, type, data, key);
1653
- association = store.findMany(type, ids);
2452
+ // this will ensure that the key always takes naming
2453
+ // conventions into consideration.
2454
+ key: function(recordType) {
2455
+ recordType.processAttributeKeys();
2456
+ return options.key;
1654
2457
  }
2458
+ };
1655
2459
 
1656
- return association;
1657
- }).property('data').cacheable().meta({ type: type });
1658
- };
2460
+ return Ember.computed(function(key, value) {
2461
+ var data;
1659
2462
 
1660
- DS.hasMany = function(type, options) {
1661
- ember_assert("The type passed to DS.hasMany must be defined", !!type);
1662
- return hasAssociation(type, options);
1663
- };
2463
+ key = meta.key(this.constructor);
1664
2464
 
1665
- DS.hasOne = function(type, options) {
1666
- ember_assert("The type passed to DS.hasOne must be defined", !!type);
1667
- return hasAssociation(type, options, true);
2465
+ if (arguments.length === 2) {
2466
+ value = transformTo(value);
2467
+ this.setProperty(key, value);
2468
+ } else {
2469
+ data = get(this, 'data');
2470
+ value = get(data, key);
2471
+
2472
+ if (value === undefined) {
2473
+ value = options.defaultValue;
2474
+ }
2475
+ }
2476
+
2477
+ return transformFrom(value);
2478
+ // `data` is never set directly. However, it may be
2479
+ // invalidated from the state manager's setData
2480
+ // event.
2481
+ }).property('data').cacheable().meta(meta);
1668
2482
  };
1669
2483
 
1670
2484
  DS.attr.transforms = {
1671
2485
  string: {
1672
2486
  from: function(serialized) {
1673
- return Em.none(serialized) ? null : String(serialized);
2487
+ return Ember.none(serialized) ? null : String(serialized);
1674
2488
  },
1675
2489
 
1676
2490
  to: function(deserialized) {
1677
- return Em.none(deserialized) ? null : String(deserialized);
2491
+ return Ember.none(deserialized) ? null : String(deserialized);
1678
2492
  }
1679
2493
  },
1680
2494
 
1681
- integer: {
2495
+ number: {
1682
2496
  from: function(serialized) {
1683
- return Em.none(serialized) ? null : Number(serialized);
2497
+ return Ember.none(serialized) ? null : Number(serialized);
1684
2498
  },
1685
2499
 
1686
2500
  to: function(deserialized) {
1687
- return Em.none(deserialized) ? null : Number(deserialized);
2501
+ return Ember.none(deserialized) ? null : Number(deserialized);
1688
2502
  }
1689
2503
  },
1690
2504
 
@@ -1746,6 +2560,135 @@ DS.attr.transforms = {
1746
2560
  }
1747
2561
  };
1748
2562
 
2563
+
2564
+ })({});
2565
+
2566
+
2567
+ (function(exports) {
2568
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
2569
+ DS.Model.reopenClass({
2570
+ typeForAssociation: function(name) {
2571
+ var association = get(this, 'associationsByName').get(name);
2572
+ return association && association.type;
2573
+ },
2574
+
2575
+ associations: Ember.computed(function() {
2576
+ var map = Ember.Map.create();
2577
+
2578
+ this.eachComputedProperty(function(name, meta) {
2579
+ if (meta.isAssociation) {
2580
+ var type = meta.type,
2581
+ typeList = map.get(type);
2582
+
2583
+ if (typeof type === 'string') {
2584
+ type = getPath(this, type, false) || getPath(window, type);
2585
+ meta.type = type;
2586
+ }
2587
+
2588
+ if (!typeList) {
2589
+ typeList = [];
2590
+ map.set(type, typeList);
2591
+ }
2592
+
2593
+ typeList.push({ name: name, kind: meta.kind });
2594
+ }
2595
+ });
2596
+
2597
+ return map;
2598
+ }).cacheable(),
2599
+
2600
+ associationsByName: Ember.computed(function() {
2601
+ var map = Ember.Map.create(), type;
2602
+
2603
+ this.eachComputedProperty(function(name, meta) {
2604
+ if (meta.isAssociation) {
2605
+ meta.key = name;
2606
+ type = meta.type;
2607
+
2608
+ if (typeof type === 'string') {
2609
+ type = getPath(this, type, false) || getPath(window, type);
2610
+ meta.type = type;
2611
+ }
2612
+
2613
+ map.set(name, meta);
2614
+ }
2615
+ });
2616
+
2617
+ return map;
2618
+ }).cacheable()
2619
+ });
2620
+
2621
+
2622
+ var embeddedFindRecord = function(store, type, data, key, one) {
2623
+ var association = data ? get(data, key) : one ? null : [];
2624
+ if (one) {
2625
+ return association ? store.load(type, association).id : null;
2626
+ } else {
2627
+ return association ? store.loadMany(type, association).ids : [];
2628
+ }
2629
+ };
2630
+
2631
+ var referencedFindRecord = function(store, type, data, key, one) {
2632
+ return data ? get(data, key) : one ? null : [];
2633
+ };
2634
+
2635
+ var hasAssociation = function(type, options, one) {
2636
+ var embedded = options && options.embedded,
2637
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
2638
+
2639
+ var meta = { type: type, isAssociation: true, options: options || {} };
2640
+ if (one) {
2641
+ meta.kind = 'belongsTo';
2642
+ } else {
2643
+ meta.kind = 'hasMany';
2644
+ }
2645
+
2646
+ return Ember.computed(function(key, value) {
2647
+ var data = get(this, 'data'), ids, id, association,
2648
+ store = get(this, 'store');
2649
+
2650
+ if (typeof type === 'string') {
2651
+ type = getPath(this, type, false) || getPath(window, type);
2652
+ }
2653
+
2654
+ key = (options && options.key) ? options.key : key;
2655
+ if (one) {
2656
+ if (arguments.length === 2) {
2657
+ data.setAssociation(key, get(value, 'clientId'));
2658
+ // put the client id in `key` in the data hash
2659
+ return value;
2660
+ } else {
2661
+ id = findRecord(store, type, data, key, true);
2662
+ association = id ? store.find(type, id) : null;
2663
+
2664
+ // if we have an association, store its client id in `key` in the data hash
2665
+ }
2666
+ } else {
2667
+ ids = findRecord(store, type, data, key);
2668
+ association = store.findMany(type, ids);
2669
+ set(association, 'parentRecord', this);
2670
+ }
2671
+
2672
+ return association;
2673
+ }).property('data').cacheable().meta(meta);
2674
+ };
2675
+
2676
+ DS.hasMany = function(type, options) {
2677
+ ember_assert("The type passed to DS.hasMany must be defined", !!type);
2678
+ return hasAssociation(type, options);
2679
+ };
2680
+
2681
+ DS.hasOne = function(type, options) {
2682
+ ember_assert("The type passed to DS.belongsTo must be defined", !!type);
2683
+ return hasAssociation(type, options, true);
2684
+ };
2685
+
2686
+ DS.belongsTo = DS.hasOne;
2687
+
2688
+ })({});
2689
+
2690
+
2691
+ (function(exports) {
1749
2692
  })({});
1750
2693
 
1751
2694