emberjs-couchapp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +36 -0
  4. data/LICENSE +13 -0
  5. data/README.md +52 -0
  6. data/Rakefile +17 -0
  7. data/bin/emberjs-couchapp +5 -0
  8. data/emberjs-couchapp.gemspec +23 -0
  9. data/lib/emberjs-couchapp.rb +5 -0
  10. data/lib/emberjs-couchapp/app_generator.rb +25 -0
  11. data/lib/emberjs-couchapp/cli.rb +13 -0
  12. data/lib/emberjs-couchapp/templates/app/.couchappignore +7 -0
  13. data/lib/emberjs-couchapp/templates/app/.couchapprc.tt +10 -0
  14. data/lib/emberjs-couchapp/templates/app/.gitignore +3 -0
  15. data/lib/emberjs-couchapp/templates/app/Assetfile.tt +136 -0
  16. data/lib/emberjs-couchapp/templates/app/Gemfile +19 -0
  17. data/lib/emberjs-couchapp/templates/app/Guardfile +3 -0
  18. data/lib/emberjs-couchapp/templates/app/LICENSE +13 -0
  19. data/lib/emberjs-couchapp/templates/app/README.md +58 -0
  20. data/lib/emberjs-couchapp/templates/app/Rakefile.tt +34 -0
  21. data/lib/emberjs-couchapp/templates/app/_id.tt +1 -0
  22. data/lib/emberjs-couchapp/templates/app/app/css/bootstrap.css +4983 -0
  23. data/lib/emberjs-couchapp/templates/app/app/css/main.css +11 -0
  24. data/lib/emberjs-couchapp/templates/app/app/index.html.tt +24 -0
  25. data/lib/emberjs-couchapp/templates/app/app/lib/controllers/.empty_directory +0 -0
  26. data/lib/emberjs-couchapp/templates/app/app/lib/core.js.tt +11 -0
  27. data/lib/emberjs-couchapp/templates/app/app/lib/ext.js.tt +20 -0
  28. data/lib/emberjs-couchapp/templates/app/app/lib/main.js.tt +3 -0
  29. data/lib/emberjs-couchapp/templates/app/app/lib/models/.empty_directory +0 -0
  30. data/lib/emberjs-couchapp/templates/app/app/lib/state_manager.js.tt +11 -0
  31. data/lib/emberjs-couchapp/templates/app/app/lib/states/start.js.tt +9 -0
  32. data/lib/emberjs-couchapp/templates/app/app/lib/store.js.tt +5 -0
  33. data/lib/emberjs-couchapp/templates/app/app/lib/views/.empty_directory +0 -0
  34. data/lib/emberjs-couchapp/templates/app/app/modules/.empty_directory +0 -0
  35. data/lib/emberjs-couchapp/templates/app/app/plugins/loader.js +60 -0
  36. data/lib/emberjs-couchapp/templates/app/app/static/img/glyphicons-halflings-white.png +0 -0
  37. data/lib/emberjs-couchapp/templates/app/app/static/img/glyphicons-halflings.png +0 -0
  38. data/lib/emberjs-couchapp/templates/app/app/templates/main_page.handlebars.tt +1 -0
  39. data/lib/emberjs-couchapp/templates/app/app/tests/%name%_tests.js.tt +5 -0
  40. data/lib/emberjs-couchapp/templates/app/app/vendor/ember-data.js +3787 -0
  41. data/lib/emberjs-couchapp/templates/app/app/vendor/ember.js +20148 -0
  42. data/lib/emberjs-couchapp/templates/app/app/vendor/jquery.js +9404 -0
  43. data/lib/emberjs-couchapp/templates/app/config.ru.tt +18 -0
  44. data/lib/emberjs-couchapp/templates/app/couchapp.json.tt +4 -0
  45. data/lib/emberjs-couchapp/templates/app/filters/my_filter.js +3 -0
  46. data/lib/emberjs-couchapp/templates/app/language +1 -0
  47. data/lib/emberjs-couchapp/templates/app/lists/my_list.js +3 -0
  48. data/lib/emberjs-couchapp/templates/app/shows/my_show.js +3 -0
  49. data/lib/emberjs-couchapp/templates/app/tests/index.html +42 -0
  50. data/lib/emberjs-couchapp/templates/app/tests/qunit/qunit.css +235 -0
  51. data/lib/emberjs-couchapp/templates/app/tests/qunit/qunit.js +1669 -0
  52. data/lib/emberjs-couchapp/templates/app/tests/run-tests.js +93 -0
  53. data/lib/emberjs-couchapp/templates/app/views/my_view/map.js +3 -0
  54. data/lib/emberjs-couchapp/templates/app/views/my_view/reduce.js +3 -0
  55. data/lib/emberjs-couchapp/version.rb +3 -0
  56. data/spec/ember/cli_spec.rb +128 -0
  57. data/spec/spec_helper.rb +44 -0
  58. metadata +175 -0
