ember-rails-lite 0.8.0

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