ember-rails 0.6.0 → 0.7.0

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