@@ -0,0 +1,11 @@
1
+ body {
2
+ padding-top: 60px;
3
+ }
4
+
5
+ a {
6
+ cursor: pointer;
7
+ }
8
+
9
+ .navbar .brand {
10
+ color: #fff;
11
+ }
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title><%= title %></title>
6
+ <link rel="stylesheet" href="app.css" media="screen" />
7
+ </head>
8
+ <body>
9
+ <div id="main" class="container">
10
+ <nav class="navbar navbar-fixed-top">
11
+ <div class="navbar-inner">
12
+ <div class="container">
13
+ <a class="brand" href="#"><%= title %></a>
14
+ </div>
15
+ </div>
16
+ </nav>
17
+ </div>
18
+ <script src="loader.js"></script>
19
+ <script src="app.js"></script>
20
+ <script>
21
+ loader.require('<%= name %>/main');
22
+ </script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,11 @@
1
+ require('jquery');
2
+ require('ember');
3
+ require('ember-data');
4
+ require('<%= name %>/ext');
5
+
6
+ Ember.ENV.CP_DEFAULT_CACHEABLE = true;
7
+ Ember.ENV.VIEW_PRESERVES_CONTEXT = true;
8
+
9
+ App = Ember.Application.create({
10
+ VERSION: '0.1'
11
+ });
@@ -0,0 +1,20 @@
1
+ var get = Ember.get, fmt = Ember.String.fmt;
2
+
3
+ Ember.View.reopen({
4
+ templateForName: function(name, type) {
5
+ if (!name) { return; }
6
+
7
+ var templates = get(this, 'templates'),
8
+ template = get(templates, name);
9
+
10
+ if (!template) {
11
+ try {
12
+ template = require(name);
13
+ } catch (e) {
14
+ throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name]));
15
+ }
16
+ }
17
+
18
+ return template;
19
+ }
20
+ });
@@ -0,0 +1,3 @@
1
+ require('<%= name %>/core');
2
+ require('<%= name %>/store');
3
+ require('<%= name %>/state_manager');
@@ -0,0 +1,11 @@
1
+ require('<%= name %>/core');
2
+ require('<%= name %>/states/start');
3
+
4
+ App.stateManager = Ember.StateManager.create({
5
+
6
+ rootElement: '#main',
7
+ initialState: 'start',
8
+
9
+ start: App.StartState
10
+
11
+ });
@@ -0,0 +1,9 @@
1
+ require('<%= name %>/core');
2
+
3
+ App.StartState = Ember.ViewState.extend({
4
+
5
+ view: Ember.View.extend({
6
+ templateName: '<%= name %>/~templates/main_page'
7
+ })
8
+
9
+ });
@@ -0,0 +1,5 @@
1
+ require('<%= name %>/core');
2
+
3
+ App.store = DS.Store.create({
4
+ revision: 4
5
+ });
@@ -0,0 +1,60 @@
1
+ (function(window) {
2
+ function requireWrapper(self) {
3
+ var require = function() {
4
+ return self.require.apply(self, arguments);
5
+ };
6
+ require.exists = function() {
7
+ return self.exists.apply(self, arguments);
8
+ };
9
+ return require;
10
+ }
11
+
12
+ var Loader = function() {
13
+ this.modules = {};
14
+ this.loaded = {};
15
+ this.exports = {};
16
+ return this;
17
+ };
18
+
19
+ Loader.prototype.require = function(name) {
20
+ if (!this.loaded[name]) {
21
+ var module = this.modules[name];
22
+ if (module) {
23
+ var require = requireWrapper(this);
24
+ try {
25
+ this.exports[name] = module.call(window, require);
26
+ return this.exports[name];
27
+ } finally {
28
+ this.loaded[name] = true;
29
+ }
30
+ } else {
31
+ throw "The module '" + name + "' has not been registered";
32
+ }
33
+ }
34
+ return this.exports[name];
35
+ };
36
+
37
+ Loader.prototype.register = function(name, module) {
38
+ if (this.exists(name)) {
39
+ throw "The module '" + name + "' has already been registered";
40
+ }
41
+ this.modules[name] = module;
42
+ return true;
43
+ };
44
+
45
+ Loader.prototype.unregister = function(name) {
46
+ var loaded = !!this.loaded[name];
47
+ if (loaded) {
48
+ delete this.exports[name];
49
+ delete this.modules[name];
50
+ delete this.loaded[name];
51
+ }
52
+ return loaded;
53
+ };
54
+
55
+ Loader.prototype.exists = function(name) {
56
+ return name in this.modules;
57
+ };
58
+
59
+ window.loader = new Loader();
60
+ })(this);
@@ -0,0 +1 @@
1
+ <h1><%= name %> v{{App.VERSION}}</h1>
@@ -0,0 +1,5 @@
1
+ module("<%= title %>");
2
+
3
+ test("App is defined", function () {
4
+ ok(typeof App !== 'undefined', "App is undefined");
5
+ });
@@ -0,0 +1,3787 @@
1
+ (function() {
2
+ window.DS = Ember.Namespace.create({
3
+ CURRENT_API_REVISION: 4
4
+ });
5
+
6
+ })();
7
+
8
+
9
+
10
+ (function() {
11
+ var get = Ember.get, set = Ember.set;
12
+
13
+ /**
14
+ A record array is an array that contains records of a certain type. The record
15
+ array materializes records as needed when they are retrieved for the first
16
+ time. You should not create record arrays yourself. Instead, an instance of
17
+ DS.RecordArray or its subclasses will be returned by your application's store
18
+ in response to queries.
19
+ */
20
+
21
+ DS.RecordArray = Ember.ArrayProxy.extend({
22
+
23
+ /**
24
+ The model type contained by this record array.
25
+
26
+ @type DS.Model
27
+ */
28
+ type: null,
29
+
30
+ // The array of client ids backing the record array. When a
31
+ // record is requested from the record array, the record
32
+ // for the client id at the same index is materialized, if
33
+ // necessary, by the store.
34
+ content: null,
35
+
36
+ // The store that created this record array.
37
+ store: null,
38
+
39
+ init: function() {
40
+ this.contentWillChange();
41
+ this._super();
42
+ },
43
+
44
+ contentWillChange: Ember.beforeObserver(function() {
45
+ set(this, 'recordCache', []);
46
+ }, 'content'),
47
+
48
+ contentArrayDidChange: function(array, index, removed, added) {
49
+ var recordCache = get(this, 'recordCache');
50
+ var args = [index, 0].concat(new Array(added));
51
+
52
+ recordCache.splice.apply(recordCache, args);
53
+ },
54
+
55
+ contentArrayWillChange: function(array, index, removed, added) {
56
+ var recordCache = get(this, 'recordCache');
57
+ recordCache.splice(index, removed);
58
+ },
59
+
60
+ objectAtContent: function(index) {
61
+ var recordCache = get(this, 'recordCache');
62
+ var record = recordCache[index];
63
+
64
+ if (!record) {
65
+ var store = get(this, 'store');
66
+ var content = get(this, 'content');
67
+
68
+ var contentObject = content.objectAt(index);
69
+
70
+ if (contentObject !== undefined) {
71
+ record = store.findByClientId(get(this, 'type'), contentObject);
72
+ recordCache[index] = record;
73
+ }
74
+ }
75
+
76
+ return record;
77
+ }
78
+ });
79
+
80
+ })();
81
+
82
+
83
+
84
+ (function() {
85
+ var get = Ember.get;
86
+
87
+ DS.FilteredRecordArray = DS.RecordArray.extend({
88
+ filterFunction: null,
89
+
90
+ replace: function() {
91
+ var type = get(this, 'type').toString();
92
+ throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
93
+ },
94
+
95
+ updateFilter: Ember.observer(function() {
96
+ var store = get(this, 'store');
97
+ store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
98
+ }, 'filterFunction')
99
+ });
100
+
101
+ })();
102
+
103
+
104
+
105
+ (function() {
106
+ var get = Ember.get, set = Ember.set;
107
+
108
+ DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
109
+ query: null,
110
+ isLoaded: false,
111
+
112
+ replace: function() {
113
+ var type = get(this, 'type').toString();
114
+ throw new Error("The result of a server query (on " + type + ") is immutable.");
115
+ },
116
+
117
+ load: function(array) {
118
+ var store = get(this, 'store'), type = get(this, 'type');
119
+
120
+ var clientIds = store.loadMany(type, array).clientIds;
121
+
122
+ this.beginPropertyChanges();
123
+ set(this, 'content', Ember.A(clientIds));
124
+ set(this, 'isLoaded', true);
125
+ this.endPropertyChanges();
126
+ }
127
+ });
128
+
129
+
130
+ })();
131
+
132
+
133
+
134
+ (function() {
135
+ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
136
+
137
+ var Set = function() {
138
+ this.hash = {};
139
+ this.list = [];
140
+ };
141
+
142
+ Set.prototype = {
143
+ add: function(item) {
144
+ var hash = this.hash,
145
+ guid = guidFor(item);
146
+
147
+ if (hash.hasOwnProperty(guid)) { return; }
148
+
149
+ hash[guid] = true;
150
+ this.list.push(item);
151
+ },
152
+
153
+ remove: function(item) {
154
+ var hash = this.hash,
155
+ guid = guidFor(item);
156
+
157
+ if (!hash.hasOwnProperty(guid)) { return; }
158
+
159
+ delete hash[guid];
160
+ var list = this.list,
161
+ index = Ember.ArrayUtils.indexOf(this, item);
162
+
163
+ list.splice(index, 1);
164
+ },
165
+
166
+ isEmpty: function() {
167
+ return this.list.length === 0;
168
+ }
169
+ };
170
+
171
+ var ManyArrayState = Ember.State.extend({
172
+ recordWasAdded: function(manager, record) {
173
+ var dirty = manager.dirty, observer;
174
+ dirty.add(record);
175
+
176
+ observer = function() {
177
+ if (!get(record, 'isDirty')) {
178
+ record.removeObserver('isDirty', observer);
179
+ manager.send('childWasSaved', record);
180
+ }
181
+ };
182
+
183
+ record.addObserver('isDirty', observer);
184
+ },
185
+
186
+ recordWasRemoved: function(manager, record) {
187
+ var dirty = manager.dirty, observer;
188
+ dirty.add(record);
189
+
190
+ observer = function() {
191
+ record.removeObserver('isDirty', observer);
192
+ if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); }
193
+ };
194
+
195
+ record.addObserver('isDirty', observer);
196
+ }
197
+ });
198
+
199
+ var states = {
200
+ clean: ManyArrayState.create({
201
+ isDirty: false,
202
+
203
+ recordWasAdded: function(manager, record) {
204
+ this._super(manager, record);
205
+ manager.goToState('dirty');
206
+ },
207
+
208
+ update: function(manager, clientIds) {
209
+ var manyArray = manager.manyArray;
210
+ set(manyArray, 'content', clientIds);
211
+ }
212
+ }),
213
+
214
+ dirty: ManyArrayState.create({
215
+ isDirty: true,
216
+
217
+ childWasSaved: function(manager, child) {
218
+ var dirty = manager.dirty;
219
+ dirty.remove(child);
220
+
221
+ if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); }
222
+ },
223
+
224
+ arrayBecameSaved: function(manager) {
225
+ manager.goToState('clean');
226
+ }
227
+ })
228
+ };
229
+
230
+ DS.ManyArrayStateManager = Ember.StateManager.extend({
231
+ manyArray: null,
232
+ initialState: 'clean',
233
+ states: states,
234
+
235
+ init: function() {
236
+ this._super();
237
+ this.dirty = new Set();
238
+ }
239
+ });
240
+
241
+ })();
242
+
243
+
244
+
245
+ (function() {
246
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
247
+
248
+ DS.ManyArray = DS.RecordArray.extend({
249
+ init: function() {
250
+ set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this }));
251
+
252
+ return this._super();
253
+ },
254
+
255
+ parentRecord: null,
256
+
257
+ isDirty: Ember.computed(function() {
258
+ return getPath(this, 'stateManager.currentState.isDirty');
259
+ }).property('stateManager.currentState').cacheable(),
260
+
261
+ fetch: function() {
262
+ var clientIds = get(this, 'content'),
263
+ store = get(this, 'store'),
264
+ type = get(this, 'type');
265
+
266
+ var ids = clientIds.map(function(clientId) {
267
+ return store.clientIdToId[clientId];
268
+ });
269
+
270
+ store.fetchMany(type, ids);
271
+ },
272
+
273
+ // Overrides Ember.Array's replace method to implement
274
+ replace: function(index, removed, added) {
275
+ var parentRecord = get(this, 'parentRecord');
276
+ var pendingParent = parentRecord && !get(parentRecord, 'id');
277
+ var stateManager = get(this, 'stateManager');
278
+
279
+ // Map the array of record objects into an array of client ids.
280
+ added = added.map(function(record) {
281
+ 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));
282
+
283
+ // If the record to which this many array belongs does not yet
284
+ // have an id, notify the newly-added record that it must wait
285
+ // for the parent to receive an id before the child can be
286
+ // saved.
287
+ if (pendingParent) {
288
+ record.send('waitingOn', parentRecord);
289
+ }
290
+
291
+ this.assignInverse(record, parentRecord);
292
+
293
+ stateManager.send('recordWasAdded', record);
294
+
295
+ return record.get('clientId');
296
+ }, this);
297
+
298
+ var store = this.store;
299
+
300
+ var len = index+removed, record;
301
+ for (var i = index; i < len; i++) {
302
+ // TODO: null out inverse FK
303
+ record = this.objectAt(i);
304
+ this.assignInverse(record, parentRecord, true);
305
+
306
+ // If we put the child record into a pending state because
307
+ // we were waiting on the parent record to get an id, we
308
+ // can tell the child it no longer needs to wait.
309
+ if (pendingParent) {
310
+ record.send('doneWaitingOn', parentRecord);
311
+ }
312
+
313
+ stateManager.send('recordWasAdded', record);
314
+ }
315
+
316
+ this._super(index, removed, added);
317
+ },
318
+
319
+ assignInverse: function(record, parentRecord, remove) {
320
+ var associationMap = get(record.constructor, 'associations'),
321
+ possibleAssociations = associationMap.get(parentRecord.constructor),
322
+ possible, actual;
323
+
324
+ if (!possibleAssociations) { return; }
325
+
326
+ for (var i = 0, l = possibleAssociations.length; i < l; i++) {
327
+ possible = possibleAssociations[i];
328
+
329
+ if (possible.kind === 'belongsTo') {
330
+ actual = possible;
331
+ break;
332
+ }
333
+ }
334
+
335
+ if (actual) {
336
+ set(record, actual.name, remove ? null : parentRecord);
337
+ }
338
+ },
339
+
340
+ // Create a child record within the parentRecord
341
+ createRecord: function(hash, transaction) {
342
+ var parentRecord = get(this, 'parentRecord'),
343
+ store = get(parentRecord, 'store'),
344
+ type = get(this, 'type'),
345
+ record;
346
+
347
+ transaction = transaction || get(parentRecord, 'transaction');
348
+
349
+ record = store.createRecord.call(store, type, hash, transaction);
350
+ this.pushObject(record);
351
+
352
+ return record;
353
+ }
354
+ });
355
+
356
+ })();
357
+
358
+
359
+
360
+ (function() {
361
+
362
+ })();
363
+
364
+
365
+
366
+ (function() {
367
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
368
+
369
+ /**
370
+ A transaction allows you to collect multiple records into a unit of work
371
+ that can be committed or rolled back as a group.
372
+
373
+ For example, if a record has local modifications that have not yet
374
+ been saved, calling `commit()` on its transaction will cause those
375
+ modifications to be sent to the adapter to be saved. Calling
376
+ `rollback()` on its transaction would cause all of the modifications to
377
+ be discarded and the record to return to the last known state before
378
+ changes were made.
379
+
380
+ If a newly created record's transaction is rolled back, it will
381
+ immediately transition to the deleted state.
382
+
383
+ If you do not explicitly create a transaction, a record is assigned to
384
+ an implicit transaction called the default transaction. In these cases,
385
+ you can treat your application's instance of `DS.Store` as a transaction
386
+ and call the `commit()` and `rollback()` methods on the store itself.
387
+
388
+ Once a record has been successfully committed or rolled back, it will
389
+ be moved back to the implicit transaction. Because it will now be in
390
+ a clean state, it can be moved to a new transaction if you wish.
391
+
392
+ ### Creating a Transaction
393
+
394
+ To create a new transaction, call the `transaction()` method of your
395
+ application's `DS.Store` instance:
396
+
397
+ var transaction = App.store.transaction();
398
+
399
+ This will return a new instance of `DS.Transaction` with no records
400
+ yet assigned to it.
401
+
402
+ ### Adding Existing Records
403
+
404
+ Add records to a transaction using the `add()` method:
405
+
406
+ record = App.store.find(Person, 1);
407
+ transaction.add(record);
408
+
409
+ Note that only records whose `isDirty` flag is `false` may be added
410
+ to a transaction. Once modifications to a record have been made
411
+ (its `isDirty` flag is `true`), it is not longer able to be added to
412
+ a transaction.
413
+
414
+ ### Creating New Records
415
+
416
+ Because newly created records are dirty from the time they are created,
417
+ and because dirty records can not be added to a transaction, you must
418
+ use the `createRecord()` method to assign new records to a transaction.
419
+
420
+ For example, instead of this:
421
+
422
+ var transaction = store.transaction();
423
+ var person = Person.createRecord({ name: "Steve" });
424
+
425
+ // won't work because person is dirty
426
+ transaction.add(person);
427
+
428
+ Call `createRecord()` on the transaction directly:
429
+
430
+ var transaction = store.transaction();
431
+ transaction.createRecord(Person, { name: "Steve" });
432
+
433
+ ### Asynchronous Commits
434
+
435
+ Typically, all of the records in a transaction will be committed
436
+ together. However, new records that have a dependency on other new
437
+ records need to wait for their parent record to be saved and assigned an
438
+ ID. In that case, the child record will continue to live in the
439
+ transaction until its parent is saved, at which time the transaction will
440
+ attempt to commit again.
441
+
442
+ For this reason, you should not re-use transactions once you have committed
443
+ them. Always make a new transaction and move the desired records to it before
444
+ calling commit.
445
+ */
446
+
447
+ DS.Transaction = Ember.Object.extend({
448
+ /**
449
+ @private
450
+
451
+ Creates the bucket data structure used to segregate records by
452
+ type.
453
+ */
454
+ init: function() {
455
+ set(this, 'buckets', {
456
+ clean: Ember.Map.create(),
457
+ created: Ember.Map.create(),
458
+ updated: Ember.Map.create(),
459
+ deleted: Ember.Map.create(),
460
+ inflight: Ember.Map.create()
461
+ });
462
+ },
463
+
464
+ /**
465
+ Creates a new record of the given type and assigns it to the transaction
466
+ on which the method was called.
467
+
468
+ This is useful as only clean records can be added to a transaction and
469
+ new records created using other methods immediately become dirty.
470
+
471
+ @param {DS.Model} type the model type to create
472
+ @param {Object} hash the data hash to assign the new record
473
+ */
474
+ createRecord: function(type, hash) {
475
+ var store = get(this, 'store');
476
+
477
+ return store.createRecord(type, hash, this);
478
+ },
479
+
480
+ /**
481
+ Adds an existing record to this transaction. Only records without
482
+ modficiations (i.e., records whose `isDirty` property is `false`)
483
+ can be added to a transaction.
484
+
485
+ @param {DS.Model} record the record to add to the transaction
486
+ */
487
+ add: function(record) {
488
+ // we could probably make this work if someone has a valid use case. Do you?
489
+ Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
490
+
491
+ var recordTransaction = get(record, 'transaction'),
492
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
493
+
494
+ Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
495
+
496
+ this.adoptRecord(record);
497
+ },
498
+
499
+ /**
500
+ Commits the transaction, which causes all of the modified records that
501
+ belong to the transaction to be sent to the adapter to be saved.
502
+
503
+ Once you call `commit()` on a transaction, you should not re-use it.
504
+
505
+ When a record is saved, it will be removed from this transaction and
506
+ moved back to the store's default transaction.
507
+ */
508
+ commit: function() {
509
+ var self = this,
510
+ iterate;
511
+
512
+ iterate = function(bucketType, fn, binding) {
513
+ var dirty = self.bucketForType(bucketType);
514
+
515
+ dirty.forEach(function(type, records) {
516
+ if (records.isEmpty()) { return; }
517
+
518
+ var array = [];
519
+
520
+ records.forEach(function(record) {
521
+ record.send('willCommit');
522
+
523
+ if (get(record, 'isPending') === false) {
524
+ array.push(record);
525
+ }
526
+ });
527
+
528
+ fn.call(binding, type, array);
529
+ });
530
+ };
531
+
532
+ var commitDetails = {
533
+ updated: {
534
+ eachType: function(fn, binding) { iterate('updated', fn, binding); }
535
+ },
536
+
537
+ created: {
538
+ eachType: function(fn, binding) { iterate('created', fn, binding); }
539
+ },
540
+
541
+ deleted: {
542
+ eachType: function(fn, binding) { iterate('deleted', fn, binding); }
543
+ }
544
+ };
545
+
546
+ var store = get(this, 'store');
547
+ var adapter = get(store, '_adapter');
548
+
549
+ this.removeCleanRecords();
550
+
551
+ if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
552
+ else { throw fmt("Adapter is either null or does not implement `commit` method", this); }
553
+ },
554
+
555
+ /**
556
+ Rolling back a transaction resets the records that belong to
557
+ that transaction.
558
+
559
+ Updated records have their properties reset to the last known
560
+ value from the persistence layer. Deleted records are reverted
561
+ to a clean, non-deleted state. Newly created records immediately
562
+ become deleted, and are not sent to the adapter to be persisted.
563
+
564
+ After the transaction is rolled back, any records that belong
565
+ to it will return to the store's default transaction, and the
566
+ current transaction should not be used again.
567
+ */
568
+ rollback: function() {
569
+ var store = get(this, 'store'),
570
+ dirty;
571
+
572
+ // Loop through all of the records in each of the dirty states
573
+ // and initiate a rollback on them. As a side effect of telling
574
+ // the record to roll back, it should also move itself out of
575
+ // the dirty bucket and into the clean bucket.
576
+ ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) {
577
+ dirty = this.bucketForType(bucketType);
578
+
579
+ dirty.forEach(function(type, records) {
580
+ records.forEach(function(record) {
581
+ record.send('rollback');
582
+ });
583
+ });
584
+ }, this);
585
+
586
+ // Now that all records in the transaction are guaranteed to be
587
+ // clean, migrate them all to the store's default transaction.
588
+ this.removeCleanRecords();
589
+ },
590
+
591
+ /**
592
+ @private
593
+
594
+ Removes a record from this transaction and back to the store's
595
+ default transaction.
596
+
597
+ Note: This method is private for now, but should probably be exposed
598
+ in the future once we have stricter error checking (for example, in the
599
+ case of the record being dirty).
600
+
601
+ @param {DS.Model} record
602
+ */
603
+ remove: function(record) {
604
+ var defaultTransaction = getPath(this, 'store.defaultTransaction');
605
+ defaultTransaction.adoptRecord(record);
606
+ },
607
+
608
+ /**
609
+ @private
610
+
611
+ Removes all of the records in the transaction's clean bucket.
612
+ */
613
+ removeCleanRecords: function() {
614
+ var clean = this.bucketForType('clean'),
615
+ self = this;
616
+
617
+ clean.forEach(function(type, records) {
618
+ records.forEach(function(record) {
619
+ self.remove(record);
620
+ });
621
+ });
622
+ },
623
+
624
+ /**
625
+ @private
626
+
627
+ Returns the bucket for the given bucket type. For example, you might call
628
+ `this.bucketForType('updated')` to get the `Ember.Map` that contains all
629
+ of the records that have changes pending.
630
+
631
+ @param {String} bucketType the type of bucket
632
+ @returns Ember.Map
633
+ */
634
+ bucketForType: function(bucketType) {
635
+ var buckets = get(this, 'buckets');
636
+
637
+ return get(buckets, bucketType);
638
+ },
639
+
640
+ /**
641
+ @private
642
+
643
+ This method moves a record into a different transaction without the normal
644
+ checks that ensure that the user is not doing something weird, like moving
645
+ a dirty record into a new transaction.
646
+
647
+ It is designed for internal use, such as when we are moving a clean record
648
+ into a new transaction when the transaction is committed.
649
+
650
+ This method must not be called unless the record is clean.
651
+
652
+ @param {DS.Model} record
653
+ */
654
+ adoptRecord: function(record) {
655
+ var oldTransaction = get(record, 'transaction');
656
+
657
+ if (oldTransaction) {
658
+ oldTransaction.removeFromBucket('clean', record);
659
+ }
660
+
661
+ this.addToBucket('clean', record);
662
+ set(record, 'transaction', this);
663
+ },
664
+
665
+ /**
666
+ @private
667
+
668
+ Adds a record to the named bucket.
669
+
670
+ @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
671
+ */
672
+ addToBucket: function(bucketType, record) {
673
+ var bucket = this.bucketForType(bucketType),
674
+ type = record.constructor;
675
+
676
+ var records = bucket.get(type);
677
+
678
+ if (!records) {
679
+ records = Ember.OrderedSet.create();
680
+ bucket.set(type, records);
681
+ }
682
+
683
+ records.add(record);
684
+ },
685
+
686
+ /**
687
+ @private
688
+
689
+ Removes a record from the named bucket.
690
+
691
+ @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
692
+ */
693
+ removeFromBucket: function(bucketType, record) {
694
+ var bucket = this.bucketForType(bucketType),
695
+ type = record.constructor;
696
+
697
+ var records = bucket.get(type);
698
+ records.remove(record);
699
+ },
700
+
701
+ /**
702
+ @private
703
+
704
+ Called by a record's state manager to indicate that the record has entered
705
+ a dirty state. The record will be moved from the `clean` bucket and into
706
+ the appropriate dirty bucket.
707
+
708
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
709
+ */
710
+ recordBecameDirty: function(bucketType, record) {
711
+ this.removeFromBucket('clean', record);
712
+ this.addToBucket(bucketType, record);
713
+ },
714
+
715
+ /**
716
+ @private
717
+
718
+ Called by a record's state manager to indicate that the record has entered
719
+ inflight state. The record will be moved from its current dirty bucket and into
720
+ the `inflight` bucket.
721
+
722
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
723
+ */
724
+ recordBecameInFlight: function(kind, record) {
725
+ this.removeFromBucket(kind, record);
726
+ this.addToBucket('inflight', record);
727
+ },
728
+
729
+ /**
730
+ @private
731
+
732
+ Called by a record's state manager to indicate that the record has entered
733
+ a clean state. The record will be moved from its current dirty or inflight bucket and into
734
+ the `clean` bucket.
735
+
736
+ @param {String} bucketType one of `created`, `updated`, or `deleted`
737
+ */
738
+ recordBecameClean: function(kind, record) {
739
+ this.removeFromBucket(kind, record);
740
+
741
+ this.remove(record);
742
+ }
743
+ });
744
+
745
+ })();
746
+
747
+
748
+
749
+ (function() {
750
+ /*globals Ember*/
751
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
752
+
753
+ var DATA_PROXY = {
754
+ get: function(name) {
755
+ return this.savedData[name];
756
+ }
757
+ };
758
+
759
+ // These values are used in the data cache when clientIds are
760
+ // needed but the underlying data has not yet been loaded by
761
+ // the server.
762
+ var UNLOADED = 'unloaded';
763
+ var LOADING = 'loading';
764
+
765
+ // Implementors Note:
766
+ //
767
+ // The variables in this file are consistently named according to the following
768
+ // scheme:
769
+ //
770
+ // * +id+ means an identifier managed by an external source, provided inside the
771
+ // data hash provided by that source.
772
+ // * +clientId+ means a transient numerical identifier generated at runtime by
773
+ // the data store. It is important primarily because newly created objects may
774
+ // not yet have an externally generated id.
775
+ // * +type+ means a subclass of DS.Model.
776
+
777
+ /**
778
+ The store contains all of the hashes for records loaded from the server.
779
+ It is also responsible for creating instances of DS.Model when you request one
780
+ of these data hashes, so that they can be bound to in your Handlebars templates.
781
+
782
+ Create a new store like this:
783
+
784
+ MyApp.store = DS.Store.create();
785
+
786
+ You can retrieve DS.Model instances from the store in several ways. To retrieve
787
+ a record for a specific id, use the `find()` method:
788
+
789
+ var record = MyApp.store.find(MyApp.Contact, 123);
790
+
791
+ By default, the store will talk to your backend using a standard REST mechanism.
792
+ You can customize how the store talks to your backend by specifying a custom adapter:
793
+
794
+ MyApp.store = DS.Store.create({
795
+ adapter: 'MyApp.CustomAdapter'
796
+ });
797
+
798
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
799
+ documentation.
800
+ */
801
+ DS.Store = Ember.Object.extend({
802
+
803
+ /**
804
+ Many methods can be invoked without specifying which store should be used.
805
+ In those cases, the first store created will be used as the default. If
806
+ an application has multiple stores, it should specify which store to use
807
+ when performing actions, such as finding records by id.
808
+
809
+ The init method registers this store as the default if none is specified.
810
+ */
811
+ init: function() {
812
+ // Enforce API revisioning. See BREAKING_CHANGES.md for more.
813
+ var revision = get(this, 'revision');
814
+
815
+ if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
816
+ 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);
817
+ }
818
+
819
+ if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
820
+ set(DS, 'defaultStore', this);
821
+ }
822
+
823
+ // internal bookkeeping; not observable
824
+ this.typeMaps = {};
825
+ this.recordCache = [];
826
+ this.clientIdToId = {};
827
+ this.recordArraysByClientId = {};
828
+
829
+ set(this, 'defaultTransaction', this.transaction());
830
+
831
+ return this._super();
832
+ },
833
+
834
+ /**
835
+ Returns a new transaction scoped to this store.
836
+
837
+ @see {DS.Transaction}
838
+ @returns DS.Transaction
839
+ */
840
+ transaction: function() {
841
+ return DS.Transaction.create({ store: this });
842
+ },
843
+
844
+ /**
845
+ @private
846
+
847
+ This is used only by the record's DataProxy. Do not use this directly.
848
+ */
849
+ dataForRecord: function(record) {
850
+ var type = record.constructor,
851
+ clientId = get(record, 'clientId'),
852
+ typeMap = this.typeMapFor(type);
853
+
854
+ return typeMap.cidToHash[clientId];
855
+ },
856
+
857
+ /**
858
+ The adapter to use to communicate to a backend server or other persistence layer.
859
+
860
+ This can be specified as an instance, a class, or a property path that specifies
861
+ where the adapter can be located.
862
+
863
+ @property {DS.Adapter|String}
864
+ */
865
+ adapter: null,
866
+
867
+ /**
868
+ @private
869
+
870
+ This property returns the adapter, after resolving a possible String.
871
+
872
+ @returns DS.Adapter
873
+ */
874
+ _adapter: Ember.computed(function() {
875
+ var adapter = get(this, 'adapter');
876
+ if (typeof adapter === 'string') {
877
+ return getPath(this, adapter, false) || getPath(window, adapter);
878
+ }
879
+ return adapter;
880
+ }).property('adapter').cacheable(),
881
+
882
+ // A monotonically increasing number to be used to uniquely identify
883
+ // data hashes and records.
884
+ clientIdCounter: 1,
885
+
886
+ // .....................
887
+ // . CREATE NEW RECORD .
888
+ // .....................
889
+
890
+ /**
891
+ Create a new record in the current store. The properties passed
892
+ to this method are set on the newly created record.
893
+
894
+ @param {subclass of DS.Model} type
895
+ @param {Object} properties a hash of properties to set on the
896
+ newly created record.
897
+ @returns DS.Model
898
+ */
899
+ createRecord: function(type, properties, transaction) {
900
+ properties = properties || {};
901
+
902
+ // Create a new instance of the model `type` and put it
903
+ // into the specified `transaction`. If no transaction is
904
+ // specified, the default transaction will be used.
905
+ //
906
+ // NOTE: A `transaction` is specified when the
907
+ // `transaction.createRecord` API is used.
908
+ var record = type._create({
909
+ store: this
910
+ });
911
+
912
+ transaction = transaction || get(this, 'defaultTransaction');
913
+ transaction.adoptRecord(record);
914
+
915
+ // Extract the primary key from the `properties` hash,
916
+ // based on the `primaryKey` for the model type.
917
+ var primaryKey = get(record, 'primaryKey'),
918
+ id = properties[primaryKey] || null;
919
+
920
+ // If the passed properties do not include a primary key,
921
+ // give the adapter an opportunity to generate one.
922
+ var adapter;
923
+ if (Ember.none(id)) {
924
+ adapter = get(this, 'adapter');
925
+ if (adapter && adapter.generateIdForRecord) {
926
+ id = adapter.generateIdForRecord(this, record);
927
+ properties.id = id;
928
+ }
929
+ }
930
+
931
+ var hash = {}, clientId;
932
+
933
+ // Push the hash into the store. If present, associate the
934
+ // extracted `id` with the hash.
935
+ clientId = this.pushHash(hash, id, type);
936
+
937
+ record.send('didChangeData');
938
+
939
+ var recordCache = get(this, 'recordCache');
940
+
941
+ // Now that we have a clientId, attach it to the record we
942
+ // just created.
943
+ set(record, 'clientId', clientId);
944
+
945
+ // Store the record we just created in the record cache for
946
+ // this clientId.
947
+ recordCache[clientId] = record;
948
+
949
+ // Set the properties specified on the record.
950
+ record.setProperties(properties);
951
+
952
+ this.updateRecordArrays(type, clientId, get(record, 'data'));
953
+
954
+ return record;
955
+ },
956
+
957
+ // .................
958
+ // . DELETE RECORD .
959
+ // .................
960
+
961
+ /**
962
+ For symmetry, a record can be deleted via the store.
963
+
964
+ @param {DS.Model} record
965
+ */
966
+ deleteRecord: function(record) {
967
+ record.send('deleteRecord');
968
+ },
969
+
970
+ // ................
971
+ // . FIND RECORDS .
972
+ // ................
973
+
974
+ /**
975
+ This is the main entry point into finding records. The first
976
+ parameter to this method is always a subclass of `DS.Model`.
977
+
978
+ You can use the `find` method on a subclass of `DS.Model`
979
+ directly if your application only has one store. For
980
+ example, instead of `store.find(App.Person, 1)`, you could
981
+ say `App.Person.find(1)`.
982
+
983
+ ---
984
+
985
+ To find a record by ID, pass the `id` as the second parameter:
986
+
987
+ store.find(App.Person, 1);
988
+ App.Person.find(1);
989
+
990
+ If the record with that `id` had not previously been loaded,
991
+ the store will return an empty record immediately and ask
992
+ the adapter to find the data by calling the adapter's `find`
993
+ method.
994
+
995
+ The `find` method will always return the same object for a
996
+ given type and `id`. To check whether the adapter has populated
997
+ a record, you can check its `isLoaded` property.
998
+
999
+ ---
1000
+
1001
+ To find all records for a type, call `find` with no additional
1002
+ parameters:
1003
+
1004
+ store.find(App.Person);
1005
+ App.Person.find();
1006
+
1007
+ This will return a `RecordArray` representing all known records
1008
+ for the given type and kick off a request to the adapter's
1009
+ `findAll` method to load any additional records for the type.
1010
+
1011
+ The `RecordArray` returned by `find()` is live. If any more
1012
+ records for the type are added at a later time through any
1013
+ mechanism, it will automatically update to reflect the change.
1014
+
1015
+ ---
1016
+
1017
+ To find a record by a query, call `find` with a hash as the
1018
+ second parameter:
1019
+
1020
+ store.find(App.Person, { page: 1 });
1021
+ App.Person.find({ page: 1 });
1022
+
1023
+ This will return a `RecordArray` immediately, but it will always
1024
+ be an empty `RecordArray` at first. It will call the adapter's
1025
+ `findQuery` method, which will populate the `RecordArray` once
1026
+ the server has returned results.
1027
+
1028
+ You can check whether a query results `RecordArray` has loaded
1029
+ by checking its `isLoaded` property.
1030
+ */
1031
+ find: function(type, id, query) {
1032
+ if (id === undefined) {
1033
+ return this.findAll(type);
1034
+ }
1035
+
1036
+ if (query !== undefined) {
1037
+ return this.findMany(type, id, query);
1038
+ } else if (Ember.typeOf(id) === 'object') {
1039
+ return this.findQuery(type, id);
1040
+ }
1041
+
1042
+ if (Ember.isArray(id)) {
1043
+ return this.findMany(type, id);
1044
+ }
1045
+
1046
+ var clientId = this.typeMapFor(type).idToCid[id];
1047
+
1048
+ return this.findByClientId(type, clientId, id);
1049
+ },
1050
+
1051
+ findByClientId: function(type, clientId, id) {
1052
+ var recordCache = get(this, 'recordCache'),
1053
+ dataCache = this.typeMapFor(type).cidToHash,
1054
+ record;
1055
+
1056
+ // If there is already a clientId assigned for this
1057
+ // type/id combination, try to find an existing
1058
+ // record for that id and return. Otherwise,
1059
+ // materialize a new record and set its data to the
1060
+ // value we already have.
1061
+ if (clientId !== undefined) {
1062
+ record = recordCache[clientId];
1063
+
1064
+ if (!record) {
1065
+ // create a new instance of the model type in the
1066
+ // 'isLoading' state
1067
+ record = this.materializeRecord(type, clientId);
1068
+
1069
+ if (typeof dataCache[clientId] === 'object') {
1070
+ record.send('didChangeData');
1071
+ }
1072
+ }
1073
+ } else {
1074
+ clientId = this.pushHash(LOADING, id, type);
1075
+
1076
+ // create a new instance of the model type in the
1077
+ // 'isLoading' state
1078
+ record = this.materializeRecord(type, clientId, id);
1079
+
1080
+ // let the adapter set the data, possibly async
1081
+ var adapter = get(this, '_adapter');
1082
+ if (adapter && adapter.find) { adapter.find(this, type, id); }
1083
+ else { throw fmt("Adapter is either null or does not implement `find` method", this); }
1084
+ }
1085
+
1086
+ return record;
1087
+ },
1088
+
1089
+ /**
1090
+ @private
1091
+
1092
+ Ask the adapter to fetch IDs that are not already loaded.
1093
+
1094
+ This method will convert `id`s to `clientId`s, filter out
1095
+ `clientId`s that already have a data hash present, and pass
1096
+ the remaining `id`s to the adapter.
1097
+
1098
+ @param {Class} type A model class
1099
+ @param {Array} ids An array of ids
1100
+ @param {Object} query
1101
+
1102
+ @returns {Array} An Array of all clientIds for the
1103
+ specified ids.
1104
+ */
1105
+ fetchMany: function(type, ids, query) {
1106
+ var typeMap = this.typeMapFor(type),
1107
+ idToClientIdMap = typeMap.idToCid,
1108
+ dataCache = typeMap.cidToHash,
1109
+ data = typeMap.cidToHash,
1110
+ needed;
1111
+
1112
+ var clientIds = Ember.A([]);
1113
+
1114
+ if (ids) {
1115
+ needed = [];
1116
+
1117
+ ids.forEach(function(id) {
1118
+ // Get the clientId for the given id
1119
+ var clientId = idToClientIdMap[id];
1120
+
1121
+ // If there is no `clientId` yet
1122
+ if (clientId === undefined) {
1123
+ // Create a new `clientId`, marking its data hash
1124
+ // as loading. Once the adapter returns the data
1125
+ // hash, it will be updated
1126
+ clientId = this.pushHash(LOADING, id, type);
1127
+ needed.push(id);
1128
+
1129
+ // If there is a clientId, but its data hash is
1130
+ // marked as unloaded (this happens when a
1131
+ // hasMany association creates clientIds for its
1132
+ // referenced ids before they were loaded)
1133
+ } else if (clientId && data[clientId] === UNLOADED) {
1134
+ // change the data hash marker to loading
1135
+ dataCache[clientId] = LOADING;
1136
+ needed.push(id);
1137
+ }
1138
+
1139
+ // this method is expected to return a list of
1140
+ // all of the clientIds for the specified ids,
1141
+ // unconditionally add it.
1142
+ clientIds.push(clientId);
1143
+ }, this);
1144
+ } else {
1145
+ needed = null;
1146
+ }
1147
+
1148
+ // If there are any needed ids, ask the adapter to load them
1149
+ if ((needed && get(needed, 'length') > 0) || query) {
1150
+ var adapter = get(this, '_adapter');
1151
+ if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
1152
+ else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
1153
+ }
1154
+
1155
+ return clientIds;
1156
+ },
1157
+
1158
+ /** @private
1159
+ */
1160
+ findMany: function(type, ids, query) {
1161
+ var clientIds = this.fetchMany(type, ids, query);
1162
+
1163
+ return this.createManyArray(type, clientIds);
1164
+ },
1165
+
1166
+ findQuery: function(type, query) {
1167
+ var array = DS.AdapterPopulatedRecordArray.create({ type: type, content: Ember.A([]), store: this });
1168
+ var adapter = get(this, '_adapter');
1169
+ if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
1170
+ else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); }
1171
+ return array;
1172
+ },
1173
+
1174
+ findAll: function(type) {
1175
+
1176
+ var typeMap = this.typeMapFor(type),
1177
+ findAllCache = typeMap.findAllCache;
1178
+
1179
+ if (findAllCache) { return findAllCache; }
1180
+
1181
+ var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this });
1182
+ this.registerRecordArray(array, type);
1183
+
1184
+ var adapter = get(this, '_adapter');
1185
+ if (adapter && adapter.findAll) { adapter.findAll(this, type); }
1186
+
1187
+ typeMap.findAllCache = array;
1188
+ return array;
1189
+ },
1190
+
1191
+ filter: function(type, query, filter) {
1192
+ // allow an optional server query
1193
+ if (arguments.length === 3) {
1194
+ this.findQuery(type, query);
1195
+ } else if (arguments.length === 2) {
1196
+ filter = query;
1197
+ }
1198
+
1199
+ var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
1200
+
1201
+ this.registerRecordArray(array, type, filter);
1202
+
1203
+ return array;
1204
+ },
1205
+
1206
+ // ............
1207
+ // . UPDATING .
1208
+ // ............
1209
+
1210
+ hashWasUpdated: function(type, clientId, record) {
1211
+ // Because hash updates are invoked at the end of the run loop,
1212
+ // it is possible that a record might be deleted after its hash
1213
+ // has been modified and this method was scheduled to be called.
1214
+ //
1215
+ // If that's the case, the record would have already been removed
1216
+ // from all record arrays; calling updateRecordArrays would just
1217
+ // add it back. If the record is deleted, just bail. It shouldn't
1218
+ // give us any more trouble after this.
1219
+
1220
+ if (get(record, 'isDeleted')) { return; }
1221
+ this.updateRecordArrays(type, clientId, get(record, 'data'));
1222
+ },
1223
+
1224
+ // ..............
1225
+ // . PERSISTING .
1226
+ // ..............
1227
+
1228
+ commit: function() {
1229
+ var defaultTransaction = get(this, 'defaultTransaction');
1230
+ set(this, 'defaultTransaction', this.transaction());
1231
+
1232
+ defaultTransaction.commit();
1233
+ },
1234
+
1235
+ didUpdateRecords: function(array, hashes) {
1236
+ if (hashes) {
1237
+ array.forEach(function(record, idx) {
1238
+ this.didUpdateRecord(record, hashes[idx]);
1239
+ }, this);
1240
+ } else {
1241
+ array.forEach(function(record) {
1242
+ this.didUpdateRecord(record);
1243
+ }, this);
1244
+ }
1245
+ },
1246
+
1247
+ didUpdateRecord: function(record, hash) {
1248
+ if (hash) {
1249
+ var clientId = get(record, 'clientId'),
1250
+ dataCache = this.typeMapFor(record.constructor).cidToHash;
1251
+
1252
+ dataCache[clientId] = hash;
1253
+ record.send('didChangeData');
1254
+ record.hashWasUpdated();
1255
+ } else {
1256
+ record.send('didSaveData');
1257
+ }
1258
+
1259
+ record.send('didCommit');
1260
+ },
1261
+
1262
+ didDeleteRecords: function(array) {
1263
+ array.forEach(function(record) {
1264
+ record.send('didCommit');
1265
+ });
1266
+ },
1267
+
1268
+ didDeleteRecord: function(record) {
1269
+ record.send('didCommit');
1270
+ },
1271
+
1272
+ _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) {
1273
+ var recordData = get(record, 'data'), id, changes;
1274
+
1275
+ if (hash) {
1276
+ typeMap.cidToHash[clientId] = hash;
1277
+
1278
+ // If the server returns a hash, we assume that the server's version
1279
+ // of the data supercedes the local changes.
1280
+ record.beginPropertyChanges();
1281
+ record.send('didChangeData');
1282
+ recordData.adapterDidUpdate();
1283
+ record.hashWasUpdated();
1284
+ record.endPropertyChanges();
1285
+
1286
+ id = hash[primaryKey];
1287
+
1288
+ typeMap.idToCid[id] = clientId;
1289
+ this.clientIdToId[clientId] = id;
1290
+ } else {
1291
+ recordData.commit();
1292
+ }
1293
+
1294
+ record.send('didCommit');
1295
+ },
1296
+
1297
+
1298
+ didCreateRecords: function(type, array, hashes) {
1299
+ var primaryKey = type.proto().primaryKey,
1300
+ typeMap = this.typeMapFor(type),
1301
+ clientId;
1302
+
1303
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
1304
+ var record = array[i], hash = hashes[i];
1305
+ clientId = get(record, 'clientId');
1306
+
1307
+ this._didCreateRecord(record, hash, typeMap, clientId, primaryKey);
1308
+ }
1309
+ },
1310
+
1311
+ didCreateRecord: function(record, hash) {
1312
+ var type = record.constructor,
1313
+ typeMap = this.typeMapFor(type),
1314
+ clientId, primaryKey;
1315
+
1316
+ // The hash is optional, but if it is not provided, the client must have
1317
+ // provided a primary key.
1318
+
1319
+ primaryKey = type.proto().primaryKey;
1320
+
1321
+ // TODO: Make Ember.assert more flexible
1322
+ if (hash) {
1323
+ Ember.assert("The server must provide a primary key: " + primaryKey, get(hash, primaryKey));
1324
+ } else {
1325
+ Ember.assert("The server did not return data, and you did not create a primary key (" + primaryKey + ") on the client", get(get(record, 'data'), primaryKey));
1326
+ }
1327
+
1328
+ clientId = get(record, 'clientId');
1329
+
1330
+ this._didCreateRecord(record, hash, typeMap, clientId, primaryKey);
1331
+ },
1332
+
1333
+ recordWasInvalid: function(record, errors) {
1334
+ record.send('becameInvalid', errors);
1335
+ },
1336
+
1337
+ // .................
1338
+ // . RECORD ARRAYS .
1339
+ // .................
1340
+
1341
+ registerRecordArray: function(array, type, filter) {
1342
+ var recordArrays = this.typeMapFor(type).recordArrays;
1343
+
1344
+ recordArrays.push(array);
1345
+
1346
+ this.updateRecordArrayFilter(array, type, filter);
1347
+ },
1348
+
1349
+ createManyArray: function(type, clientIds) {
1350
+ var array = DS.ManyArray.create({ type: type, content: clientIds, store: this });
1351
+
1352
+ clientIds.forEach(function(clientId) {
1353
+ var recordArrays = this.recordArraysForClientId(clientId);
1354
+ recordArrays.add(array);
1355
+ }, this);
1356
+
1357
+ return array;
1358
+ },
1359
+
1360
+ updateRecordArrayFilter: function(array, type, filter) {
1361
+ var typeMap = this.typeMapFor(type),
1362
+ dataCache = typeMap.cidToHash,
1363
+ clientIds = typeMap.clientIds,
1364
+ clientId, hash, proxy;
1365
+
1366
+ var recordCache = get(this, 'recordCache'),
1367
+ foundRecord,
1368
+ record;
1369
+
1370
+ for (var i=0, l=clientIds.length; i<l; i++) {
1371
+ clientId = clientIds[i];
1372
+ foundRecord = false;
1373
+
1374
+ hash = dataCache[clientId];
1375
+ if (typeof hash === 'object') {
1376
+ if (record = recordCache[clientId]) {
1377
+ if (!get(record, 'isDeleted')) {
1378
+ proxy = get(record, 'data');
1379
+ foundRecord = true;
1380
+ }
1381
+ } else {
1382
+ DATA_PROXY.savedData = hash;
1383
+ proxy = DATA_PROXY;
1384
+ foundRecord = true;
1385
+ }
1386
+
1387
+ if (foundRecord) { this.updateRecordArray(array, filter, type, clientId, proxy); }
1388
+ }
1389
+ }
1390
+ },
1391
+
1392
+ updateRecordArrays: function(type, clientId, dataProxy) {
1393
+ var recordArrays = this.typeMapFor(type).recordArrays,
1394
+ filter;
1395
+
1396
+ recordArrays.forEach(function(array) {
1397
+ filter = get(array, 'filterFunction');
1398
+ this.updateRecordArray(array, filter, type, clientId, dataProxy);
1399
+ }, this);
1400
+ },
1401
+
1402
+ updateRecordArray: function(array, filter, type, clientId, dataProxy) {
1403
+ var shouldBeInArray;
1404
+
1405
+ if (!filter) {
1406
+ shouldBeInArray = true;
1407
+ } else {
1408
+ shouldBeInArray = filter(dataProxy);
1409
+ }
1410
+
1411
+ var content = get(array, 'content');
1412
+ var alreadyInArray = content.indexOf(clientId) !== -1;
1413
+
1414
+ var recordArrays = this.recordArraysForClientId(clientId);
1415
+
1416
+ if (shouldBeInArray && !alreadyInArray) {
1417
+ recordArrays.add(array);
1418
+ content.pushObject(clientId);
1419
+ } else if (!shouldBeInArray && alreadyInArray) {
1420
+ recordArrays.remove(array);
1421
+ content.removeObject(clientId);
1422
+ }
1423
+ },
1424
+
1425
+ removeFromRecordArrays: function(record) {
1426
+ var clientId = get(record, 'clientId');
1427
+ var recordArrays = this.recordArraysForClientId(clientId);
1428
+
1429
+ recordArrays.forEach(function(array) {
1430
+ var content = get(array, 'content');
1431
+ content.removeObject(clientId);
1432
+ });
1433
+ },
1434
+
1435
+ // ............
1436
+ // . INDEXING .
1437
+ // ............
1438
+
1439
+ recordArraysForClientId: function(clientId) {
1440
+ var recordArrays = get(this, 'recordArraysByClientId');
1441
+ var ret = recordArrays[clientId];
1442
+
1443
+ if (!ret) {
1444
+ ret = recordArrays[clientId] = Ember.OrderedSet.create();
1445
+ }
1446
+
1447
+ return ret;
1448
+ },
1449
+
1450
+ typeMapFor: function(type) {
1451
+ var typeMaps = get(this, 'typeMaps');
1452
+ var guidForType = Ember.guidFor(type);
1453
+
1454
+ var typeMap = typeMaps[guidForType];
1455
+
1456
+ if (typeMap) {
1457
+ return typeMap;
1458
+ } else {
1459
+ return (typeMaps[guidForType] =
1460
+ {
1461
+ idToCid: {},
1462
+ clientIds: [],
1463
+ cidToHash: {},
1464
+ recordArrays: []
1465
+ });
1466
+ }
1467
+ },
1468
+
1469
+ /** @private
1470
+
1471
+ For a given type and id combination, returns the client id used by the store.
1472
+ If no client id has been assigned yet, one will be created and returned.
1473
+
1474
+ @param {DS.Model} type
1475
+ @param {String|Number} id
1476
+ */
1477
+ clientIdForId: function(type, id) {
1478
+ var clientId = this.typeMapFor(type).idToCid[id];
1479
+
1480
+ if (clientId !== undefined) { return clientId; }
1481
+
1482
+ return this.pushHash(UNLOADED, id, type);
1483
+ },
1484
+
1485
+ // ................
1486
+ // . LOADING DATA .
1487
+ // ................
1488
+
1489
+ /**
1490
+ Load a new data hash into the store for a given id and type combination.
1491
+ If data for that record had been loaded previously, the new information
1492
+ overwrites the old.
1493
+
1494
+ If the record you are loading data for has outstanding changes that have not
1495
+ yet been saved, an exception will be thrown.
1496
+
1497
+ @param {DS.Model} type
1498
+ @param {String|Number} id
1499
+ @param {Object} hash the data hash to load
1500
+ */
1501
+ load: function(type, id, hash) {
1502
+ if (hash === undefined) {
1503
+ hash = id;
1504
+ var primaryKey = type.proto().primaryKey;
1505
+ Ember.assert("A data hash was loaded for a record of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", primaryKey in hash);
1506
+ id = hash[primaryKey];
1507
+ }
1508
+
1509
+ var typeMap = this.typeMapFor(type),
1510
+ dataCache = typeMap.cidToHash,
1511
+ clientId = typeMap.idToCid[id],
1512
+ recordCache = get(this, 'recordCache');
1513
+
1514
+ if (clientId !== undefined) {
1515
+ dataCache[clientId] = hash;
1516
+
1517
+ var record = recordCache[clientId];
1518
+ if (record) {
1519
+ record.send('didChangeData');
1520
+ }
1521
+ } else {
1522
+ clientId = this.pushHash(hash, id, type);
1523
+ }
1524
+
1525
+ DATA_PROXY.savedData = hash;
1526
+ this.updateRecordArrays(type, clientId, DATA_PROXY);
1527
+
1528
+ return { id: id, clientId: clientId };
1529
+ },
1530
+
1531
+ loadMany: function(type, ids, hashes) {
1532
+ var clientIds = Ember.A([]);
1533
+
1534
+ if (hashes === undefined) {
1535
+ hashes = ids;
1536
+ ids = [];
1537
+ var primaryKey = type.proto().primaryKey;
1538
+
1539
+ ids = Ember.ArrayUtils.map(hashes, function(hash) {
1540
+ return hash[primaryKey];
1541
+ });
1542
+ }
1543
+
1544
+ for (var i=0, l=get(ids, 'length'); i<l; i++) {
1545
+ var loaded = this.load(type, ids[i], hashes[i]);
1546
+ clientIds.pushObject(loaded.clientId);
1547
+ }
1548
+
1549
+ return { clientIds: clientIds, ids: ids };
1550
+ },
1551
+
1552
+ /** @private
1553
+
1554
+ Stores a data hash for the specified type and id combination and returns
1555
+ the client id.
1556
+
1557
+ @param {Object} hash
1558
+ @param {String|Number} id
1559
+ @param {DS.Model} type
1560
+ @returns {Number}
1561
+ */
1562
+ pushHash: function(hash, id, type) {
1563
+ var typeMap = this.typeMapFor(type);
1564
+
1565
+ var idToClientIdMap = typeMap.idToCid,
1566
+ clientIdToIdMap = this.clientIdToId,
1567
+ clientIds = typeMap.clientIds,
1568
+ dataCache = typeMap.cidToHash;
1569
+
1570
+ var clientId = ++this.clientIdCounter;
1571
+
1572
+ dataCache[clientId] = hash;
1573
+
1574
+ // if we're creating an item, this process will be done
1575
+ // later, once the object has been persisted.
1576
+ if (id) {
1577
+ idToClientIdMap[id] = clientId;
1578
+ clientIdToIdMap[clientId] = id;
1579
+ }
1580
+
1581
+ clientIds.push(clientId);
1582
+
1583
+ return clientId;
1584
+ },
1585
+
1586
+ // ..........................
1587
+ // . RECORD MATERIALIZATION .
1588
+ // ..........................
1589
+
1590
+ materializeRecord: function(type, clientId, id) {
1591
+ var record;
1592
+
1593
+ get(this, 'recordCache')[clientId] = record = type._create({
1594
+ store: this,
1595
+ clientId: clientId,
1596
+ _id: id
1597
+ });
1598
+
1599
+ get(this, 'defaultTransaction').adoptRecord(record);
1600
+
1601
+ record.send('loadingData');
1602
+ return record;
1603
+ },
1604
+
1605
+ destroy: function() {
1606
+ if (get(DS, 'defaultStore') === this) {
1607
+ set(DS, 'defaultStore', null);
1608
+ }
1609
+
1610
+ return this._super();
1611
+ }
1612
+ });
1613
+
1614
+ })();
1615
+
1616
+
1617
+
1618
+ (function() {
1619
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
1620
+
1621
+ /**
1622
+ This file encapsulates the various states that a record can transition
1623
+ through during its lifecycle.
1624
+
1625
+ ### State Manager
1626
+
1627
+ A record's state manager explicitly tracks what state a record is in
1628
+ at any given time. For instance, if a record is newly created and has
1629
+ not yet been sent to the adapter to be saved, it would be in the
1630
+ `created.uncommitted` state. If a record has had local modifications
1631
+ made to it that are in the process of being saved, the record would be
1632
+ in the `updated.inFlight` state. (These state paths will be explained
1633
+ in more detail below.)
1634
+
1635
+ Events are sent by the record or its store to the record's state manager.
1636
+ How the state manager reacts to these events is dependent on which state
1637
+ it is in. In some states, certain events will be invalid and will cause
1638
+ an exception to be raised.
1639
+
1640
+ States are hierarchical. For example, a record can be in the
1641
+ `deleted.start` state, then transition into the `deleted.inFlight` state.
1642
+ If a child state does not implement an event handler, the state manager
1643
+ will attempt to invoke the event on all parent states until the root state is
1644
+ reached. The state hierarchy of a record is described in terms of a path
1645
+ string. You can determine a record's current state by getting its manager's
1646
+ current state path:
1647
+
1648
+ record.getPath('stateManager.currentState.path');
1649
+ //=> "created.uncommitted"
1650
+
1651
+ The `DS.Model` states are themselves stateless. What we mean is that,
1652
+ though each instance of a record also has a unique instance of a
1653
+ `DS.StateManager`, the hierarchical states that each of *those* points
1654
+ to is a shared data structure. For performance reasons, instead of each
1655
+ record getting its own copy of the hierarchy of states, each state
1656
+ manager points to this global, immutable shared instance. How does a
1657
+ state know which record it should be acting on? We pass a reference to
1658
+ the current state manager as the first parameter to every method invoked
1659
+ on a state.
1660
+
1661
+ The state manager passed as the first parameter is where you should stash
1662
+ state about the record if needed; you should never store data on the state
1663
+ object itself. If you need access to the record being acted on, you can
1664
+ retrieve the state manager's `record` property. For example, if you had
1665
+ an event handler `myEvent`:
1666
+
1667
+ myEvent: function(manager) {
1668
+ var record = manager.get('record');
1669
+ record.doSomething();
1670
+ }
1671
+
1672
+ For more information about state managers in general, see the Ember.js
1673
+ documentation on `Ember.StateManager`.
1674
+
1675
+ ### Events, Flags, and Transitions
1676
+
1677
+ A state may implement zero or more events, flags, or transitions.
1678
+
1679
+ #### Events
1680
+
1681
+ Events are named functions that are invoked when sent to a record. The
1682
+ state manager will first look for a method with the given name on the
1683
+ current state. If no method is found, it will search the current state's
1684
+ parent, and then its grandparent, and so on until reaching the top of
1685
+ the hierarchy. If the root is reached without an event handler being found,
1686
+ an exception will be raised. This can be very helpful when debugging new
1687
+ features.
1688
+
1689
+ Here's an example implementation of a state with a `myEvent` event handler:
1690
+
1691
+ aState: DS.State.create({
1692
+ myEvent: function(manager, param) {
1693
+ console.log("Received myEvent with "+param);
1694
+ }
1695
+ })
1696
+
1697
+ To trigger this event:
1698
+
1699
+ record.send('myEvent', 'foo');
1700
+ //=> "Received myEvent with foo"
1701
+
1702
+ Note that an optional parameter can be sent to a record's `send()` method,
1703
+ which will be passed as the second parameter to the event handler.
1704
+
1705
+ Events should transition to a different state if appropriate. This can be
1706
+ done by calling the state manager's `goToState()` method with a path to the
1707
+ desired state. The state manager will attempt to resolve the state path
1708
+ relative to the current state. If no state is found at that path, it will
1709
+ attempt to resolve it relative to the current state's parent, and then its
1710
+ parent, and so on until the root is reached. For example, imagine a hierarchy
1711
+ like this:
1712
+
1713
+ * created
1714
+ * start <-- currentState
1715
+ * inFlight
1716
+ * updated
1717
+ * inFlight
1718
+
1719
+ If we are currently in the `start` state, calling
1720
+ `goToState('inFlight')` would transition to the `created.inFlight` state,
1721
+ while calling `goToState('updated.inFlight')` would transition to
1722
+ the `updated.inFlight` state.
1723
+
1724
+ Remember that *only events* should ever cause a state transition. You should
1725
+ never call `goToState()` from outside a state's event handler. If you are
1726
+ tempted to do so, create a new event and send that to the state manager.
1727
+
1728
+ #### Flags
1729
+
1730
+ Flags are Boolean values that can be used to introspect a record's current
1731
+ state in a more user-friendly way than examining its state path. For example,
1732
+ instead of doing this:
1733
+
1734
+ var statePath = record.getPath('stateManager.currentState.path');
1735
+ if (statePath === 'created.inFlight') {
1736
+ doSomething();
1737
+ }
1738
+
1739
+ You can say:
1740
+
1741
+ if (record.get('isNew') && record.get('isSaving')) {
1742
+ doSomething();
1743
+ }
1744
+
1745
+ If your state does not set a value for a given flag, the value will
1746
+ be inherited from its parent (or the first place in the state hierarchy
1747
+ where it is defined).
1748
+
1749
+ The current set of flags are defined below. If you want to add a new flag,
1750
+ in addition to the area below, you will also need to declare it in the
1751
+ `DS.Model` class.
1752
+
1753
+ #### Transitions
1754
+
1755
+ Transitions are like event handlers but are called automatically upon
1756
+ entering or exiting a state. To implement a transition, just call a method
1757
+ either `enter` or `exit`:
1758
+
1759
+ myState: DS.State.create({
1760
+ // Gets called automatically when entering
1761
+ // this state.
1762
+ enter: function(manager) {
1763
+ console.log("Entered myState");
1764
+ }
1765
+ })
1766
+
1767
+ Note that enter and exit events are called once per transition. If the
1768
+ current state changes, but changes to another child state of the parent,
1769
+ the transition event on the parent will not be triggered.
1770
+ */
1771
+
1772
+ var stateProperty = Ember.computed(function(key) {
1773
+ var parent = get(this, 'parentState');
1774
+ if (parent) {
1775
+ return get(parent, key);
1776
+ }
1777
+ }).property();
1778
+
1779
+ var isEmptyObject = function(object) {
1780
+ for (var name in object) {
1781
+ if (object.hasOwnProperty(name)) { return false; }
1782
+ }
1783
+
1784
+ return true;
1785
+ };
1786
+
1787
+ var hasDefinedProperties = function(object) {
1788
+ for (var name in object) {
1789
+ if (object.hasOwnProperty(name) && object[name]) { return true; }
1790
+ }
1791
+
1792
+ return false;
1793
+ };
1794
+
1795
+ DS.State = Ember.State.extend({
1796
+ isLoaded: stateProperty,
1797
+ isDirty: stateProperty,
1798
+ isSaving: stateProperty,
1799
+ isDeleted: stateProperty,
1800
+ isError: stateProperty,
1801
+ isNew: stateProperty,
1802
+ isValid: stateProperty,
1803
+ isPending: stateProperty,
1804
+
1805
+ // For states that are substates of a
1806
+ // DirtyState (updated or created), it is
1807
+ // useful to be able to determine which
1808
+ // type of dirty state it is.
1809
+ dirtyType: stateProperty
1810
+ });
1811
+
1812
+ var setProperty = function(manager, context) {
1813
+ var key = context.key, value = context.value;
1814
+
1815
+ var record = get(manager, 'record'),
1816
+ data = get(record, 'data');
1817
+
1818
+ set(data, key, value);
1819
+ };
1820
+
1821
+ var setAssociation = function(manager, context) {
1822
+ var key = context.key, value = context.value;
1823
+
1824
+ var record = get(manager, 'record'),
1825
+ data = get(record, 'data');
1826
+
1827
+ data.setAssociation(key, value);
1828
+ };
1829
+
1830
+ var didChangeData = function(manager) {
1831
+ var record = get(manager, 'record'),
1832
+ data = get(record, 'data');
1833
+
1834
+ data._savedData = null;
1835
+ record.notifyPropertyChange('data');
1836
+ };
1837
+
1838
+ // The waitingOn event shares common functionality
1839
+ // between the different dirty states, but each is
1840
+ // treated slightly differently. This method is exposed
1841
+ // so that each implementation can invoke the common
1842
+ // behavior, and then implement the behavior specific
1843
+ // to the state.
1844
+ var waitingOn = function(manager, object) {
1845
+ var record = get(manager, 'record'),
1846
+ pendingQueue = get(record, 'pendingQueue'),
1847
+ objectGuid = guidFor(object);
1848
+
1849
+ var observer = function() {
1850
+ if (get(object, 'id')) {
1851
+ manager.send('doneWaitingOn', object);
1852
+ Ember.removeObserver(object, 'id', observer);
1853
+ }
1854
+ };
1855
+
1856
+ pendingQueue[objectGuid] = [object, observer];
1857
+ Ember.addObserver(object, 'id', observer);
1858
+ };
1859
+
1860
+ // Implementation notes:
1861
+ //
1862
+ // Each state has a boolean value for all of the following flags:
1863
+ //
1864
+ // * isLoaded: The record has a populated `data` property. When a
1865
+ // record is loaded via `store.find`, `isLoaded` is false
1866
+ // until the adapter sets it. When a record is created locally,
1867
+ // its `isLoaded` property is always true.
1868
+ // * isDirty: The record has local changes that have not yet been
1869
+ // saved by the adapter. This includes records that have been
1870
+ // created (but not yet saved) or deleted.
1871
+ // * isSaving: The record's transaction has been committed, but
1872
+ // the adapter has not yet acknowledged that the changes have
1873
+ // been persisted to the backend.
1874
+ // * isDeleted: The record was marked for deletion. When `isDeleted`
1875
+ // is true and `isDirty` is true, the record is deleted locally
1876
+ // but the deletion was not yet persisted. When `isSaving` is
1877
+ // true, the change is in-flight. When both `isDirty` and
1878
+ // `isSaving` are false, the change has persisted.
1879
+ // * isError: The adapter reported that it was unable to save
1880
+ // local changes to the backend. This may also result in the
1881
+ // record having its `isValid` property become false if the
1882
+ // adapter reported that server-side validations failed.
1883
+ // * isNew: The record was created on the client and the adapter
1884
+ // did not yet report that it was successfully saved.
1885
+ // * isValid: No client-side validations have failed and the
1886
+ // adapter did not report any server-side validation failures.
1887
+ // * isPending: A record `isPending` when it belongs to an
1888
+ // association on another record and that record has not been
1889
+ // saved. A record in this state cannot be saved because it
1890
+ // lacks a "foreign key" that will be supplied by its parent
1891
+ // association when the parent record has been created. When
1892
+ // the adapter reports that the parent has saved, the
1893
+ // `isPending` property on all children will become `false`
1894
+ // and the transaction will try to commit the records.
1895
+
1896
+ // This mixin is mixed into various uncommitted states. Make
1897
+ // sure to mix it in *after* the class definition, so its
1898
+ // super points to the class definition.
1899
+ var Uncommitted = Ember.Mixin.create({
1900
+ setProperty: setProperty,
1901
+ setAssociation: setAssociation,
1902
+ });
1903
+
1904
+ // These mixins are mixed into substates of the concrete
1905
+ // subclasses of DirtyState.
1906
+
1907
+ var CreatedUncommitted = Ember.Mixin.create({
1908
+ deleteRecord: function(manager) {
1909
+ var record = get(manager, 'record');
1910
+ this._super(manager);
1911
+
1912
+ record.withTransaction(function(t) {
1913
+ t.recordBecameClean('created', record);
1914
+ });
1915
+ manager.goToState('deleted.saved');
1916
+ }
1917
+ });
1918
+
1919
+ var UpdatedUncommitted = Ember.Mixin.create({
1920
+ deleteRecord: function(manager) {
1921
+ this._super(manager);
1922
+
1923
+ var record = get(manager, 'record');
1924
+
1925
+ record.withTransaction(function(t) {
1926
+ t.recordBecameClean('updated', record);
1927
+ });
1928
+
1929
+ manager.goToState('deleted');
1930
+ }
1931
+ });
1932
+
1933
+ // The dirty state is a abstract state whose functionality is
1934
+ // shared between the `created` and `updated` states.
1935
+ //
1936
+ // The deleted state shares the `isDirty` flag with the
1937
+ // subclasses of `DirtyState`, but with a very different
1938
+ // implementation.
1939
+ var DirtyState = DS.State.extend({
1940
+ initialState: 'uncommitted',
1941
+
1942
+ // FLAGS
1943
+ isDirty: true,
1944
+
1945
+ // SUBSTATES
1946
+
1947
+ // When a record first becomes dirty, it is `uncommitted`.
1948
+ // This means that there are local pending changes,
1949
+ // but they have not yet begun to be saved.
1950
+ uncommitted: DS.State.extend({
1951
+ // TRANSITIONS
1952
+ enter: function(manager) {
1953
+ var dirtyType = get(this, 'dirtyType'),
1954
+ record = get(manager, 'record');
1955
+
1956
+ record.withTransaction(function (t) {
1957
+ t.recordBecameDirty(dirtyType, record);
1958
+ });
1959
+ },
1960
+
1961
+ // EVENTS
1962
+ deleteRecord: Ember.K,
1963
+
1964
+ waitingOn: function(manager, object) {
1965
+ waitingOn(manager, object);
1966
+ manager.goToState('pending');
1967
+ },
1968
+
1969
+ willCommit: function(manager) {
1970
+ manager.goToState('inFlight');
1971
+ },
1972
+
1973
+ becameInvalid: function(manager) {
1974
+ var dirtyType = get(this, 'dirtyType'),
1975
+ record = get(manager, 'record');
1976
+
1977
+ record.withTransaction(function (t) {
1978
+ t.recordBecameInFlight(dirtyType, record);
1979
+ });
1980
+
1981
+ manager.goToState('invalid');
1982
+ },
1983
+
1984
+ rollback: function(manager) {
1985
+ var record = get(manager, 'record'),
1986
+ dirtyType = get(this, 'dirtyType'),
1987
+ data = get(record, 'data');
1988
+
1989
+ data.rollback();
1990
+
1991
+ record.withTransaction(function(t) {
1992
+ t.recordBecameClean(dirtyType, record);
1993
+ });
1994
+
1995
+ manager.goToState('loaded');
1996
+ }
1997
+ }, Uncommitted),
1998
+
1999
+ // Once a record has been handed off to the adapter to be
2000
+ // saved, it is in the 'in flight' state. Changes to the
2001
+ // record cannot be made during this window.
2002
+ inFlight: DS.State.extend({
2003
+ // FLAGS
2004
+ isSaving: true,
2005
+
2006
+ // TRANSITIONS
2007
+ enter: function(manager) {
2008
+ var dirtyType = get(this, 'dirtyType'),
2009
+ record = get(manager, 'record');
2010
+
2011
+ record.withTransaction(function (t) {
2012
+ t.recordBecameInFlight(dirtyType, record);
2013
+ });
2014
+ },
2015
+
2016
+ // EVENTS
2017
+ didCommit: function(manager) {
2018
+ var dirtyType = get(this, 'dirtyType'),
2019
+ record = get(manager, 'record');
2020
+
2021
+ record.withTransaction(function(t) {
2022
+ t.recordBecameClean('inflight', record);
2023
+ });
2024
+
2025
+ manager.goToState('loaded');
2026
+ manager.send('invokeLifecycleCallbacks', dirtyType);
2027
+ },
2028
+
2029
+ becameInvalid: function(manager, errors) {
2030
+ var record = get(manager, 'record');
2031
+
2032
+ set(record, 'errors', errors);
2033
+
2034
+ manager.goToState('invalid');
2035
+ manager.send('invokeLifecycleCallbacks');
2036
+ },
2037
+
2038
+ becameError: function(manager) {
2039
+ manager.goToState('error');
2040
+ manager.send('invokeLifecycleCallbacks');
2041
+ },
2042
+
2043
+ didChangeData: didChangeData
2044
+ }),
2045
+
2046
+ // If a record becomes associated with a newly created
2047
+ // parent record, it will be `pending` until the parent
2048
+ // record has successfully persisted. Once this happens,
2049
+ // this record can use the parent's primary key as its
2050
+ // foreign key.
2051
+ //
2052
+ // If the record's transaction had already started to
2053
+ // commit, the record will transition to the `inFlight`
2054
+ // state. If it had not, the record will transition to
2055
+ // the `uncommitted` state.
2056
+ pending: DS.State.extend({
2057
+ initialState: 'uncommitted',
2058
+
2059
+ // FLAGS
2060
+ isPending: true,
2061
+
2062
+ // SUBSTATES
2063
+
2064
+ // A pending record whose transaction has not yet
2065
+ // started to commit is in this state.
2066
+ uncommitted: DS.State.extend({
2067
+ // EVENTS
2068
+ deleteRecord: function(manager) {
2069
+ var record = get(manager, 'record'),
2070
+ pendingQueue = get(record, 'pendingQueue'),
2071
+ tuple;
2072
+
2073
+ // since we are leaving the pending state, remove any
2074
+ // observers we have registered on other records.
2075
+ for (var prop in pendingQueue) {
2076
+ if (!pendingQueue.hasOwnProperty(prop)) { continue; }
2077
+
2078
+ tuple = pendingQueue[prop];
2079
+ Ember.removeObserver(tuple[0], 'id', tuple[1]);
2080
+ }
2081
+ },
2082
+
2083
+ willCommit: function(manager) {
2084
+ manager.goToState('committing');
2085
+ },
2086
+
2087
+ doneWaitingOn: function(manager, object) {
2088
+ var record = get(manager, 'record'),
2089
+ pendingQueue = get(record, 'pendingQueue'),
2090
+ objectGuid = guidFor(object);
2091
+
2092
+ delete pendingQueue[objectGuid];
2093
+
2094
+ if (isEmptyObject(pendingQueue)) {
2095
+ manager.send('doneWaiting');
2096
+ }
2097
+ },
2098
+
2099
+ doneWaiting: function(manager) {
2100
+ var dirtyType = get(this, 'dirtyType');
2101
+ manager.goToState(dirtyType + '.uncommitted');
2102
+ }
2103
+ }, Uncommitted),
2104
+
2105
+ // A pending record whose transaction has started
2106
+ // to commit is in this state. Since it has not yet
2107
+ // been sent to the adapter, it is not `inFlight`
2108
+ // until all of its dependencies have been committed.
2109
+ committing: DS.State.extend({
2110
+ // FLAGS
2111
+ isSaving: true,
2112
+
2113
+ // EVENTS
2114
+ doneWaitingOn: function(manager, object) {
2115
+ var record = get(manager, 'record'),
2116
+ pendingQueue = get(record, 'pendingQueue'),
2117
+ objectGuid = guidFor(object);
2118
+
2119
+ delete pendingQueue[objectGuid];
2120
+
2121
+ if (isEmptyObject(pendingQueue)) {
2122
+ manager.send('doneWaiting');
2123
+ }
2124
+ },
2125
+
2126
+ doneWaiting: function(manager) {
2127
+ var record = get(manager, 'record'),
2128
+ transaction = get(record, 'transaction');
2129
+
2130
+ // Now that the record is no longer pending, schedule
2131
+ // the transaction to commit.
2132
+ Ember.run.once(transaction, transaction.commit);
2133
+ },
2134
+
2135
+ willCommit: function(manager) {
2136
+ var record = get(manager, 'record'),
2137
+ pendingQueue = get(record, 'pendingQueue');
2138
+
2139
+ if (isEmptyObject(pendingQueue)) {
2140
+ var dirtyType = get(this, 'dirtyType');
2141
+ manager.goToState(dirtyType + '.inFlight');
2142
+ }
2143
+ }
2144
+ })
2145
+ }),
2146
+
2147
+ // A record is in the `invalid` state when its client-side
2148
+ // invalidations have failed, or if the adapter has indicated
2149
+ // the the record failed server-side invalidations.
2150
+ invalid: DS.State.extend({
2151
+ // FLAGS
2152
+ isValid: false,
2153
+
2154
+ exit: function(manager) {
2155
+ var record = get(manager, 'record');
2156
+
2157
+ record.withTransaction(function (t) {
2158
+ t.recordBecameClean('inflight', record);
2159
+ });
2160
+ },
2161
+
2162
+ // EVENTS
2163
+ deleteRecord: function(manager) {
2164
+ manager.goToState('deleted');
2165
+ },
2166
+
2167
+ setAssociation: setAssociation,
2168
+
2169
+ setProperty: function(manager, context) {
2170
+ setProperty(manager, context);
2171
+
2172
+ var record = get(manager, 'record'),
2173
+ errors = get(record, 'errors'),
2174
+ key = context.key;
2175
+
2176
+ delete errors[key];
2177
+
2178
+ if (!hasDefinedProperties(errors)) {
2179
+ manager.send('becameValid');
2180
+ }
2181
+ },
2182
+
2183
+ rollback: function(manager) {
2184
+ manager.send('becameValid');
2185
+ manager.send('rollback');
2186
+ },
2187
+
2188
+ becameValid: function(manager) {
2189
+ manager.goToState('uncommitted');
2190
+ },
2191
+
2192
+ invokeLifecycleCallbacks: function(manager) {
2193
+ var record = get(manager, 'record');
2194
+ record.fire('becameInvalid', record);
2195
+ }
2196
+ })
2197
+ });
2198
+
2199
+ // The created and updated states are created outside the state
2200
+ // chart so we can reopen their substates and add mixins as
2201
+ // necessary.
2202
+
2203
+ var createdState = DirtyState.create({
2204
+ dirtyType: 'created',
2205
+
2206
+ // FLAGS
2207
+ isNew: true
2208
+ });
2209
+
2210
+ var updatedState = DirtyState.create({
2211
+ dirtyType: 'updated'
2212
+ });
2213
+
2214
+ // The created.uncommitted state and created.pending.uncommitted share
2215
+ // some logic defined in CreatedUncommitted.
2216
+ createdState.states.uncommitted.reopen(CreatedUncommitted);
2217
+ createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
2218
+
2219
+ // The created.uncommitted state needs to immediately transition to the
2220
+ // deleted state if it is rolled back.
2221
+ createdState.states.uncommitted.reopen({
2222
+ rollback: function(manager) {
2223
+ this._super(manager);
2224
+ manager.goToState('deleted.saved');
2225
+ }
2226
+ });
2227
+
2228
+ // The updated.uncommitted state and updated.pending.uncommitted share
2229
+ // some logic defined in UpdatedUncommitted.
2230
+ updatedState.states.uncommitted.reopen(UpdatedUncommitted);
2231
+ updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
2232
+ updatedState.states.inFlight.reopen({
2233
+ didSaveData: function(manager) {
2234
+ var record = get(manager, 'record'),
2235
+ data = get(record, 'data');
2236
+
2237
+ data.saveData();
2238
+ data.adapterDidUpdate();
2239
+ }
2240
+ });
2241
+
2242
+ var states = {
2243
+ rootState: Ember.State.create({
2244
+ // FLAGS
2245
+ isLoaded: false,
2246
+ isDirty: false,
2247
+ isSaving: false,
2248
+ isDeleted: false,
2249
+ isError: false,
2250
+ isNew: false,
2251
+ isValid: true,
2252
+ isPending: false,
2253
+
2254
+ // SUBSTATES
2255
+
2256
+ // A record begins its lifecycle in the `empty` state.
2257
+ // If its data will come from the adapter, it will
2258
+ // transition into the `loading` state. Otherwise, if
2259
+ // the record is being created on the client, it will
2260
+ // transition into the `created` state.
2261
+ empty: DS.State.create({
2262
+ // EVENTS
2263
+ loadingData: function(manager) {
2264
+ manager.goToState('loading');
2265
+ },
2266
+
2267
+ didChangeData: function(manager) {
2268
+ didChangeData(manager);
2269
+
2270
+ manager.goToState('loaded.created');
2271
+ }
2272
+ }),
2273
+
2274
+ // A record enters this state when the store askes
2275
+ // the adapter for its data. It remains in this state
2276
+ // until the adapter provides the requested data.
2277
+ //
2278
+ // Usually, this process is asynchronous, using an
2279
+ // XHR to retrieve the data.
2280
+ loading: DS.State.create({
2281
+ // TRANSITIONS
2282
+ exit: function(manager) {
2283
+ var record = get(manager, 'record');
2284
+ record.fire('didLoad');
2285
+ },
2286
+
2287
+ // EVENTS
2288
+ didChangeData: function(manager, data) {
2289
+ didChangeData(manager);
2290
+ manager.send('loadedData');
2291
+ },
2292
+
2293
+ loadedData: function(manager) {
2294
+ manager.goToState('loaded');
2295
+ }
2296
+ }),
2297
+
2298
+ // A record enters this state when its data is populated.
2299
+ // Most of a record's lifecycle is spent inside substates
2300
+ // of the `loaded` state.
2301
+ loaded: DS.State.create({
2302
+ initialState: 'saved',
2303
+
2304
+ // FLAGS
2305
+ isLoaded: true,
2306
+
2307
+ // SUBSTATES
2308
+
2309
+ // If there are no local changes to a record, it remains
2310
+ // in the `saved` state.
2311
+ saved: DS.State.create({
2312
+
2313
+ // EVENTS
2314
+ setProperty: function(manager, context) {
2315
+ setProperty(manager, context);
2316
+ manager.goToState('updated');
2317
+ },
2318
+
2319
+ setAssociation: function(manager, context) {
2320
+ setAssociation(manager, context);
2321
+ manager.goToState('updated');
2322
+ },
2323
+
2324
+ didChangeData: didChangeData,
2325
+
2326
+ deleteRecord: function(manager) {
2327
+ manager.goToState('deleted');
2328
+ },
2329
+
2330
+ waitingOn: function(manager, object) {
2331
+ waitingOn(manager, object);
2332
+ manager.goToState('updated.pending');
2333
+ },
2334
+
2335
+ invokeLifecycleCallbacks: function(manager, dirtyType) {
2336
+ var record = get(manager, 'record');
2337
+ if (dirtyType === 'created') {
2338
+ record.fire('didCreate', record);
2339
+ } else {
2340
+ record.fire('didUpdate', record);
2341
+ }
2342
+ }
2343
+ }),
2344
+
2345
+ // A record is in this state after it has been locally
2346
+ // created but before the adapter has indicated that
2347
+ // it has been saved.
2348
+ created: createdState,
2349
+
2350
+ // A record is in this state if it has already been
2351
+ // saved to the server, but there are new local changes
2352
+ // that have not yet been saved.
2353
+ updated: updatedState
2354
+ }),
2355
+
2356
+ // A record is in this state if it was deleted from the store.
2357
+ deleted: DS.State.create({
2358
+ // FLAGS
2359
+ isDeleted: true,
2360
+ isLoaded: true,
2361
+ isDirty: true,
2362
+
2363
+ // TRANSITIONS
2364
+ enter: function(manager) {
2365
+ var record = get(manager, 'record'),
2366
+ store = get(record, 'store');
2367
+
2368
+ store.removeFromRecordArrays(record);
2369
+ },
2370
+
2371
+ // SUBSTATES
2372
+
2373
+ // When a record is deleted, it enters the `start`
2374
+ // state. It will exit this state when the record's
2375
+ // transaction starts to commit.
2376
+ start: DS.State.create({
2377
+ // TRANSITIONS
2378
+ enter: function(manager) {
2379
+ var record = get(manager, 'record');
2380
+
2381
+ record.withTransaction(function(t) {
2382
+ t.recordBecameDirty('deleted', record);
2383
+ });
2384
+ },
2385
+
2386
+ // EVENTS
2387
+ willCommit: function(manager) {
2388
+ manager.goToState('inFlight');
2389
+ },
2390
+
2391
+ rollback: function(manager) {
2392
+ var record = get(manager, 'record'),
2393
+ data = get(record, 'data');
2394
+
2395
+ data.rollback();
2396
+ record.withTransaction(function(t) {
2397
+ t.recordBecameClean('deleted', record);
2398
+ });
2399
+ manager.goToState('loaded');
2400
+ }
2401
+ }),
2402
+
2403
+ // After a record's transaction is committing, but
2404
+ // before the adapter indicates that the deletion
2405
+ // has saved to the server, a record is in the
2406
+ // `inFlight` substate of `deleted`.
2407
+ inFlight: DS.State.create({
2408
+ // FLAGS
2409
+ isSaving: true,
2410
+
2411
+ // TRANSITIONS
2412
+ enter: function(manager) {
2413
+ var record = get(manager, 'record');
2414
+
2415
+ record.withTransaction(function (t) {
2416
+ t.recordBecameInFlight('deleted', record);
2417
+ });
2418
+ },
2419
+
2420
+ // EVENTS
2421
+ didCommit: function(manager) {
2422
+ var record = get(manager, 'record');
2423
+
2424
+ record.withTransaction(function(t) {
2425
+ t.recordBecameClean('inflight', record);
2426
+ });
2427
+
2428
+ manager.goToState('saved');
2429
+
2430
+ manager.send('invokeLifecycleCallbacks');
2431
+ }
2432
+ }),
2433
+
2434
+ // Once the adapter indicates that the deletion has
2435
+ // been saved, the record enters the `saved` substate
2436
+ // of `deleted`.
2437
+ saved: DS.State.create({
2438
+ // FLAGS
2439
+ isDirty: false,
2440
+
2441
+ invokeLifecycleCallbacks: function(manager) {
2442
+ var record = get(manager, 'record');
2443
+ record.fire('didDelete', record);
2444
+ }
2445
+ })
2446
+ }),
2447
+
2448
+ // If the adapter indicates that there was an unknown
2449
+ // error saving a record, the record enters the `error`
2450
+ // state.
2451
+ error: DS.State.create({
2452
+ isError: true,
2453
+
2454
+ // EVENTS
2455
+
2456
+ invokeLifecycleCallbacks: function(manager) {
2457
+ var record = get(manager, 'record');
2458
+ record.fire('becameError', record);
2459
+ }
2460
+ })
2461
+ })
2462
+ };
2463
+
2464
+ DS.StateManager = Ember.StateManager.extend({
2465
+ record: null,
2466
+ initialState: 'rootState',
2467
+ states: states
2468
+ });
2469
+
2470
+ })();
2471
+
2472
+
2473
+
2474
+ (function() {
2475
+ var get = Ember.get, set = Ember.set;
2476
+
2477
+ // When a record is changed on the client, it is considered "dirty"--there are
2478
+ // pending changes that need to be saved to a persistence layer, such as a
2479
+ // server.
2480
+ //
2481
+ // If the record is rolled back, it re-enters a clean state, any changes are
2482
+ // discarded, and its attributes are reset back to the last known good copy
2483
+ // of the data that came from the server.
2484
+ //
2485
+ // If the record is committed, the changes are sent to the server to be saved,
2486
+ // and once the server confirms that they are valid, the record's "canonical"
2487
+ // data becomes the original canonical data plus the changes merged in.
2488
+ //
2489
+ // A DataProxy is an object that encapsulates this change tracking. It
2490
+ // contains three buckets:
2491
+ //
2492
+ // * `savedData` - the last-known copy of the data from the server
2493
+ // * `unsavedData` - a hash that contains any changes that have not yet
2494
+ // been committed
2495
+ // * `associations` - this is similar to `savedData`, but holds the client
2496
+ // ids of associated records
2497
+ //
2498
+ // When setting a property on the object, the value is placed into the
2499
+ // `unsavedData` bucket:
2500
+ //
2501
+ // proxy.set('key', 'value');
2502
+ //
2503
+ // // unsavedData:
2504
+ // {
2505
+ // key: "value"
2506
+ // }
2507
+ //
2508
+ // When retrieving a property from the object, it first looks to see
2509
+ // if that value exists in the `unsavedData` bucket, and returns it if so.
2510
+ // Otherwise, it returns the value from the `savedData` bucket.
2511
+ //
2512
+ // When the adapter notifies a record that it has been saved, it merges the
2513
+ // `unsavedData` bucket into the `savedData` bucket. If the record's
2514
+ // transaction is rolled back, the `unsavedData` hash is simply discarded.
2515
+ //
2516
+ // This object is a regular JS object for performance. It is only
2517
+ // used internally for bookkeeping purposes.
2518
+
2519
+ var DataProxy = DS._DataProxy = function(record) {
2520
+ this.record = record;
2521
+
2522
+ this.unsavedData = {};
2523
+
2524
+ this.associations = {};
2525
+ };
2526
+
2527
+ DataProxy.prototype = {
2528
+ get: function(key) { return Ember.get(this, key); },
2529
+ set: function(key, value) { return Ember.set(this, key, value); },
2530
+
2531
+ setAssociation: function(key, value) {
2532
+ this.associations[key] = value;
2533
+ },
2534
+
2535
+ savedData: function() {
2536
+ var savedData = this._savedData;
2537
+ if (savedData) { return savedData; }
2538
+
2539
+ var record = this.record,
2540
+ clientId = get(record, 'clientId'),
2541
+ store = get(record, 'store');
2542
+
2543
+ if (store) {
2544
+ savedData = store.dataForRecord(record);
2545
+ this._savedData = savedData;
2546
+ return savedData;
2547
+ }
2548
+ },
2549
+
2550
+ unknownProperty: function(key) {
2551
+ var unsavedData = this.unsavedData,
2552
+ associations = this.associations,
2553
+ savedData = this.savedData(),
2554
+ store;
2555
+
2556
+ var value = unsavedData[key], association;
2557
+
2558
+ // if this is a belongsTo association, this will
2559
+ // be a clientId.
2560
+ association = associations[key];
2561
+
2562
+ if (association !== undefined) {
2563
+ store = get(this.record, 'store');
2564
+ return store.clientIdToId[association];
2565
+ }
2566
+
2567
+ if (savedData && value === undefined) {
2568
+ value = savedData[key];
2569
+ }
2570
+
2571
+ return value;
2572
+ },
2573
+
2574
+ setUnknownProperty: function(key, value) {
2575
+ var record = this.record,
2576
+ unsavedData = this.unsavedData;
2577
+
2578
+ unsavedData[key] = value;
2579
+
2580
+ record.hashWasUpdated();
2581
+
2582
+ return value;
2583
+ },
2584
+
2585
+ commit: function() {
2586
+ this.saveData();
2587
+
2588
+ this.record.notifyPropertyChange('data');
2589
+ },
2590
+
2591
+ rollback: function() {
2592
+ this.unsavedData = {};
2593
+
2594
+ this.record.notifyPropertyChange('data');
2595
+ },
2596
+
2597
+ saveData: function() {
2598
+ var record = this.record;
2599
+
2600
+ var unsavedData = this.unsavedData;
2601
+ var savedData = this.savedData();
2602
+
2603
+ for (var prop in unsavedData) {
2604
+ if (unsavedData.hasOwnProperty(prop)) {
2605
+ savedData[prop] = unsavedData[prop];
2606
+ delete unsavedData[prop];
2607
+ }
2608
+ }
2609
+ },
2610
+
2611
+ adapterDidUpdate: function() {
2612
+ this.unsavedData = {};
2613
+ }
2614
+ };
2615
+
2616
+ })();
2617
+
2618
+
2619
+
2620
+ (function() {
2621
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none;
2622
+
2623
+ var retrieveFromCurrentState = Ember.computed(function(key) {
2624
+ return get(getPath(this, 'stateManager.currentState'), key);
2625
+ }).property('stateManager.currentState').cacheable();
2626
+
2627
+ DS.Model = Ember.Object.extend(Ember.Evented, {
2628
+ isLoaded: retrieveFromCurrentState,
2629
+ isDirty: retrieveFromCurrentState,
2630
+ isSaving: retrieveFromCurrentState,
2631
+ isDeleted: retrieveFromCurrentState,
2632
+ isError: retrieveFromCurrentState,
2633
+ isNew: retrieveFromCurrentState,
2634
+ isPending: retrieveFromCurrentState,
2635
+ isValid: retrieveFromCurrentState,
2636
+
2637
+ clientId: null,
2638
+ transaction: null,
2639
+ stateManager: null,
2640
+ pendingQueue: null,
2641
+ errors: null,
2642
+
2643
+ // because unknownProperty is used, any internal property
2644
+ // must be initialized here.
2645
+ primaryKey: 'id',
2646
+ id: Ember.computed(function(key, value) {
2647
+ var primaryKey = get(this, 'primaryKey'),
2648
+ data = get(this, 'data');
2649
+
2650
+ if (arguments.length === 2) {
2651
+ set(data, primaryKey, value);
2652
+ return value;
2653
+ }
2654
+
2655
+ var id = get(data, primaryKey);
2656
+ return id ? id : this._id;
2657
+ }).property('primaryKey', 'data'),
2658
+
2659
+ // The following methods are callbacks invoked by `toJSON`. You
2660
+ // can override one of the callbacks to override specific behavior,
2661
+ // or toJSON itself.
2662
+ //
2663
+ // If you override toJSON, you can invoke these callbacks manually
2664
+ // to get the default behavior.
2665
+
2666
+ /**
2667
+ Add the record's primary key to the JSON hash.
2668
+
2669
+ The default implementation uses the record's specified `primaryKey`
2670
+ and the `id` computed property, which are passed in as parameters.
2671
+
2672
+ @param {Object} json the JSON hash being built
2673
+ @param {Number|String} id the record's id
2674
+ @param {String} key the primaryKey for the record
2675
+ */
2676
+ addIdToJSON: function(json, id, key) {
2677
+ if (id) { json[key] = id; }
2678
+ },
2679
+
2680
+ /**
2681
+ Add the attributes' current values to the JSON hash.
2682
+
2683
+ The default implementation gets the current value of each
2684
+ attribute from the `data`, and uses a `defaultValue` if
2685
+ specified in the `DS.attr` definition.
2686
+
2687
+ @param {Object} json the JSON hash being build
2688
+ @param {Ember.Map} attributes a Map of attributes
2689
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2690
+ */
2691
+ addAttributesToJSON: function(json, attributes, data) {
2692
+ attributes.forEach(function(name, meta) {
2693
+ var key = meta.key(this.constructor),
2694
+ value = get(data, key);
2695
+
2696
+ if (value === undefined) {
2697
+ value = meta.options.defaultValue;
2698
+ }
2699
+
2700
+ json[key] = value;
2701
+ }, this);
2702
+ },
2703
+
2704
+ /**
2705
+ Add the value of a `hasMany` association to the JSON hash.
2706
+
2707
+ The default implementation honors the `embedded` option
2708
+ passed to `DS.hasMany`. If embedded, `toJSON` is recursively
2709
+ called on the child records. If not, the `id` of each
2710
+ record is added.
2711
+
2712
+ Note that if a record is not embedded and does not
2713
+ yet have an `id` (usually provided by the server), it
2714
+ will not be included in the output.
2715
+
2716
+ @param {Object} json the JSON hash being built
2717
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2718
+ @param {Object} meta information about the association
2719
+ @param {Object} options options passed to `toJSON`
2720
+ */
2721
+ addHasManyToJSON: function(json, data, meta, options) {
2722
+ var key = meta.key,
2723
+ manyArray = get(this, key),
2724
+ records = [], i, l,
2725
+ clientId, id;
2726
+
2727
+ if (meta.options.embedded) {
2728
+ // TODO: Avoid materializing embedded hashes if possible
2729
+ manyArray.forEach(function(record) {
2730
+ records.push(record.toJSON(options));
2731
+ });
2732
+ } else {
2733
+ var clientIds = get(manyArray, 'content');
2734
+
2735
+ for (i=0, l=clientIds.length; i<l; i++) {
2736
+ clientId = clientIds[i];
2737
+ id = get(this, 'store').clientIdToId[clientId];
2738
+
2739
+ if (id !== undefined) {
2740
+ records.push(id);
2741
+ }
2742
+ }
2743
+ }
2744
+
2745
+ key = meta.options.key || get(this, 'namingConvention').keyToJSONKey(key);
2746
+ json[key] = records;
2747
+ },
2748
+
2749
+ /**
2750
+ Add the value of a `belongsTo` association to the JSON hash.
2751
+
2752
+ The default implementation always includes the `id`.
2753
+
2754
+ @param {Object} json the JSON hash being built
2755
+ @param {DataProxy} data the record's data, accessed with `get` and `set`.
2756
+ @param {Object} meta information about the association
2757
+ @param {Object} options options passed to `toJSON`
2758
+ */
2759
+ addBelongsToToJSON: function(json, data, meta, options) {
2760
+ var key = meta.key, value, id;
2761
+
2762
+ if (meta.options.embedded) {
2763
+ key = meta.options.key || get(this, 'namingConvention').keyToJSONKey(key);
2764
+ value = get(data.record, key);
2765
+ json[key] = value ? value.toJSON(options) : null;
2766
+ } else {
2767
+ key = meta.options.key || get(this, 'namingConvention').foreignKey(key);
2768
+ id = data.get(key);
2769
+ json[key] = none(id) ? null : id;
2770
+ }
2771
+ },
2772
+ /**
2773
+ Create a JSON representation of the record, including its `id`,
2774
+ attributes and associations. Honor any settings defined on the
2775
+ attributes or associations (such as `embedded` or `key`).
2776
+ */
2777
+ toJSON: function(options) {
2778
+ var data = get(this, 'data'),
2779
+ result = {},
2780
+ type = this.constructor,
2781
+ attributes = get(type, 'attributes'),
2782
+ primaryKey = get(this, 'primaryKey'),
2783
+ id = get(this, 'id'),
2784
+ store = get(this, 'store'),
2785
+ associations;
2786
+
2787
+ options = options || {};
2788
+
2789
+ // delegate to `addIdToJSON` callback
2790
+ this.addIdToJSON(result, id, primaryKey);
2791
+
2792
+ // delegate to `addAttributesToJSON` callback
2793
+ this.addAttributesToJSON(result, attributes, data);
2794
+
2795
+ associations = get(type, 'associationsByName');
2796
+
2797
+ // add associations, delegating to `addHasManyToJSON` and
2798
+ // `addBelongsToToJSON`.
2799
+ associations.forEach(function(key, meta) {
2800
+ if (options.associations && meta.kind === 'hasMany') {
2801
+ this.addHasManyToJSON(result, data, meta, options);
2802
+ } else if (meta.kind === 'belongsTo') {
2803
+ this.addBelongsToToJSON(result, data, meta, options);
2804
+ }
2805
+ }, this);
2806
+
2807
+ return result;
2808
+ },
2809
+
2810
+ data: Ember.computed(function() {
2811
+ return new DS._DataProxy(this);
2812
+ }).cacheable(),
2813
+
2814
+ didLoad: Ember.K,
2815
+ didUpdate: Ember.K,
2816
+ didCreate: Ember.K,
2817
+ didDelete: Ember.K,
2818
+ becameInvalid: Ember.K,
2819
+ becameError: Ember.K,
2820
+
2821
+ init: function() {
2822
+ var stateManager = DS.StateManager.create({
2823
+ record: this
2824
+ });
2825
+
2826
+ set(this, 'pendingQueue', {});
2827
+
2828
+ set(this, 'stateManager', stateManager);
2829
+ stateManager.goToState('empty');
2830
+ },
2831
+
2832
+ destroy: function() {
2833
+ if (!get(this, 'isDeleted')) {
2834
+ this.deleteRecord();
2835
+ }
2836
+ this._super();
2837
+ },
2838
+
2839
+ send: function(name, context) {
2840
+ return get(this, 'stateManager').send(name, context);
2841
+ },
2842
+
2843
+ withTransaction: function(fn) {
2844
+ var transaction = get(this, 'transaction');
2845
+ if (transaction) { fn(transaction); }
2846
+ },
2847
+
2848
+ setProperty: function(key, value) {
2849
+ this.send('setProperty', { key: key, value: value });
2850
+ },
2851
+
2852
+ deleteRecord: function() {
2853
+ this.send('deleteRecord');
2854
+ },
2855
+
2856
+ waitingOn: function(record) {
2857
+ this.send('waitingOn', record);
2858
+ },
2859
+
2860
+ notifyHashWasUpdated: function() {
2861
+ var store = get(this, 'store');
2862
+ if (store) {
2863
+ store.hashWasUpdated(this.constructor, get(this, 'clientId'), this);
2864
+ }
2865
+ },
2866
+
2867
+ unknownProperty: function(key) {
2868
+ var data = get(this, 'data');
2869
+
2870
+ if (data && key in data) {
2871
+ Ember.assert("You attempted to access the " + key + " property on a record without defining an attribute.", false);
2872
+ }
2873
+ },
2874
+
2875
+ setUnknownProperty: function(key, value) {
2876
+ var data = get(this, 'data');
2877
+
2878
+ if (data && key in data) {
2879
+ Ember.assert("You attempted to set the " + key + " property on a record without defining an attribute.", false);
2880
+ } else {
2881
+ return this._super(key, value);
2882
+ }
2883
+ },
2884
+
2885
+ namingConvention: {
2886
+ keyToJSONKey: function(key) {
2887
+ // TODO: Strip off `is` from the front. Example: `isHipster` becomes `hipster`
2888
+ return Ember.String.decamelize(key);
2889
+ },
2890
+
2891
+ foreignKey: function(key) {
2892
+ return Ember.String.decamelize(key) + '_id';
2893
+ }
2894
+ },
2895
+
2896
+ /** @private */
2897
+ hashWasUpdated: function() {
2898
+ // At the end of the run loop, notify record arrays that
2899
+ // this record has changed so they can re-evaluate its contents
2900
+ // to determine membership.
2901
+ Ember.run.once(this, this.notifyHashWasUpdated);
2902
+ },
2903
+
2904
+ dataDidChange: Ember.observer(function() {
2905
+ var associations = get(this.constructor, 'associationsByName'),
2906
+ data = get(this, 'data'), store = get(this, 'store'),
2907
+ idToClientId = store.idToClientId,
2908
+ cachedValue;
2909
+
2910
+ associations.forEach(function(name, association) {
2911
+ if (association.kind === 'hasMany') {
2912
+ cachedValue = this.cacheFor(name);
2913
+
2914
+ if (cachedValue) {
2915
+ var key = association.options.key || name,
2916
+ ids = data.get(key) || [];
2917
+ var clientIds = Ember.ArrayUtils.map(ids, function(id) {
2918
+ return store.clientIdForId(association.type, id);
2919
+ });
2920
+
2921
+ set(cachedValue, 'content', Ember.A(clientIds));
2922
+ cachedValue.fetch();
2923
+ }
2924
+ }
2925
+ }, this);
2926
+ }, 'data'),
2927
+
2928
+ /**
2929
+ @private
2930
+
2931
+ Override the default event firing from Ember.Evented to
2932
+ also call methods with the given name.
2933
+ */
2934
+ fire: function(name) {
2935
+ this[name].apply(this, [].slice.call(arguments, 1));
2936
+ this._super.apply(this, arguments);
2937
+ }
2938
+ });
2939
+
2940
+ // Helper function to generate store aliases.
2941
+ // This returns a function that invokes the named alias
2942
+ // on the default store, but injects the class as the
2943
+ // first parameter.
2944
+ var storeAlias = function(methodName) {
2945
+ return function() {
2946
+ var store = get(DS, 'defaultStore'),
2947
+ args = [].slice.call(arguments);
2948
+
2949
+ args.unshift(this);
2950
+ return store[methodName].apply(store, args);
2951
+ };
2952
+ };
2953
+
2954
+ DS.Model.reopenClass({
2955
+ find: storeAlias('find'),
2956
+ filter: storeAlias('filter'),
2957
+
2958
+ _create: DS.Model.create,
2959
+
2960
+ create: function() {
2961
+ throw new Ember.Error("You should not call `create` on a model. Instead, call `createRecord` with the attributes you would like to set.");
2962
+ },
2963
+
2964
+ createRecord: storeAlias('createRecord')
2965
+ });
2966
+
2967
+ })();
2968
+
2969
+
2970
+
2971
+ (function() {
2972
+ var get = Ember.get, getPath = Ember.getPath;
2973
+ DS.Model.reopenClass({
2974
+ attributes: Ember.computed(function() {
2975
+ var map = Ember.Map.create();
2976
+
2977
+ this.eachComputedProperty(function(name, meta) {
2978
+ if (meta.isAttribute) { map.set(name, meta); }
2979
+ });
2980
+
2981
+ return map;
2982
+ }).cacheable(),
2983
+
2984
+ processAttributeKeys: function() {
2985
+ if (this.processedAttributeKeys) { return; }
2986
+
2987
+ var namingConvention = this.proto().namingConvention;
2988
+
2989
+ this.eachComputedProperty(function(name, meta) {
2990
+ if (meta.isAttribute && !meta.options.key) {
2991
+ meta.options.key = namingConvention.keyToJSONKey(name, this);
2992
+ }
2993
+ }, this);
2994
+ }
2995
+ });
2996
+
2997
+ DS.attr = function(type, options) {
2998
+ var transform = DS.attr.transforms[type];
2999
+ Ember.assert("Could not find model attribute of type " + type, !!transform);
3000
+
3001
+ var transformFrom = transform.from;
3002
+ var transformTo = transform.to;
3003
+
3004
+ options = options || {};
3005
+
3006
+ var meta = {
3007
+ type: type,
3008
+ isAttribute: true,
3009
+ options: options,
3010
+
3011
+ // this will ensure that the key always takes naming
3012
+ // conventions into consideration.
3013
+ key: function(recordType) {
3014
+ recordType.processAttributeKeys();
3015
+ return options.key;
3016
+ }
3017
+ };
3018
+
3019
+ return Ember.computed(function(key, value) {
3020
+ var data;
3021
+
3022
+ key = meta.key(this.constructor);
3023
+
3024
+ if (arguments.length === 2) {
3025
+ value = transformTo(value);
3026
+ this.setProperty(key, value);
3027
+ } else {
3028
+ data = get(this, 'data');
3029
+ value = get(data, key);
3030
+
3031
+ if (value === undefined) {
3032
+ value = options.defaultValue;
3033
+ }
3034
+ }
3035
+
3036
+ return transformFrom(value);
3037
+ // `data` is never set directly. However, it may be
3038
+ // invalidated from the state manager's setData
3039
+ // event.
3040
+ }).property('data').cacheable().meta(meta);
3041
+ };
3042
+
3043
+ DS.attr.transforms = {
3044
+ string: {
3045
+ from: function(serialized) {
3046
+ return Ember.none(serialized) ? null : String(serialized);
3047
+ },
3048
+
3049
+ to: function(deserialized) {
3050
+ return Ember.none(deserialized) ? null : String(deserialized);
3051
+ }
3052
+ },
3053
+
3054
+ number: {
3055
+ from: function(serialized) {
3056
+ return Ember.none(serialized) ? null : Number(serialized);
3057
+ },
3058
+
3059
+ to: function(deserialized) {
3060
+ return Ember.none(deserialized) ? null : Number(deserialized);
3061
+ }
3062
+ },
3063
+
3064
+ 'boolean': {
3065
+ from: function(serialized) {
3066
+ return Boolean(serialized);
3067
+ },
3068
+
3069
+ to: function(deserialized) {
3070
+ return Boolean(deserialized);
3071
+ }
3072
+ },
3073
+
3074
+ date: {
3075
+ from: function(serialized) {
3076
+ var type = typeof serialized;
3077
+
3078
+ if (type === "string" || type === "number") {
3079
+ return new Date(serialized);
3080
+ } else if (serialized === null || serialized === undefined) {
3081
+ // if the value is not present in the data,
3082
+ // return undefined, not null.
3083
+ return serialized;
3084
+ } else {
3085
+ return null;
3086
+ }
3087
+ },
3088
+
3089
+ to: function(date) {
3090
+ if (date instanceof Date) {
3091
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3092
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
3093
+
3094
+ var pad = function(num) {
3095
+ return num < 10 ? "0"+num : ""+num;
3096
+ };
3097
+
3098
+ var utcYear = date.getUTCFullYear(),
3099
+ utcMonth = date.getUTCMonth(),
3100
+ utcDayOfMonth = date.getUTCDate(),
3101
+ utcDay = date.getUTCDay(),
3102
+ utcHours = date.getUTCHours(),
3103
+ utcMinutes = date.getUTCMinutes(),
3104
+ utcSeconds = date.getUTCSeconds();
3105
+
3106
+
3107
+ var dayOfWeek = days[utcDay];
3108
+ var dayOfMonth = pad(utcDayOfMonth);
3109
+ var month = months[utcMonth];
3110
+
3111
+ return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " +
3112
+ pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT";
3113
+ } else if (date === undefined) {
3114
+ return undefined;
3115
+ } else {
3116
+ return null;
3117
+ }
3118
+ }
3119
+ }
3120
+ };
3121
+
3122
+
3123
+ })();
3124
+
3125
+
3126
+
3127
+ (function() {
3128
+
3129
+ })();
3130
+
3131
+
3132
+
3133
+ (function() {
3134
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath,
3135
+ none = Ember.none;
3136
+
3137
+ var embeddedFindRecord = function(store, type, data, key, one) {
3138
+ var association = get(data, key);
3139
+ return none(association) ? undefined : store.load(type, association).id;
3140
+ };
3141
+
3142
+ var referencedFindRecord = function(store, type, data, key, one) {
3143
+ return get(data, key);
3144
+ };
3145
+
3146
+ var hasAssociation = function(type, options, one) {
3147
+ options = options || {};
3148
+
3149
+ var embedded = options.embedded,
3150
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
3151
+
3152
+ var meta = { type: type, isAssociation: true, options: options, kind: 'belongsTo' };
3153
+
3154
+ return Ember.computed(function(key, value) {
3155
+ var data = get(this, 'data'), ids, id, association,
3156
+ store = get(this, 'store');
3157
+
3158
+ if (typeof type === 'string') {
3159
+ type = getPath(this, type, false) || getPath(window, type);
3160
+ }
3161
+
3162
+ if (arguments.length === 2) {
3163
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
3164
+ this.send('setAssociation', { key: key, value: value === null ? null : get(value, 'clientId') });
3165
+ //data.setAssociation(key, get(value, 'clientId'));
3166
+ // put the client id in `key` in the data hash
3167
+ return value;
3168
+ } else {
3169
+ // Embedded belongsTo associations should not look for
3170
+ // a foreign key.
3171
+ if (embedded) {
3172
+ key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
3173
+
3174
+ // Non-embedded associations should look for a foreign key.
3175
+ // For example, instead of person, we might look for person_id
3176
+ } else {
3177
+ key = options.key || get(this, 'namingConvention').foreignKey(key);
3178
+ }
3179
+ id = findRecord(store, type, data, key, true);
3180
+ association = id ? store.find(type, id) : null;
3181
+ }
3182
+
3183
+ return association;
3184
+ }).property('data').cacheable().meta(meta);
3185
+ };
3186
+
3187
+ DS.belongsTo = function(type, options) {
3188
+ Ember.assert("The type passed to DS.belongsTo must be defined", !!type);
3189
+ return hasAssociation(type, options);
3190
+ };
3191
+
3192
+ })();
3193
+
3194
+
3195
+
3196
+ (function() {
3197
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
3198
+ var embeddedFindRecord = function(store, type, data, key) {
3199
+ var association = get(data, key);
3200
+ return association ? store.loadMany(type, association).ids : [];
3201
+ };
3202
+
3203
+ var referencedFindRecord = function(store, type, data, key, one) {
3204
+ return get(data, key);
3205
+ };
3206
+
3207
+ var hasAssociation = function(type, options) {
3208
+ options = options || {};
3209
+
3210
+ var embedded = options.embedded,
3211
+ findRecord = embedded ? embeddedFindRecord : referencedFindRecord;
3212
+
3213
+ var meta = { type: type, isAssociation: true, options: options, kind: 'hasMany' };
3214
+
3215
+ return Ember.computed(function(key, value) {
3216
+ var data = get(this, 'data'),
3217
+ store = get(this, 'store'),
3218
+ ids, id, association;
3219
+
3220
+ if (typeof type === 'string') {
3221
+ type = getPath(this, type, false) || getPath(window, type);
3222
+ }
3223
+
3224
+ key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
3225
+ ids = findRecord(store, type, data, key);
3226
+ association = store.findMany(type, ids);
3227
+ set(association, 'parentRecord', this);
3228
+
3229
+ return association;
3230
+ }).property().cacheable().meta(meta);
3231
+ };
3232
+
3233
+ DS.hasMany = function(type, options) {
3234
+ Ember.assert("The type passed to DS.hasMany must be defined", !!type);
3235
+ return hasAssociation(type, options);
3236
+ };
3237
+
3238
+ })();
3239
+
3240
+
3241
+
3242
+ (function() {
3243
+ var get = Ember.get, getPath = Ember.getPath;
3244
+
3245
+ DS.Model.reopenClass({
3246
+ typeForAssociation: function(name) {
3247
+ var association = get(this, 'associationsByName').get(name);
3248
+ return association && association.type;
3249
+ },
3250
+
3251
+ associations: Ember.computed(function() {
3252
+ var map = Ember.Map.create();
3253
+
3254
+ this.eachComputedProperty(function(name, meta) {
3255
+ if (meta.isAssociation) {
3256
+ var type = meta.type,
3257
+ typeList = map.get(type);
3258
+
3259
+ if (typeof type === 'string') {
3260
+ type = getPath(this, type, false) || getPath(window, type);
3261
+ meta.type = type;
3262
+ }
3263
+
3264
+ if (!typeList) {
3265
+ typeList = [];
3266
+ map.set(type, typeList);
3267
+ }
3268
+
3269
+ typeList.push({ name: name, kind: meta.kind });
3270
+ }
3271
+ });
3272
+
3273
+ return map;
3274
+ }).cacheable(),
3275
+
3276
+ associationsByName: Ember.computed(function() {
3277
+ var map = Ember.Map.create(), type;
3278
+
3279
+ this.eachComputedProperty(function(name, meta) {
3280
+ if (meta.isAssociation) {
3281
+ meta.key = name;
3282
+ type = meta.type;
3283
+
3284
+ if (typeof type === 'string') {
3285
+ type = getPath(this, type, false) || getPath(window, type);
3286
+ meta.type = type;
3287
+ }
3288
+
3289
+ map.set(name, meta);
3290
+ }
3291
+ });
3292
+
3293
+ return map;
3294
+ }).cacheable()
3295
+ });
3296
+
3297
+ })();
3298
+
3299
+
3300
+
3301
+ (function() {
3302
+
3303
+ })();
3304
+
3305
+
3306
+
3307
+ (function() {
3308
+ /**
3309
+ An adapter is an object that receives requests from a store and
3310
+ translates them into the appropriate action to take against your
3311
+ persistence layer. The persistence layer is usually an HTTP API, but may
3312
+ be anything, such as the browser's local storage.
3313
+
3314
+ ### Creating an Adapter
3315
+
3316
+ First, create a new subclass of `DS.Adapter`:
3317
+
3318
+ App.MyAdapter = DS.Adapter.extend({
3319
+ // ...your code here
3320
+ });
3321
+
3322
+ To tell your store which adapter to use, set its `adapter` property:
3323
+
3324
+ App.store = DS.Store.create({
3325
+ revision: 3,
3326
+ adapter: App.MyAdapter.create()
3327
+ });
3328
+
3329
+ `DS.Adapter` is an abstract base class that you should override in your
3330
+ application to customize it for your backend. The minimum set of methods
3331
+ that you should implement is:
3332
+
3333
+ * `find()`
3334
+ * `createRecord()`
3335
+ * `updateRecord()`
3336
+ * `deleteRecord()`
3337
+
3338
+ To improve the network performance of your application, you can optimize
3339
+ your adapter by overriding these lower-level methods:
3340
+
3341
+ * `findMany()`
3342
+ * `createRecords()`
3343
+ * `updateRecords()`
3344
+ * `deleteRecords()`
3345
+ * `commit()`
3346
+
3347
+ For more information about the adapter API, please see `README.md`.
3348
+ */
3349
+
3350
+ DS.Adapter = Ember.Object.extend({
3351
+ /**
3352
+ The `find()` method is invoked when the store is asked for a record that
3353
+ has not previously been loaded. In response to `find()` being called, you
3354
+ should query your persistence layer for a record with the given ID. Once
3355
+ found, you can asynchronously call the store's `load()` method to load
3356
+ the record.
3357
+
3358
+ Here is an example `find` implementation:
3359
+
3360
+ find: function(store, type, id) {
3361
+ var url = type.url;
3362
+ url = url.fmt(id);
3363
+
3364
+ jQuery.getJSON(url, function(data) {
3365
+ // data is a Hash of key/value pairs. If your server returns a
3366
+ // root, simply do something like:
3367
+ // store.load(type, id, data.person)
3368
+ store.load(type, id, data);
3369
+ });
3370
+ }
3371
+ */
3372
+ find: null,
3373
+
3374
+ /**
3375
+ If the globally unique IDs for your records should be generated on the client,
3376
+ implement the `generateIdForRecord()` method. This method will be invoked
3377
+ each time you create a new record, and the value returned from it will be
3378
+ assigned to the record's `primaryKey`.
3379
+
3380
+ Most traditional REST-like HTTP APIs will not use this method. Instead, the ID
3381
+ of the record will be set by the server, and your adapter will update the store
3382
+ with the new ID when it calls `didCreateRecord()`. Only implement this method if
3383
+ you intend to generate record IDs on the client-side.
3384
+
3385
+ The `generateIdForRecord()` method will be invoked with the requesting store as
3386
+ the first parameter and the newly created record as the second parameter:
3387
+
3388
+ generateIdForRecord: function(store, record) {
3389
+ var uuid = App.generateUUIDWithStatisticallyLowOddsOfCollision();
3390
+ return uuid;
3391
+ }
3392
+ */
3393
+ generateIdForRecord: null,
3394
+
3395
+ commit: function(store, commitDetails) {
3396
+ commitDetails.updated.eachType(function(type, array) {
3397
+ this.updateRecords(store, type, array.slice());
3398
+ }, this);
3399
+
3400
+ commitDetails.created.eachType(function(type, array) {
3401
+ this.createRecords(store, type, array.slice());
3402
+ }, this);
3403
+
3404
+ commitDetails.deleted.eachType(function(type, array) {
3405
+ this.deleteRecords(store, type, array.slice());
3406
+ }, this);
3407
+ },
3408
+
3409
+ createRecords: function(store, type, records) {
3410
+ records.forEach(function(record) {
3411
+ this.createRecord(store, type, record);
3412
+ }, this);
3413
+ },
3414
+
3415
+ updateRecords: function(store, type, records) {
3416
+ records.forEach(function(record) {
3417
+ this.updateRecord(store, type, record);
3418
+ }, this);
3419
+ },
3420
+
3421
+ deleteRecords: function(store, type, records) {
3422
+ records.forEach(function(record) {
3423
+ this.deleteRecord(store, type, record);
3424
+ }, this);
3425
+ },
3426
+
3427
+ findMany: function(store, type, ids) {
3428
+ ids.forEach(function(id) {
3429
+ this.find(store, type, id);
3430
+ }, this);
3431
+ }
3432
+ });
3433
+
3434
+ })();
3435
+
3436
+
3437
+
3438
+ (function() {
3439
+ var set = Ember.set;
3440
+
3441
+ Ember.onLoad('application', function(app) {
3442
+ app.registerInjection({
3443
+ name: "store",
3444
+ before: "controllers",
3445
+
3446
+ injection: function(app, stateManager, property) {
3447
+ if (property === 'Store') {
3448
+ set(stateManager, 'store', app[property].create());
3449
+ }
3450
+ }
3451
+ });
3452
+ });
3453
+
3454
+ })();
3455
+
3456
+
3457
+
3458
+ (function() {
3459
+ DS.fixtureAdapter = DS.Adapter.create({
3460
+ find: function(store, type, id) {
3461
+ var fixtures = type.FIXTURES;
3462
+
3463
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
3464
+ if (fixtures.hasLoaded) { return; }
3465
+
3466
+ setTimeout(function() {
3467
+ store.loadMany(type, fixtures);
3468
+ fixtures.hasLoaded = true;
3469
+ }, 300);
3470
+ },
3471
+
3472
+ findMany: function() {
3473
+ this.find.apply(this, arguments);
3474
+ },
3475
+
3476
+ findAll: function(store, type) {
3477
+ var fixtures = type.FIXTURES;
3478
+
3479
+ Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
3480
+
3481
+ var ids = fixtures.map(function(item, index, self){ return item.id; });
3482
+ store.loadMany(type, ids, fixtures);
3483
+ }
3484
+
3485
+ });
3486
+
3487
+ })();
3488
+
3489
+
3490
+
3491
+ (function() {
3492
+ /*global jQuery*/
3493
+
3494
+ var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
3495
+
3496
+ DS.RESTAdapter = DS.Adapter.extend({
3497
+ bulkCommit: false,
3498
+
3499
+ createRecord: function(store, type, record) {
3500
+ var root = this.rootForType(type);
3501
+
3502
+ var data = {};
3503
+ data[root] = record.toJSON();
3504
+
3505
+ this.ajax(this.buildURL(root), "POST", {
3506
+ data: data,
3507
+ success: function(json) {
3508
+ this.sideload(store, type, json, root);
3509
+ store.didCreateRecord(record, json[root]);
3510
+ }
3511
+ });
3512
+ },
3513
+
3514
+ createRecords: function(store, type, records) {
3515
+ if (get(this, 'bulkCommit') === false) {
3516
+ return this._super(store, type, records);
3517
+ }
3518
+
3519
+ var root = this.rootForType(type),
3520
+ plural = this.pluralize(root);
3521
+
3522
+ var data = {};
3523
+ data[plural] = records.map(function(record) {
3524
+ return record.toJSON();
3525
+ });
3526
+
3527
+ this.ajax(this.buildURL(root), "POST", {
3528
+ data: data,
3529
+
3530
+ success: function(json) {
3531
+ this.sideload(store, type, json, plural);
3532
+ store.didCreateRecords(type, records, json[plural]);
3533
+ }
3534
+ });
3535
+ },
3536
+
3537
+ updateRecord: function(store, type, record) {
3538
+ var id = get(record, 'id');
3539
+ var root = this.rootForType(type);
3540
+
3541
+ var data = {};
3542
+ data[root] = record.toJSON();
3543
+
3544
+ this.ajax(this.buildURL(root, id), "PUT", {
3545
+ data: data,
3546
+ success: function(json) {
3547
+ this.sideload(store, type, json, root);
3548
+ store.didUpdateRecord(record, json && json[root]);
3549
+ }
3550
+ });
3551
+ },
3552
+
3553
+ updateRecords: function(store, type, records) {
3554
+ if (get(this, 'bulkCommit') === false) {
3555
+ return this._super(store, type, records);
3556
+ }
3557
+
3558
+ var root = this.rootForType(type),
3559
+ plural = this.pluralize(root);
3560
+
3561
+ var data = {};
3562
+ data[plural] = records.map(function(record) {
3563
+ return record.toJSON();
3564
+ });
3565
+
3566
+ this.ajax(this.buildURL(root, "bulk"), "PUT", {
3567
+ data: data,
3568
+ success: function(json) {
3569
+ this.sideload(store, type, json, plural);
3570
+ store.didUpdateRecords(records, json[plural]);
3571
+ }
3572
+ });
3573
+ },
3574
+
3575
+ deleteRecord: function(store, type, record) {
3576
+ var id = get(record, 'id');
3577
+ var root = this.rootForType(type);
3578
+
3579
+ this.ajax(this.buildURL(root, id), "DELETE", {
3580
+ success: function(json) {
3581
+ if (json) { this.sideload(store, type, json); }
3582
+ store.didDeleteRecord(record);
3583
+ }
3584
+ });
3585
+ },
3586
+
3587
+ deleteRecords: function(store, type, records) {
3588
+ if (get(this, 'bulkCommit') === false) {
3589
+ return this._super(store, type, records);
3590
+ }
3591
+
3592
+ var root = this.rootForType(type),
3593
+ plural = this.pluralize(root);
3594
+
3595
+ var data = {};
3596
+ data[plural] = records.map(function(record) {
3597
+ return get(record, 'id');
3598
+ });
3599
+
3600
+ this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
3601
+ data: data,
3602
+ success: function(json) {
3603
+ if (json) { this.sideload(store, type, json); }
3604
+ store.didDeleteRecords(records);
3605
+ }
3606
+ });
3607
+ },
3608
+
3609
+ find: function(store, type, id) {
3610
+ var root = this.rootForType(type);
3611
+
3612
+ this.ajax(this.buildURL(root, id), "GET", {
3613
+ success: function(json) {
3614
+ this.sideload(store, type, json, root);
3615
+ store.load(type, json[root]);
3616
+ }
3617
+ });
3618
+ },
3619
+
3620
+ findMany: function(store, type, ids) {
3621
+ var root = this.rootForType(type), plural = this.pluralize(root);
3622
+
3623
+ this.ajax(this.buildURL(root), "GET", {
3624
+ data: { ids: ids },
3625
+ success: function(json) {
3626
+ this.sideload(store, type, json, plural);
3627
+ store.loadMany(type, json[plural]);
3628
+ }
3629
+ });
3630
+ },
3631
+
3632
+ findAll: function(store, type) {
3633
+ var root = this.rootForType(type), plural = this.pluralize(root);
3634
+
3635
+ this.ajax(this.buildURL(root), "GET", {
3636
+ success: function(json) {
3637
+ this.sideload(store, type, json, plural);
3638
+ store.loadMany(type, json[plural]);
3639
+ }
3640
+ });
3641
+ },
3642
+
3643
+ findQuery: function(store, type, query, recordArray) {
3644
+ var root = this.rootForType(type), plural = this.pluralize(root);
3645
+
3646
+ this.ajax(this.buildURL(root), "GET", {
3647
+ data: query,
3648
+ success: function(json) {
3649
+ this.sideload(store, type, json, plural);
3650
+ recordArray.load(json[plural]);
3651
+ }
3652
+ });
3653
+ },
3654
+
3655
+ // HELPERS
3656
+
3657
+ plurals: {},
3658
+
3659
+ // define a plurals hash in your subclass to define
3660
+ // special-case pluralization
3661
+ pluralize: function(name) {
3662
+ return this.plurals[name] || name + "s";
3663
+ },
3664
+
3665
+ rootForType: function(type) {
3666
+ if (type.url) { return type.url; }
3667
+
3668
+ // use the last part of the name as the URL
3669
+ var parts = type.toString().split(".");
3670
+ var name = parts[parts.length - 1];
3671
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
3672
+ },
3673
+
3674
+ ajax: function(url, type, hash) {
3675
+ hash.url = url;
3676
+ hash.type = type;
3677
+ hash.dataType = 'json';
3678
+ hash.contentType = 'application/json; charset=utf-8';
3679
+ hash.context = this;
3680
+
3681
+ if (hash.data && type !== 'GET') {
3682
+ hash.data = JSON.stringify(hash.data);
3683
+ }
3684
+
3685
+ jQuery.ajax(hash);
3686
+ },
3687
+
3688
+ sideload: function(store, type, json, root) {
3689
+ var sideloadedType, mappings, loaded = {};
3690
+
3691
+ loaded[root] = true;
3692
+
3693
+ for (var prop in json) {
3694
+ if (!json.hasOwnProperty(prop)) { continue; }
3695
+ if (prop === root) { continue; }
3696
+
3697
+ sideloadedType = type.typeForAssociation(prop);
3698
+
3699
+ if (!sideloadedType) {
3700
+ mappings = get(this, 'mappings');
3701
+ Ember.assert("Your server returned a hash with the key " + prop + " but you have no mappings", !!mappings);
3702
+
3703
+ sideloadedType = get(mappings, prop);
3704
+
3705
+ if (typeof sideloadedType === 'string') {
3706
+ sideloadedType = getPath(window, sideloadedType);
3707
+ }
3708
+
3709
+ Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType);
3710
+ }
3711
+
3712
+ this.sideloadAssociations(store, sideloadedType, json, prop, loaded);
3713
+ }
3714
+ },
3715
+
3716
+ sideloadAssociations: function(store, type, json, prop, loaded) {
3717
+ loaded[prop] = true;
3718
+
3719
+ get(type, 'associationsByName').forEach(function(key, meta) {
3720
+ key = meta.key || key;
3721
+ if (meta.kind === 'belongsTo') {
3722
+ key = this.pluralize(key);
3723
+ }
3724
+ if (json[key] && !loaded[key]) {
3725
+ this.sideloadAssociations(store, meta.type, json, key, loaded);
3726
+ }
3727
+ }, this);
3728
+
3729
+ this.loadValue(store, type, json[prop]);
3730
+ },
3731
+
3732
+ loadValue: function(store, type, value) {
3733
+ if (value instanceof Array) {
3734
+ store.loadMany(type, value);
3735
+ } else {
3736
+ store.load(type, value);
3737
+ }
3738
+ },
3739
+
3740
+ buildURL: function(record, suffix) {
3741
+ var url = [""];
3742
+
3743
+ Ember.assert("Namespace URL (" + this.namespace + ") must not start with slash", !this.namespace || this.namespace.toString().charAt(0) !== "/");
3744
+ Ember.assert("Record URL (" + record + ") must not start with slash", !record || record.toString().charAt(0) !== "/");
3745
+ Ember.assert("URL suffix (" + suffix + ") must not start with slash", !suffix || suffix.toString().charAt(0) !== "/");
3746
+
3747
+ if (this.namespace !== undefined) {
3748
+ url.push(this.namespace);
3749
+ }
3750
+
3751
+ url.push(this.pluralize(record));
3752
+ if (suffix !== undefined) {
3753
+ url.push(suffix);
3754
+ }
3755
+
3756
+ return url.join("/");
3757
+ }
3758
+ });
3759
+
3760
+
3761
+ })();
3762
+
3763
+
3764
+
3765
+ (function() {
3766
+ //Copyright (C) 2011 by Living Social, Inc.
3767
+
3768
+ //Permission is hereby granted, free of charge, to any person obtaining a copy of
3769
+ //this software and associated documentation files (the "Software"), to deal in
3770
+ //the Software without restriction, including without limitation the rights to
3771
+ //use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
3772
+ //of the Software, and to permit persons to whom the Software is furnished to do
3773
+ //so, subject to the following conditions:
3774
+
3775
+ //The above copyright notice and this permission notice shall be included in all
3776
+ //copies or substantial portions of the Software.
3777
+
3778
+ //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3779
+ //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3780
+ //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3781
+ //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3782
+ //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3783
+ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3784
+ //SOFTWARE.
3785
+
3786
+ })();
3787
+