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