rightnow_oms 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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