ember-rails 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -27,7 +27,7 @@ to use `rails g ember:bootstrap` to create the following directory structure und
27
27
  controllers/
28
28
  helpers/
29
29
  models/
30
- states/
30
+ routes/
31
31
  templates/
32
32
  views/
33
33
 
@@ -18,6 +18,7 @@ var jQuery = window.jQuery = function() { return jQuery; };
18
18
  jQuery.ready = function() { return jQuery; };
19
19
  jQuery.inArray = function() { return jQuery; };
20
20
  jQuery.jquery = "1.7.2";
21
+ jQuery.event = {fixHooks: {}};
21
22
 
22
23
  // Precompiler
23
24
  var EmberRails = {
@@ -17,16 +17,6 @@ module Ember
17
17
  app.assets.register_engine '.handlebars', Ember::Handlebars::Template
18
18
  app.assets.register_engine '.hbs', Ember::Handlebars::Template
19
19
  app.assets.register_engine '.hjs', Ember::Handlebars::Template
20
-
21
- # Add the gem's vendored ember to the end of the asset search path
22
- variant = app.config.ember.variant
23
-
24
- if variant.nil?
25
- warn "[EMBER-RAILS] `ember.variant` was not found in your current environment"
26
- end
27
-
28
- ember_path = File.expand_path("../../../../vendor/ember/#{variant}", __FILE__)
29
- app.config.assets.paths.unshift ember_path
30
20
  end
31
21
  end
32
22
  end
@@ -1,5 +1,5 @@
1
1
  module Ember
2
2
  module Rails
3
- VERSION = '0.6.0'
3
+ VERSION = '0.7.0'
4
4
  end
5
5
  end
data/lib/ember_rails.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'rails'
1
2
  require 'ember/rails/version'
2
3
  require 'ember/version'
3
4
  require 'ember/handlebars/version'
@@ -19,15 +20,28 @@ module Ember
19
20
  end
20
21
 
21
22
  initializer "ember_rails.setup_vendor", :after => "ember_rails.setup", :group => :all do |app|
22
- # Add the gem's vendored ember to the end of the asset search path
23
- variant = app.config.ember.variant
24
- ember_path = app.root.join("vendor/assets/ember/#{variant}")
25
- app.config.assets.paths.unshift(ember_path.to_s) if ember_path.exist?
23
+ if variant = app.config.ember.variant
24
+ # Add the gem's vendored ember to the end of the asset search path
25
+ ember_path = File.expand_path("../../vendor/ember/#{variant}", __FILE__)
26
+ app.config.assets.paths.push(ember_path.to_s)
27
+
28
+ # Allow a local variant override
29
+ ember_path = app.root.join("vendor/assets/ember/#{variant}")
30
+ app.config.assets.paths.unshift(ember_path.to_s) if ember_path.exist?
31
+ else
32
+ warn "No ember.js variant was specified in your config environment."
33
+ warn "You can set a specific variant in your application config in "
34
+ warn "order for sprockets to locate ember's assets:"
35
+ warn ""
36
+ warn " config.ember.variant = :development"
37
+ warn ""
38
+ warn "Valid values are :development and :production"
39
+ end
26
40
  end
27
41
 
28
42
  initializer "ember_rails.find_ember", :after => "ember_rails.setup_vendor", :group => :all do |app|
29
- config.ember.ember_location = location_for(app, "ember.js")
30
- config.ember.handlebars_location = location_for(app, "handlebars.js")
43
+ config.ember.ember_location ||= location_for(app, "ember.js")
44
+ config.ember.handlebars_location ||= location_for(app, "handlebars.js")
31
45
  end
32
46
 
33
47
  def location_for(app, file)
@@ -51,20 +51,6 @@ module Ember
51
51
  def create_app_stubs
52
52
  generate "ember:view", "application"
53
53
  end
54
-
55
- def inject_proper_ember_version
56
- environment <<-RUBY.strip_heredoc, :env => :development
57
- config.ember.variant = :development
58
- RUBY
59
-
60
- environment <<-RUBY.strip_heredoc, :env => :test
61
- config.ember.variant = :development
62
- RUBY
63
-
64
- environment <<-RUBY.strip_heredoc, :env => :production
65
- config.ember.variant = :production
66
- RUBY
67
- end
68
54
  end
69
55
  end
70
56
  end
@@ -7,11 +7,14 @@ module Ember
7
7
 
8
8
  desc "Creates a new Ember.js controller"
9
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"
10
11
 
11
12
  def create_controller_files
12
13
  file_path = File.join('app/assets/javascripts/controllers', class_path, "#{file_name}_controller.js")
13
14
  if options.array?
14
15
  template 'array_controller.js', file_path
16
+ elsif options.object?
17
+ template 'object_controller.js', file_path
15
18
  else
16
19
  template 'controller.js', file_path
17
20
  end
@@ -20,7 +20,7 @@ module Ember
20
20
  cmd command
21
21
  else
22
22
  Dir.chdir git_root do
23
- command = "git fetch --force --quiet --tags && git reset origin/master --hard"
23
+ command = "git fetch origin && git reset origin/master --hard"
24
24
  say_status("updating", command, :green)
25
25
 
26
26
  cmd command
@@ -35,13 +35,49 @@ module Ember
35
35
  end
36
36
  end
37
37
 
38
- self.class.source_root File.join(git_root, "dist")
38
+ source_paths << File.join(git_root, "dist")
39
39
 
40
40
  copy_file "ember.js", "vendor/assets/ember/development/ember.js"
41
41
  copy_file "ember.min.js", "vendor/assets/ember/production/ember.js"
42
42
  end
43
43
  end
44
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
+
45
81
  private
46
82
 
47
83
  def cmd(command)
@@ -14,7 +14,7 @@ module Rails
14
14
 
15
15
  say_status :invoke, "ember controller and view (singular)", :white
16
16
  with_padding do
17
- invoke "ember:view"
17
+ invoke "ember:view", [singular_name], :object => true
18
18
  end
19
19
 
20
20
  @_invocations[Ember::Generators::ControllerGenerator].delete "create_controller_files"
@@ -7,6 +7,7 @@ module Ember
7
7
 
8
8
  desc "Creates a new Ember.js view and associated Handlebars template"
9
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"
10
11
 
11
12
  def create_view_files
12
13
  template 'view.js', File.join('app/assets/javascripts/views', class_path, "#{file_name}_view.js")
@@ -7,9 +7,4 @@
7
7
  //= require_tree ./routes
8
8
  //= require_self
9
9
 
10
- var router = <%= application_name.camelize %>.router = <%= application_name.camelize %>.Router.create({
11
- location: 'hash'
12
- });
13
-
14
- <%= application_name.camelize %>.initialize(router);
15
-
10
+ <%= application_name.camelize %>.initialize();
@@ -1,7 +1,3 @@
1
1
  <%= application_name.camelize %>.<%= class_name %>Controller = Ember.ArrayController.extend({
2
- // Implement your controller here.
3
- //
4
- // An ArrayController has a `content` property, which you should
5
- // set up in your router.
6
- });
7
2
 
3
+ });
@@ -1,4 +1,3 @@
1
- <%= application_name.camelize %>.<%= class_name %>Controller = Ember.ObjectController.extend({
2
- // Implement your controller here.
3
- });
1
+ <%= application_name.camelize %>.<%= class_name %>Controller = Ember.Controller.extend({
4
2
 
3
+ });
@@ -0,0 +1,3 @@
1
+ <%= application_name.camelize %>.<%= class_name %>Controller = Ember.ObjectController.extend({
2
+
3
+ });
@@ -1,6 +1,8 @@
1
1
  <%= application_name.camelize %>.Router = Ember.Router.extend({
2
- root: Ember.State.extend({
3
- index: Ember.State.extend({
2
+ location: 'hash',
3
+
4
+ root: Ember.Route.extend({
5
+ index: Ember.Route.extend({
4
6
  route: '/'
5
7
 
6
8
  // You'll likely want to connect a view here.
@@ -36,42 +36,14 @@ DS.RecordArray = Ember.ArrayProxy.extend({
36
36
  // The store that created this record array.
37
37
  store: null,
38
38
 
39
- init: function() {
40
- set(this, 'recordCache', Ember.A([]));
41
- this._super();
42
- },
43
-
44
- arrayDidChange: function(array, index, removed, added) {
45
- var recordCache = get(this, 'recordCache');
46
- recordCache.replace(index, 0, new Array(added));
47
-
48
- this._super(array, index, removed, added);
49
- },
50
-
51
- arrayWillChange: function(array, index, removed, added) {
52
- this._super(array, index, removed, added);
53
-
54
- var recordCache = get(this, 'recordCache');
55
- recordCache.replace(index, removed);
56
- },
57
-
58
39
  objectAtContent: function(index) {
59
- var recordCache = get(this, 'recordCache');
60
- var record = recordCache.objectAt(index);
61
-
62
- if (!record) {
63
- var store = get(this, 'store');
64
- var content = get(this, 'content');
65
-
66
- var contentObject = content.objectAt(index);
40
+ var content = get(this, 'content'),
41
+ clientId = content.objectAt(index),
42
+ store = get(this, 'store');
67
43
 
68
- if (contentObject !== undefined) {
69
- record = store.findByClientId(get(this, 'type'), contentObject);
70
- recordCache.replace(index, 1, [record]);
71
- }
44
+ if (clientId !== undefined) {
45
+ return store.findByClientId(get(this, 'type'), clientId);
72
46
  }
73
-
74
- return record;
75
47
  }
76
48
  });
77
49
 
@@ -156,7 +128,7 @@ Set.prototype = {
156
128
 
157
129
  delete hash[guid];
158
130
  var list = this.list,
159
- index = Ember.ArrayUtils.indexOf(this, item);
131
+ index = Ember.EnumerableUtils.indexOf(this, item);
160
132
 
161
133
  list.splice(index, 1);
162
134
  },
@@ -166,7 +138,7 @@ Set.prototype = {
166
138
  }
167
139
  };
168
140
 
169
- var ManyArrayState = Ember.State.extend({
141
+ var LoadedState = Ember.State.extend({
170
142
  recordWasAdded: function(manager, record) {
171
143
  var dirty = manager.dirty, observer;
172
144
  dirty.add(record);
@@ -195,7 +167,21 @@ var ManyArrayState = Ember.State.extend({
195
167
  });
196
168
 
197
169
  var states = {
198
- clean: ManyArrayState.create({
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,
199
185
  isDirty: false,
200
186
 
201
187
  recordWasAdded: function(manager, record) {
@@ -209,7 +195,8 @@ var states = {
209
195
  }
210
196
  }),
211
197
 
212
- dirty: ManyArrayState.create({
198
+ dirty: LoadedState.create({
199
+ isLoaded: true,
213
200
  isDirty: true,
214
201
 
215
202
  childWasSaved: function(manager, child) {
@@ -222,17 +209,37 @@ var states = {
222
209
  arrayBecameSaved: function(manager) {
223
210
  manager.goToState('clean');
224
211
  }
225
- })
212
+ })
226
213
  };
227
214
 
228
215
  DS.ManyArrayStateManager = Ember.StateManager.extend({
229
216
  manyArray: null,
230
- initialState: 'clean',
217
+ initialState: 'loading',
231
218
  states: states,
232
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
+
233
229
  init: function() {
234
230
  this._super();
235
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
+ }
236
243
  }
237
244
  });
238
245
 
@@ -241,7 +248,7 @@ DS.ManyArrayStateManager = Ember.StateManager.extend({
241
248
 
242
249
 
243
250
  (function() {
244
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
251
+ var get = Ember.get, set = Ember.set;
245
252
 
246
253
  DS.ManyArray = DS.RecordArray.extend({
247
254
  init: function() {
@@ -253,23 +260,27 @@ DS.ManyArray = DS.RecordArray.extend({
253
260
  parentRecord: null,
254
261
 
255
262
  isDirty: Ember.computed(function() {
256
- return getPath(this, 'stateManager.currentState.isDirty');
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');
257
268
  }).property('stateManager.currentState').cacheable(),
258
269
 
270
+ send: function(event, context) {
271
+ this.get('stateManager').send(event, context);
272
+ },
273
+
259
274
  fetch: function() {
260
275
  var clientIds = get(this, 'content'),
261
276
  store = get(this, 'store'),
262
277
  type = get(this, 'type');
263
278
 
264
- var ids = clientIds.map(function(clientId) {
265
- return store.clientIdToId[clientId];
266
- });
267
-
268
- store.fetchMany(type, ids);
279
+ store.fetchUnloadedClientIds(type, clientIds);
269
280
  },
270
281
 
271
282
  // Overrides Ember.Array's replace method to implement
272
- replace: function(index, removed, added) {
283
+ replaceContent: function(index, removed, added) {
273
284
  var parentRecord = get(this, 'parentRecord');
274
285
  var pendingParent = parentRecord && !get(parentRecord, 'id');
275
286
  var stateManager = get(this, 'stateManager');
@@ -286,7 +297,10 @@ DS.ManyArray = DS.RecordArray.extend({
286
297
  record.send('waitingOn', parentRecord);
287
298
  }
288
299
 
289
- this.assignInverse(record, parentRecord);
300
+ var oldParent = this.assignInverse(record, parentRecord);
301
+
302
+ record.get('transaction')
303
+ .relationshipBecameDirty(record, oldParent, parentRecord);
290
304
 
291
305
  stateManager.send('recordWasAdded', record);
292
306
 
@@ -299,7 +313,10 @@ DS.ManyArray = DS.RecordArray.extend({
299
313
  for (var i = index; i < len; i++) {
300
314
  // TODO: null out inverse FK
301
315
  record = this.objectAt(i);
302
- this.assignInverse(record, parentRecord, true);
316
+ var oldParent = this.assignInverse(record, parentRecord, true);
317
+
318
+ record.get('transaction')
319
+ .relationshipBecameDirty(record, parentRecord, null);
303
320
 
304
321
  // If we put the child record into a pending state because
305
322
  // we were waiting on the parent record to get an id, we
@@ -317,7 +334,7 @@ DS.ManyArray = DS.RecordArray.extend({
317
334
  assignInverse: function(record, parentRecord, remove) {
318
335
  var associationMap = get(record.constructor, 'associations'),
319
336
  possibleAssociations = associationMap.get(parentRecord.constructor),
320
- possible, actual;
337
+ possible, actual, oldParent;
321
338
 
322
339
  if (!possibleAssociations) { return; }
323
340
 
@@ -331,7 +348,9 @@ DS.ManyArray = DS.RecordArray.extend({
331
348
  }
332
349
 
333
350
  if (actual) {
351
+ oldParent = get(record, actual.name);
334
352
  set(record, actual.name, remove ? null : parentRecord);
353
+ return oldParent;
335
354
  }
336
355
  },
337
356
 
@@ -362,7 +381,8 @@ DS.ManyArray = DS.RecordArray.extend({
362
381
 
363
382
 
364
383
  (function() {
365
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
384
+ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt,
385
+ removeObject = Ember.EnumerableUtils.removeObject;
366
386
 
367
387
  /**
368
388
  A transaction allows you to collect multiple records into a unit of work
@@ -381,7 +401,7 @@ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.Strin
381
401
  If you do not explicitly create a transaction, a record is assigned to
382
402
  an implicit transaction called the default transaction. In these cases,
383
403
  you can treat your application's instance of `DS.Store` as a transaction
384
- and call the `commit()` and `rollback()` methods on the store itself.
404
+ and call the `commit()` and `rollback()` methods on the store itself.
385
405
 
386
406
  Once a record has been successfully committed or rolled back, it will
387
407
  be moved back to the implicit transaction. Because it will now be in
@@ -457,6 +477,12 @@ DS.Transaction = Ember.Object.extend({
457
477
  deleted: Ember.Map.create(),
458
478
  inflight: Ember.Map.create()
459
479
  });
480
+
481
+ this.dirtyRelationships = {
482
+ byChild: Ember.Map.create(),
483
+ byNewParent: Ember.Map.create(),
484
+ byOldParent: Ember.Map.create()
485
+ };
460
486
  },
461
487
 
462
488
  /**
@@ -487,7 +513,7 @@ DS.Transaction = Ember.Object.extend({
487
513
  Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
488
514
 
489
515
  var recordTransaction = get(record, 'transaction'),
490
- defaultTransaction = getPath(this, 'store.defaultTransaction');
516
+ defaultTransaction = get(this, 'store.defaultTransaction');
491
517
 
492
518
  Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
493
519
 
@@ -599,7 +625,7 @@ DS.Transaction = Ember.Object.extend({
599
625
  @param {DS.Model} record
600
626
  */
601
627
  remove: function(record) {
602
- var defaultTransaction = getPath(this, 'store.defaultTransaction');
628
+ var defaultTransaction = get(this, 'store.defaultTransaction');
603
629
  defaultTransaction.adoptRecord(record);
604
630
  },
605
631
 
@@ -696,6 +722,113 @@ DS.Transaction = Ember.Object.extend({
696
722
  records.remove(record);
697
723
  },
698
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
+
699
832
  /**
700
833
  @private
701
834
 
@@ -746,7 +879,7 @@ DS.Transaction = Ember.Object.extend({
746
879
 
747
880
  (function() {
748
881
  /*globals Ember*/
749
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
882
+ var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
750
883
 
751
884
  var DATA_PROXY = {
752
885
  get: function(name) {
@@ -824,6 +957,13 @@ DS.Store = Ember.Object.extend({
824
957
  this.clientIdToId = {};
825
958
  this.recordArraysByClientId = {};
826
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
+
827
967
  set(this, 'defaultTransaction', this.transaction());
828
968
 
829
969
  return this._super();
@@ -872,7 +1012,7 @@ DS.Store = Ember.Object.extend({
872
1012
  _adapter: Ember.computed(function() {
873
1013
  var adapter = get(this, 'adapter');
874
1014
  if (typeof adapter === 'string') {
875
- return getPath(this, adapter, false) || getPath(window, adapter);
1015
+ return get(this, adapter, false) || get(window, adapter);
876
1016
  }
877
1017
  return adapter;
878
1018
  }).property('adapter').cacheable(),
@@ -1048,8 +1188,7 @@ DS.Store = Ember.Object.extend({
1048
1188
 
1049
1189
  findByClientId: function(type, clientId, id) {
1050
1190
  var recordCache = get(this, 'recordCache'),
1051
- dataCache = this.typeMapFor(type).cidToHash,
1052
- record;
1191
+ dataCache, record;
1053
1192
 
1054
1193
  // If there is already a clientId assigned for this
1055
1194
  // type/id combination, try to find an existing
@@ -1064,6 +1203,8 @@ DS.Store = Ember.Object.extend({
1064
1203
  // 'isLoading' state
1065
1204
  record = this.materializeRecord(type, clientId);
1066
1205
 
1206
+ dataCache = this.typeMapFor(type).cidToHash;
1207
+
1067
1208
  if (typeof dataCache[clientId] === 'object') {
1068
1209
  record.send('didChangeData');
1069
1210
  }
@@ -1087,78 +1228,123 @@ DS.Store = Ember.Object.extend({
1087
1228
  /**
1088
1229
  @private
1089
1230
 
1090
- Ask the adapter to fetch IDs that are not already loaded.
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
+ },
1091
1253
 
1092
- This method will convert `id`s to `clientId`s, filter out
1093
- `clientId`s that already have a data hash present, and pass
1094
- the remaining `id`s to the adapter.
1254
+ /**
1255
+ @private
1095
1256
 
1096
- @param {Class} type A model class
1097
- @param {Array} ids An array of ids
1098
- @param {Object} query
1257
+ This method is the entry point that associations use to update
1258
+ themselves when their underlying data changes.
1099
1259
 
1100
- @returns {Array} An Array of all clientIds for the
1101
- specified ids.
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.
1102
1263
  */
1103
- fetchMany: function(type, ids, query) {
1104
- var typeMap = this.typeMapFor(type),
1105
- idToClientIdMap = typeMap.idToCid,
1106
- dataCache = typeMap.cidToHash,
1107
- data = typeMap.cidToHash,
1108
- needed;
1264
+ fetchUnloadedClientIds: function(type, clientIds) {
1265
+ var neededClientIds = this.neededClientIds(type, clientIds);
1266
+ this.fetchMany(type, neededClientIds);
1267
+ },
1109
1268
 
1110
- var clientIds = Ember.A([]);
1269
+ /**
1270
+ @private
1111
1271
 
1112
- if (ids) {
1113
- needed = [];
1114
-
1115
- ids.forEach(function(id) {
1116
- // Get the clientId for the given id
1117
- var clientId = idToClientIdMap[id];
1118
-
1119
- // If there is no `clientId` yet
1120
- if (clientId === undefined) {
1121
- // Create a new `clientId`, marking its data hash
1122
- // as loading. Once the adapter returns the data
1123
- // hash, it will be updated
1124
- clientId = this.pushHash(LOADING, id, type);
1125
- needed.push(id);
1126
-
1127
- // If there is a clientId, but its data hash is
1128
- // marked as unloaded (this happens when a
1129
- // hasMany association creates clientIds for its
1130
- // referenced ids before they were loaded)
1131
- } else if (clientId && data[clientId] === UNLOADED) {
1132
- // change the data hash marker to loading
1133
- dataCache[clientId] = LOADING;
1134
- needed.push(id);
1135
- }
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.
1136
1275
 
1137
- // this method is expected to return a list of
1138
- // all of the clientIds for the specified ids,
1139
- // unconditionally add it.
1140
- clientIds.push(clientId);
1141
- }, this);
1142
- } else {
1143
- needed = null;
1144
- }
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;
1145
1282
 
1146
- // If there are any needed ids, ask the adapter to load them
1147
- if ((needed && get(needed, 'length') > 0) || query) {
1148
- var adapter = get(this, '_adapter');
1149
- if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
1150
- else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
1151
- }
1283
+ var neededIds = Ember.EnumerableUtils.map(clientIds, function(clientId) {
1284
+ return clientIdToId[clientId];
1285
+ });
1286
+
1287
+ if (!neededIds.length) { return; }
1152
1288
 
1153
- return clientIds;
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); }
1154
1292
  },
1155
1293
 
1156
- /** @private
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.
1157
1313
  */
1158
- findMany: function(type, ids, query) {
1159
- var clientIds = this.fetchMany(type, ids, query);
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
+ }
1160
1346
 
1161
- return this.createManyArray(type, clientIds);
1347
+ return manyArray;
1162
1348
  },
1163
1349
 
1164
1350
  findQuery: function(type, query) {
@@ -1201,6 +1387,10 @@ DS.Store = Ember.Object.extend({
1201
1387
  return array;
1202
1388
  },
1203
1389
 
1390
+ recordIsLoaded: function(type, id) {
1391
+ return !Ember.none(this.typeMapFor(type).idToCid[id]);
1392
+ },
1393
+
1204
1394
  // ............
1205
1395
  // . UPDATING .
1206
1396
  // ............
@@ -1361,21 +1551,28 @@ DS.Store = Ember.Object.extend({
1361
1551
  clientIds = typeMap.clientIds,
1362
1552
  clientId, hash, proxy;
1363
1553
 
1364
- var recordCache = get(this, 'recordCache'), record;
1554
+ var recordCache = get(this, 'recordCache'),
1555
+ foundRecord,
1556
+ record;
1365
1557
 
1366
1558
  for (var i=0, l=clientIds.length; i<l; i++) {
1367
1559
  clientId = clientIds[i];
1560
+ foundRecord = false;
1368
1561
 
1369
1562
  hash = dataCache[clientId];
1370
1563
  if (typeof hash === 'object') {
1371
1564
  if (record = recordCache[clientId]) {
1372
- proxy = get(record, 'data');
1565
+ if (!get(record, 'isDeleted')) {
1566
+ proxy = get(record, 'data');
1567
+ foundRecord = true;
1568
+ }
1373
1569
  } else {
1374
1570
  DATA_PROXY.savedData = hash;
1375
1571
  proxy = DATA_PROXY;
1572
+ foundRecord = true;
1376
1573
  }
1377
1574
 
1378
- this.updateRecordArray(array, filter, type, clientId, proxy);
1575
+ if (foundRecord) { this.updateRecordArray(array, filter, type, clientId, proxy); }
1379
1576
  }
1380
1577
  }
1381
1578
  },
@@ -1388,6 +1585,18 @@ DS.Store = Ember.Object.extend({
1388
1585
  filter = get(array, 'filterFunction');
1389
1586
  this.updateRecordArray(array, filter, type, clientId, dataProxy);
1390
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
+ }
1391
1600
  },
1392
1601
 
1393
1602
  updateRecordArray: function(array, filter, type, clientId, dataProxy) {
@@ -1473,6 +1682,24 @@ DS.Store = Ember.Object.extend({
1473
1682
  return this.pushHash(UNLOADED, id, type);
1474
1683
  },
1475
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
+
1476
1703
  // ................
1477
1704
  // . LOADING DATA .
1478
1705
  // ................
@@ -1527,7 +1754,7 @@ DS.Store = Ember.Object.extend({
1527
1754
  ids = [];
1528
1755
  var primaryKey = type.proto().primaryKey;
1529
1756
 
1530
- ids = Ember.ArrayUtils.map(hashes, function(hash) {
1757
+ ids = Ember.EnumerableUtils.map(hashes, function(hash) {
1531
1758
  return hash[primaryKey];
1532
1759
  });
1533
1760
  }
@@ -1607,7 +1834,7 @@ DS.Store = Ember.Object.extend({
1607
1834
 
1608
1835
 
1609
1836
  (function() {
1610
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
1837
+ var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
1611
1838
 
1612
1839
  /**
1613
1840
  This file encapsulates the various states that a record can transition
@@ -1636,7 +1863,7 @@ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.g
1636
1863
  string. You can determine a record's current state by getting its manager's
1637
1864
  current state path:
1638
1865
 
1639
- record.getPath('stateManager.currentState.path');
1866
+ record.get('stateManager.currentState.path');
1640
1867
  //=> "created.uncommitted"
1641
1868
 
1642
1869
  The `DS.Model` states are themselves stateless. What we mean is that,
@@ -1722,7 +1949,7 @@ var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.g
1722
1949
  state in a more user-friendly way than examining its state path. For example,
1723
1950
  instead of doing this:
1724
1951
 
1725
- var statePath = record.getPath('stateManager.currentState.path');
1952
+ var statePath = record.get('stateManager.currentState.path');
1726
1953
  if (statePath === 'created.inFlight') {
1727
1954
  doSomething();
1728
1955
  }
@@ -1889,7 +2116,7 @@ var waitingOn = function(manager, object) {
1889
2116
  // super points to the class definition.
1890
2117
  var Uncommitted = Ember.Mixin.create({
1891
2118
  setProperty: setProperty,
1892
- setAssociation: setAssociation,
2119
+ setAssociation: setAssociation
1893
2120
  });
1894
2121
 
1895
2122
  // These mixins are mixed into substates of the concrete
@@ -1983,7 +2210,7 @@ var DirtyState = DS.State.extend({
1983
2210
  t.recordBecameClean(dirtyType, record);
1984
2211
  });
1985
2212
 
1986
- manager.goToState('loaded');
2213
+ manager.goToState('saved');
1987
2214
  }
1988
2215
  }, Uncommitted),
1989
2216
 
@@ -2013,7 +2240,7 @@ var DirtyState = DS.State.extend({
2013
2240
  t.recordBecameClean('inflight', record);
2014
2241
  });
2015
2242
 
2016
- manager.goToState('loaded');
2243
+ manager.goToState('saved');
2017
2244
  manager.send('invokeLifecycleCallbacks', dirtyType);
2018
2245
  },
2019
2246
 
@@ -2164,7 +2391,7 @@ var DirtyState = DS.State.extend({
2164
2391
  errors = get(record, 'errors'),
2165
2392
  key = context.key;
2166
2393
 
2167
- delete errors[key];
2394
+ set(errors, key, null);
2168
2395
 
2169
2396
  if (!hasDefinedProperties(errors)) {
2170
2397
  manager.send('becameValid');
@@ -2182,7 +2409,7 @@ var DirtyState = DS.State.extend({
2182
2409
 
2183
2410
  invokeLifecycleCallbacks: function(manager) {
2184
2411
  var record = get(manager, 'record');
2185
- record.fire('becameInvalid', record);
2412
+ record.trigger('becameInvalid', record);
2186
2413
  }
2187
2414
  })
2188
2415
  });
@@ -2272,7 +2499,7 @@ var states = {
2272
2499
  // TRANSITIONS
2273
2500
  exit: function(manager) {
2274
2501
  var record = get(manager, 'record');
2275
- record.fire('didLoad');
2502
+ record.trigger('didLoad');
2276
2503
  },
2277
2504
 
2278
2505
  // EVENTS
@@ -2326,9 +2553,9 @@ var states = {
2326
2553
  invokeLifecycleCallbacks: function(manager, dirtyType) {
2327
2554
  var record = get(manager, 'record');
2328
2555
  if (dirtyType === 'created') {
2329
- record.fire('didCreate', record);
2556
+ record.trigger('didCreate', record);
2330
2557
  } else {
2331
- record.fire('didUpdate', record);
2558
+ record.trigger('didUpdate', record);
2332
2559
  }
2333
2560
  }
2334
2561
  }),
@@ -2431,7 +2658,7 @@ var states = {
2431
2658
 
2432
2659
  invokeLifecycleCallbacks: function(manager) {
2433
2660
  var record = get(manager, 'record');
2434
- record.fire('didDelete', record);
2661
+ record.trigger('didDelete', record);
2435
2662
  }
2436
2663
  })
2437
2664
  }),
@@ -2446,7 +2673,7 @@ var states = {
2446
2673
 
2447
2674
  invokeLifecycleCallbacks: function(manager) {
2448
2675
  var record = get(manager, 'record');
2449
- record.fire('becameError', record);
2676
+ record.trigger('becameError', record);
2450
2677
  }
2451
2678
  })
2452
2679
  })
@@ -2609,10 +2836,10 @@ DataProxy.prototype = {
2609
2836
 
2610
2837
 
2611
2838
  (function() {
2612
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none;
2839
+ var get = Ember.get, set = Ember.set, none = Ember.none;
2613
2840
 
2614
2841
  var retrieveFromCurrentState = Ember.computed(function(key) {
2615
- return get(getPath(this, 'stateManager.currentState'), key);
2842
+ return get(get(this, 'stateManager.currentState'), key);
2616
2843
  }).property('stateManager.currentState').cacheable();
2617
2844
 
2618
2845
  DS.Model = Ember.Object.extend(Ember.Evented, {
@@ -2903,12 +3130,18 @@ DS.Model = Ember.Object.extend(Ember.Evented, {
2903
3130
  cachedValue = this.cacheFor(name);
2904
3131
 
2905
3132
  if (cachedValue) {
2906
- var key = association.options.key || name,
3133
+ var key = association.options.key || get(this, 'namingConvention').keyToJSONKey(name),
2907
3134
  ids = data.get(key) || [];
2908
- var clientIds = Ember.ArrayUtils.map(ids, function(id) {
2909
- return store.clientIdForId(association.type, id);
2910
- });
2911
-
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
+
2912
3145
  set(cachedValue, 'content', Ember.A(clientIds));
2913
3146
  cachedValue.fetch();
2914
3147
  }
@@ -2922,8 +3155,8 @@ DS.Model = Ember.Object.extend(Ember.Evented, {
2922
3155
  Override the default event firing from Ember.Evented to
2923
3156
  also call methods with the given name.
2924
3157
  */
2925
- fire: function(name) {
2926
- this[name].apply(this, [].slice.call(arguments, 1));
3158
+ trigger: function(name) {
3159
+ Ember.tryInvoke(this, name, [].slice.call(arguments, 1));
2927
3160
  this._super.apply(this, arguments);
2928
3161
  }
2929
3162
  });
@@ -2943,6 +3176,7 @@ var storeAlias = function(methodName) {
2943
3176
  };
2944
3177
 
2945
3178
  DS.Model.reopenClass({
3179
+ isLoaded: storeAlias('recordIsLoaded'),
2946
3180
  find: storeAlias('find'),
2947
3181
  filter: storeAlias('filter'),
2948
3182
 
@@ -2960,7 +3194,7 @@ DS.Model.reopenClass({
2960
3194
 
2961
3195
 
2962
3196
  (function() {
2963
- var get = Ember.get, getPath = Ember.getPath;
3197
+ var get = Ember.get;
2964
3198
  DS.Model.reopenClass({
2965
3199
  attributes: Ember.computed(function() {
2966
3200
  var map = Ember.Map.create();
@@ -2985,6 +3219,17 @@ DS.Model.reopenClass({
2985
3219
  }
2986
3220
  });
2987
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
+
2988
3233
  DS.attr = function(type, options) {
2989
3234
  var transform = DS.attr.transforms[type];
2990
3235
  Ember.assert("Could not find model attribute of type " + type, !!transform);
@@ -3014,14 +3259,12 @@ DS.attr = function(type, options) {
3014
3259
 
3015
3260
  if (arguments.length === 2) {
3016
3261
  value = transformTo(value);
3017
- this.setProperty(key, value);
3018
- } else {
3019
- data = get(this, 'data');
3020
- value = get(data, key);
3021
3262
 
3022
- if (value === undefined) {
3023
- value = options.defaultValue;
3263
+ if (value !== getAttr(this, options, key)) {
3264
+ this.setProperty(key, value);
3024
3265
  }
3266
+ } else {
3267
+ value = getAttr(this, options, key);
3025
3268
  }
3026
3269
 
3027
3270
  return transformFrom(value);
@@ -3122,7 +3365,7 @@ DS.attr.transforms = {
3122
3365
 
3123
3366
 
3124
3367
  (function() {
3125
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath,
3368
+ var get = Ember.get, set = Ember.set,
3126
3369
  none = Ember.none;
3127
3370
 
3128
3371
  var embeddedFindRecord = function(store, type, data, key, one) {
@@ -3147,12 +3390,12 @@ var hasAssociation = function(type, options, one) {
3147
3390
  store = get(this, 'store');
3148
3391
 
3149
3392
  if (typeof type === 'string') {
3150
- type = getPath(this, type, false) || getPath(window, type);
3393
+ type = get(this, type, false) || get(window, type);
3151
3394
  }
3152
3395
 
3153
3396
  if (arguments.length === 2) {
3154
3397
  key = options.key || get(this, 'namingConvention').foreignKey(key);
3155
- this.send('setAssociation', { key: key, value: value === null ? null : get(value, 'clientId') });
3398
+ this.send('setAssociation', { key: key, value: Ember.none(value) ? null : get(value, 'clientId') });
3156
3399
  //data.setAssociation(key, get(value, 'clientId'));
3157
3400
  // put the client id in `key` in the data hash
3158
3401
  return value;
@@ -3185,7 +3428,7 @@ DS.belongsTo = function(type, options) {
3185
3428
 
3186
3429
 
3187
3430
  (function() {
3188
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
3431
+ var get = Ember.get, set = Ember.set;
3189
3432
  var embeddedFindRecord = function(store, type, data, key) {
3190
3433
  var association = get(data, key);
3191
3434
  return association ? store.loadMany(type, association).ids : [];
@@ -3209,12 +3452,12 @@ var hasAssociation = function(type, options) {
3209
3452
  ids, id, association;
3210
3453
 
3211
3454
  if (typeof type === 'string') {
3212
- type = getPath(this, type, false) || getPath(window, type);
3455
+ type = get(this, type, false) || get(window, type);
3213
3456
  }
3214
3457
 
3215
3458
  key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
3216
3459
  ids = findRecord(store, type, data, key);
3217
- association = store.findMany(type, ids);
3460
+ association = store.findMany(type, ids || []);
3218
3461
  set(association, 'parentRecord', this);
3219
3462
 
3220
3463
  return association;
@@ -3231,7 +3474,7 @@ DS.hasMany = function(type, options) {
3231
3474
 
3232
3475
 
3233
3476
  (function() {
3234
- var get = Ember.get, getPath = Ember.getPath;
3477
+ var get = Ember.get;
3235
3478
 
3236
3479
  DS.Model.reopenClass({
3237
3480
  typeForAssociation: function(name) {
@@ -3248,7 +3491,7 @@ DS.Model.reopenClass({
3248
3491
  typeList = map.get(type);
3249
3492
 
3250
3493
  if (typeof type === 'string') {
3251
- type = getPath(this, type, false) || getPath(window, type);
3494
+ type = get(this, type, false) || get(window, type);
3252
3495
  meta.type = type;
3253
3496
  }
3254
3497
 
@@ -3273,7 +3516,7 @@ DS.Model.reopenClass({
3273
3516
  type = meta.type;
3274
3517
 
3275
3518
  if (typeof type === 'string') {
3276
- type = getPath(this, type, false) || getPath(window, type);
3519
+ type = get(this, type, false) || get(window, type);
3277
3520
  meta.type = type;
3278
3521
  }
3279
3522
 
@@ -3430,9 +3673,28 @@ DS.Adapter = Ember.Object.extend({
3430
3673
  var set = Ember.set;
3431
3674
 
3432
3675
  Ember.onLoad('application', function(app) {
3433
- app.registerInjection(function(app, stateManager, property) {
3434
- if (property === 'Store') {
3435
- set(stateManager, 'store', app[property].create());
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
+ }
3436
3698
  }
3437
3699
  });
3438
3700
  });
@@ -3442,34 +3704,138 @@ Ember.onLoad('application', function(app) {
3442
3704
 
3443
3705
 
3444
3706
  (function() {
3445
- DS.fixtureAdapter = DS.Adapter.create({
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
+
3446
3743
  find: function(store, type, id) {
3447
- var fixtures = type.FIXTURES;
3744
+ var fixtures = this.fixturesForType(type);
3448
3745
 
3449
3746
  Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
3450
- if (fixtures.hasLoaded) { return; }
3451
3747
 
3452
- setTimeout(function() {
3453
- store.loadMany(type, fixtures);
3454
- fixtures.hasLoaded = true;
3455
- }, 300);
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
+ }
3456
3757
  },
3457
3758
 
3458
- findMany: function() {
3459
- this.find.apply(this, arguments);
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
+ }
3460
3775
  },
3461
3776
 
3462
3777
  findAll: function(store, type) {
3463
- var fixtures = type.FIXTURES;
3778
+ var fixtures = this.fixturesForType(type);
3464
3779
 
3465
3780
  Ember.assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
3466
3781
 
3467
- var ids = fixtures.map(function(item, index, self){ return item.id; });
3468
- store.loadMany(type, ids, fixtures);
3469
- }
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);
3470
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
+ }
3471
3835
  });
3472
3836
 
3837
+ DS.fixtureAdapter = DS.FixtureAdapter.create();
3838
+
3473
3839
  })();
3474
3840
 
3475
3841
 
@@ -3477,7 +3843,7 @@ DS.fixtureAdapter = DS.Adapter.create({
3477
3843
  (function() {
3478
3844
  /*global jQuery*/
3479
3845
 
3480
- var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
3846
+ var get = Ember.get, set = Ember.set;
3481
3847
 
3482
3848
  DS.RESTAdapter = DS.Adapter.extend({
3483
3849
  bulkCommit: false,
@@ -3490,13 +3856,20 @@ DS.RESTAdapter = DS.Adapter.extend({
3490
3856
 
3491
3857
  this.ajax(this.buildURL(root), "POST", {
3492
3858
  data: data,
3859
+ context: this,
3493
3860
  success: function(json) {
3494
- this.sideload(store, type, json, root);
3495
- store.didCreateRecord(record, json[root]);
3861
+ this.didCreateRecord(store, type, record, json);
3496
3862
  }
3497
3863
  });
3498
3864
  },
3499
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
+
3500
3873
  createRecords: function(store, type, records) {
3501
3874
  if (get(this, 'bulkCommit') === false) {
3502
3875
  return this._super(store, type, records);
@@ -3512,14 +3885,20 @@ DS.RESTAdapter = DS.Adapter.extend({
3512
3885
 
3513
3886
  this.ajax(this.buildURL(root), "POST", {
3514
3887
  data: data,
3515
-
3888
+ context: this,
3516
3889
  success: function(json) {
3517
- this.sideload(store, type, json, plural);
3518
- store.didCreateRecords(type, records, json[plural]);
3890
+ this.didCreateRecords(store, type, records, json);
3519
3891
  }
3520
3892
  });
3521
3893
  },
3522
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
+
3523
3902
  updateRecord: function(store, type, record) {
3524
3903
  var id = get(record, 'id');
3525
3904
  var root = this.rootForType(type);
@@ -3529,13 +3908,20 @@ DS.RESTAdapter = DS.Adapter.extend({
3529
3908
 
3530
3909
  this.ajax(this.buildURL(root, id), "PUT", {
3531
3910
  data: data,
3911
+ context: this,
3532
3912
  success: function(json) {
3533
- this.sideload(store, type, json, root);
3534
- store.didUpdateRecord(record, json && json[root]);
3913
+ this.didUpdateRecord(store, type, record, json);
3535
3914
  }
3536
3915
  });
3537
3916
  },
3538
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
+
3539
3925
  updateRecords: function(store, type, records) {
3540
3926
  if (get(this, 'bulkCommit') === false) {
3541
3927
  return this._super(store, type, records);
@@ -3551,25 +3937,37 @@ DS.RESTAdapter = DS.Adapter.extend({
3551
3937
 
3552
3938
  this.ajax(this.buildURL(root, "bulk"), "PUT", {
3553
3939
  data: data,
3940
+ context: this,
3554
3941
  success: function(json) {
3555
- this.sideload(store, type, json, plural);
3556
- store.didUpdateRecords(records, json[plural]);
3942
+ this.didUpdateRecords(store, type, records, json);
3557
3943
  }
3558
3944
  });
3559
3945
  },
3560
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
+
3561
3954
  deleteRecord: function(store, type, record) {
3562
3955
  var id = get(record, 'id');
3563
3956
  var root = this.rootForType(type);
3564
3957
 
3565
3958
  this.ajax(this.buildURL(root, id), "DELETE", {
3959
+ context: this,
3566
3960
  success: function(json) {
3567
- if (json) { this.sideload(store, type, json); }
3568
- store.didDeleteRecord(record);
3961
+ this.didDeleteRecord(store, type, record, json);
3569
3962
  }
3570
3963
  });
3571
3964
  },
3572
3965
 
3966
+ didDeleteRecord: function(store, type, record, json) {
3967
+ if (json) { this.sideload(store, type, json); }
3968
+ store.didDeleteRecord(record);
3969
+ },
3970
+
3573
3971
  deleteRecords: function(store, type, records) {
3574
3972
  if (get(this, 'bulkCommit') === false) {
3575
3973
  return this._super(store, type, records);
@@ -3585,20 +3983,25 @@ DS.RESTAdapter = DS.Adapter.extend({
3585
3983
 
3586
3984
  this.ajax(this.buildURL(root, 'bulk'), "DELETE", {
3587
3985
  data: data,
3986
+ context: this,
3588
3987
  success: function(json) {
3589
- if (json) { this.sideload(store, type, json); }
3590
- store.didDeleteRecords(records);
3988
+ this.didDeleteRecords(store, type, records, json);
3591
3989
  }
3592
3990
  });
3593
3991
  },
3594
3992
 
3993
+ didDeleteRecords: function(store, type, records, json) {
3994
+ if (json) { this.sideload(store, type, json); }
3995
+ store.didDeleteRecords(records);
3996
+ },
3997
+
3595
3998
  find: function(store, type, id) {
3596
3999
  var root = this.rootForType(type);
3597
4000
 
3598
4001
  this.ajax(this.buildURL(root, id), "GET", {
3599
4002
  success: function(json) {
3600
- store.load(type, json[root]);
3601
4003
  this.sideload(store, type, json, root);
4004
+ store.load(type, json[root]);
3602
4005
  }
3603
4006
  });
3604
4007
  },
@@ -3609,8 +4012,8 @@ DS.RESTAdapter = DS.Adapter.extend({
3609
4012
  this.ajax(this.buildURL(root), "GET", {
3610
4013
  data: { ids: ids },
3611
4014
  success: function(json) {
3612
- store.loadMany(type, json[plural]);
3613
4015
  this.sideload(store, type, json, plural);
4016
+ store.loadMany(type, json[plural]);
3614
4017
  }
3615
4018
  });
3616
4019
  },
@@ -3620,8 +4023,8 @@ DS.RESTAdapter = DS.Adapter.extend({
3620
4023
 
3621
4024
  this.ajax(this.buildURL(root), "GET", {
3622
4025
  success: function(json) {
3623
- store.loadMany(type, json[plural]);
3624
4026
  this.sideload(store, type, json, plural);
4027
+ store.loadMany(type, json[plural]);
3625
4028
  }
3626
4029
  });
3627
4030
  },
@@ -3632,8 +4035,8 @@ DS.RESTAdapter = DS.Adapter.extend({
3632
4035
  this.ajax(this.buildURL(root), "GET", {
3633
4036
  data: query,
3634
4037
  success: function(json) {
3635
- recordArray.load(json[plural]);
3636
4038
  this.sideload(store, type, json, plural);
4039
+ recordArray.load(json[plural]);
3637
4040
  }
3638
4041
  });
3639
4042
  },
@@ -3672,7 +4075,9 @@ DS.RESTAdapter = DS.Adapter.extend({
3672
4075
  },
3673
4076
 
3674
4077
  sideload: function(store, type, json, root) {
3675
- var sideloadedType, mappings;
4078
+ var sideloadedType, mappings, loaded = {};
4079
+
4080
+ loaded[root] = true;
3676
4081
 
3677
4082
  for (var prop in json) {
3678
4083
  if (!json.hasOwnProperty(prop)) { continue; }
@@ -3685,13 +4090,34 @@ DS.RESTAdapter = DS.Adapter.extend({
3685
4090
  Ember.assert("Your server returned a hash with the key " + prop + " but you have no mappings", !!mappings);
3686
4091
 
3687
4092
  sideloadedType = get(mappings, prop);
4093
+
4094
+ if (typeof sideloadedType === 'string') {
4095
+ sideloadedType = get(window, sideloadedType);
4096
+ }
4097
+
3688
4098
  Ember.assert("Your server returned a hash with the key " + prop + " but you have no mapping for it", !!sideloadedType);
3689
4099
  }
3690
4100
 
3691
- this.loadValue(store, sideloadedType, json[prop]);
4101
+ this.sideloadAssociations(store, sideloadedType, json, prop, loaded);
3692
4102
  }
3693
4103
  },
3694
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
+
3695
4121
  loadValue: function(store, type, value) {
3696
4122
  if (value instanceof Array) {
3697
4123
  store.loadMany(type, value);