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