ember-rails 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
+