reactive-record 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +29 -0
- data/app/controllers/reactive_record/application_controller.rb +4 -0
- data/app/controllers/reactive_record/reactive_record_controller.rb +22 -0
- data/config/routes.rb +5 -0
- data/lib/Gemfile +17 -0
- data/lib/reactive-record.rb +28 -0
- data/lib/reactive_record/active_record/aggregations.rb +38 -0
- data/lib/reactive_record/active_record/associations.rb +54 -0
- data/lib/reactive_record/active_record/base.rb +9 -0
- data/lib/reactive_record/active_record/class_methods.rb +113 -0
- data/lib/reactive_record/active_record/instance_methods.rb +76 -0
- data/lib/reactive_record/active_record/reactive_record/base.rb +287 -0
- data/lib/reactive_record/active_record/reactive_record/collection.rb +100 -0
- data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +274 -0
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +262 -0
- data/lib/reactive_record/engine.rb +13 -0
- data/lib/reactive_record/interval.rb +190 -0
- data/lib/reactive_record/serializers.rb +7 -0
- data/lib/reactive_record/server_data_cache.rb +250 -0
- data/lib/reactive_record/version.rb +3 -0
- metadata +191 -0
@@ -0,0 +1,287 @@
|
|
1
|
+
module ReactiveRecord
|
2
|
+
class Base
|
3
|
+
|
4
|
+
# Its all about lazy loading. This prevents us from grabbing enormous association collections, or large attributes
|
5
|
+
# unless they are explicitly requested.
|
6
|
+
|
7
|
+
# During prerendering we get each attribute as its requested and fill it in both on the javascript side, as well as
|
8
|
+
# remember that the attribute needs to be part of the download to client.
|
9
|
+
|
10
|
+
# On the client we fill in the record data with empty values (nil, or one element collections) but only as the attribute
|
11
|
+
# is requested. Each request queues up a request to get the real data from the server.
|
12
|
+
|
13
|
+
# The ReactiveRecord class serves two purposes. First it is the unique data corresponding to the last known state of a
|
14
|
+
# database record. This means All records matching a specific database record are unique. This is unlike AR but is
|
15
|
+
# important both for the lazy loading and also so that when values change react can be informed of the change.
|
16
|
+
|
17
|
+
# Secondly it serves as name space for all the ReactiveRecord specific methods, so every AR Instance has a ReactiveRecord
|
18
|
+
|
19
|
+
# Because there is no point in generating a new ar_instance everytime a search is made we cache the first ar_instance created.
|
20
|
+
# Its possible however during loading to create a new ar_instances that will in the end point to the same record.
|
21
|
+
|
22
|
+
# VECTORS... are an important concept. They are the substitute for a primary key before a record is loaded.
|
23
|
+
# Vectors have the form [ModelClass, method_call, method_call, method_call...]
|
24
|
+
|
25
|
+
# Each method call is either a simple method name or an array in the form [method_name, param, param ...]
|
26
|
+
# Example [User, [find, 123], todos, active, [due, "1/1/2016"], title]
|
27
|
+
# Roughly corresponds to this query: User.find(123).todos.active.due("1/1/2016").select(:title)
|
28
|
+
|
29
|
+
attr_accessor :ar_instance
|
30
|
+
attr_accessor :vector
|
31
|
+
attr_accessor :model
|
32
|
+
|
33
|
+
# While data is being loaded from the server certain internal behaviors need to change
|
34
|
+
# for example records all record changes are synced as they happen.
|
35
|
+
# This is implemented this way so that the ServerDataCache class can use pure active
|
36
|
+
# record methods in its implementation
|
37
|
+
|
38
|
+
def self.data_loading?
|
39
|
+
@data_loading
|
40
|
+
end
|
41
|
+
|
42
|
+
def data_loading?
|
43
|
+
self.class.data_loading?
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.load_from_json(json, target = nil)
|
47
|
+
@data_loading = true
|
48
|
+
ServerDataCache.load_from_json(json, target)
|
49
|
+
@data_loading = false
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.find(model, attribute, value)
|
53
|
+
|
54
|
+
# will return the unique record with this attribute-value pair
|
55
|
+
# value cannot be an association or aggregation
|
56
|
+
|
57
|
+
model = model.base_class
|
58
|
+
# already have a record with this attribute-value pair?
|
59
|
+
record = @records[model].detect { |record| record.attributes[attribute] == value}
|
60
|
+
|
61
|
+
unless record
|
62
|
+
# if not, and then the record may be loaded, but not have this attribute set yet,
|
63
|
+
# so find the id of of record with the attribute-value pair, and see if that is loaded.
|
64
|
+
# find_in_db returns nil if we are not prerendering which will force us to create a new record
|
65
|
+
# because there is no way of knowing the id.
|
66
|
+
if attribute != model.primary_key and id = find_in_db(model, attribute, value)
|
67
|
+
record = @records[model].detect { |record| record.id == id}
|
68
|
+
end
|
69
|
+
# if we don't have a record then create one
|
70
|
+
(record = new(model)).vector = [model, ["find_by_#{attribute}", value]] unless record
|
71
|
+
# and set the value
|
72
|
+
record.sync_attribute(attribute, value)
|
73
|
+
# and set the primary if we have one
|
74
|
+
record.sync_attribute(model.primary_key, id) if id
|
75
|
+
end
|
76
|
+
|
77
|
+
# finally initialize and return the ar_instance
|
78
|
+
record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.new_from_vector(model, aggregate_parent, *vector)
|
82
|
+
# this is the equivilent of find but for associations and aggregations
|
83
|
+
# because we are not fetching a specific attribute yet, there is NO communication with the
|
84
|
+
# server. That only happens during find.
|
85
|
+
|
86
|
+
model = model.base_class
|
87
|
+
|
88
|
+
# do we already have a record with this vector? If so return it, otherwise make a new one.
|
89
|
+
|
90
|
+
record = @records[model].detect { |record| record.vector == vector}
|
91
|
+
(record = new(model)).vector = vector unless record
|
92
|
+
record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize(model, hash = {}, ar_instance = nil)
|
97
|
+
@model = model
|
98
|
+
@attributes = hash
|
99
|
+
@synced_attributes = {}
|
100
|
+
@ar_instance = ar_instance
|
101
|
+
records[model] << self
|
102
|
+
end
|
103
|
+
|
104
|
+
def find(*args)
|
105
|
+
self.find(*args)
|
106
|
+
end
|
107
|
+
|
108
|
+
def new_from_vector(*args)
|
109
|
+
self.class.new_from_vector(*args)
|
110
|
+
end
|
111
|
+
|
112
|
+
def primary_key
|
113
|
+
@model.primary_key
|
114
|
+
end
|
115
|
+
|
116
|
+
def id
|
117
|
+
attributes[primary_key]
|
118
|
+
end
|
119
|
+
|
120
|
+
def id=(value)
|
121
|
+
# we need to keep the id unique
|
122
|
+
existing_record = records[@model].detect { |record| record.attributes[primary_key] == value}
|
123
|
+
if existing_record
|
124
|
+
@ar_instance.instance_eval { @backing_record = existing_record }
|
125
|
+
existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
|
126
|
+
else
|
127
|
+
attributes[primary_key] = value
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def attributes
|
132
|
+
@last_access_at = Time.now
|
133
|
+
@attributes
|
134
|
+
end
|
135
|
+
|
136
|
+
def reactive_get!(attribute)
|
137
|
+
unless @destroyed
|
138
|
+
apply_method(attribute) unless @attributes.has_key? attribute
|
139
|
+
React::State.get_state(self, attribute) unless data_loading?
|
140
|
+
attributes[attribute]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def reactive_set!(attribute, value)
|
145
|
+
unless @destroyed or attributes[attribute] == value
|
146
|
+
if association = @model.reflect_on_association(attribute)
|
147
|
+
if association.collection?
|
148
|
+
collection = Collection.new(association.klass, @ar_instance, association)
|
149
|
+
collection.replace(value || [])
|
150
|
+
value = collection
|
151
|
+
else
|
152
|
+
inverse_of = association.inverse_of
|
153
|
+
inverse_association = association.klass.reflect_on_association(inverse_of)
|
154
|
+
if inverse_association.collection?
|
155
|
+
if !value
|
156
|
+
attributes[attribute].attributes[inverse_of].delete(@ar_instance)
|
157
|
+
elsif value.attributes[inverse_of]
|
158
|
+
value.attributes[inverse_of] << @ar_instance
|
159
|
+
else
|
160
|
+
value.attributes[inverse_of] = Collection.new(@model, value, inverse_association)
|
161
|
+
value.attributes[inverse_of].replace [@ar_instance]
|
162
|
+
end
|
163
|
+
elsif value
|
164
|
+
attributes[attribute].attributes[inverse_of] = nil if attributes[attribute]
|
165
|
+
value.attributes[inverse_of] = @ar_instance
|
166
|
+
React::State.set_state(value.instance_variable_get(:@backing_record), inverse_of, @ar_instance) unless data_loading?
|
167
|
+
else
|
168
|
+
attributes[attribute].attributes[inverse_of] = nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
attributes[attribute] = value
|
173
|
+
React::State.set_state(self, attribute, value) unless data_loading?
|
174
|
+
end
|
175
|
+
value
|
176
|
+
end
|
177
|
+
|
178
|
+
def changed?(*args)
|
179
|
+
if args.count == 0
|
180
|
+
React::State.get_state(self, self)
|
181
|
+
@attributes != @synced_attributes
|
182
|
+
else
|
183
|
+
key = args[0]
|
184
|
+
React::State.get_state(@attributes, key)
|
185
|
+
@attributes.has_key?(key) != @synced_attributes.has_key?(key) or @attributes[key] != @synced_attributes[key]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def sync!(hash = {})
|
190
|
+
@attributes.merge! hash
|
191
|
+
@synced_attributes = @attributes.dup
|
192
|
+
@synced_attributes.each { |key, value| @synced_attributes[key] = value.dup if value.is_a? Collection }
|
193
|
+
@saving = false
|
194
|
+
React::State.set_state(self, self, :synced) unless data_loading?
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
def sync_attribute(attribute, value)
|
199
|
+
@synced_attributes[attribute] = attributes[attribute] = value
|
200
|
+
@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
|
201
|
+
value
|
202
|
+
end
|
203
|
+
|
204
|
+
def revert
|
205
|
+
@attributes.each do |attribute, value|
|
206
|
+
@ar_instance.send("#{attribute}=", @synced_attributes[attribute])
|
207
|
+
end
|
208
|
+
@attributes.delete_if { |attribute, value| !@synced_attributes.has_key?(attribute) }
|
209
|
+
end
|
210
|
+
|
211
|
+
def saving!
|
212
|
+
React::State.set_state(self, self, :saving) unless data_loading?
|
213
|
+
@saving = true
|
214
|
+
end
|
215
|
+
|
216
|
+
def saving?
|
217
|
+
React::State.get_state(self, self)
|
218
|
+
@saving
|
219
|
+
end
|
220
|
+
|
221
|
+
def find_association(association, id)
|
222
|
+
|
223
|
+
inverse_of = association.inverse_of
|
224
|
+
|
225
|
+
instance = if id
|
226
|
+
find(association.klass, association.klass.primary_key, id)
|
227
|
+
else
|
228
|
+
new_from_vector(association.klass, nil, *vector, association.attribute)
|
229
|
+
end
|
230
|
+
|
231
|
+
instance_backing_record_attributes = instance.instance_variable_get(:@backing_record).attributes
|
232
|
+
inverse_association = association.klass.reflect_on_association(inverse_of)
|
233
|
+
|
234
|
+
if inverse_association.collection?
|
235
|
+
instance_backing_record_attributes[inverse_of] = if id and id != ""
|
236
|
+
Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
|
237
|
+
else
|
238
|
+
Collection.new(@model, instance, inverse_association, *vector, association.attribute, inverse_of)
|
239
|
+
end unless instance_backing_record_attributes[inverse_of]
|
240
|
+
instance_backing_record_attributes[inverse_of].replace [@ar_instance]
|
241
|
+
else
|
242
|
+
instance_backing_record_attributes[inverse_of] = @ar_instance
|
243
|
+
end if inverse_of
|
244
|
+
instance
|
245
|
+
end
|
246
|
+
|
247
|
+
def apply_method(method)
|
248
|
+
# Fills in the value returned by sending "method" to the corresponding server side db instance
|
249
|
+
if id or vector
|
250
|
+
sync_attribute(
|
251
|
+
method,
|
252
|
+
if association = @model.reflect_on_association(method)
|
253
|
+
if association.collection?
|
254
|
+
Collection.new(association.klass, @ar_instance, association, *vector, method)
|
255
|
+
else
|
256
|
+
find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
|
257
|
+
end
|
258
|
+
elsif aggregation = @model.reflect_on_aggregation(method)
|
259
|
+
new_from_vector(aggregation.klass, self, *vector, method)
|
260
|
+
elsif id and id != ""
|
261
|
+
self.class.fetch_from_db([@model, [:find, id], method]) || self.class.load_from_db(*vector, method)
|
262
|
+
else # its a attribute in an aggregate or we are on the client and don't know the id
|
263
|
+
self.class.fetch_from_db([*vector, method]) || self.class.load_from_db(*vector, method)
|
264
|
+
end
|
265
|
+
)
|
266
|
+
elsif association = @model.reflect_on_association(method) and association.collection?
|
267
|
+
@attributes[method] = Collection.new(association.klass, @ar_instance, association)
|
268
|
+
elsif aggregation = @model.reflect_on_aggregation(method)
|
269
|
+
@attributes[method] = aggregation.klass.new
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.infer_type_from_hash(klass, hash)
|
274
|
+
klass = klass.base_class
|
275
|
+
return klass unless hash
|
276
|
+
type = hash[klass.inheritance_column]
|
277
|
+
begin
|
278
|
+
return Object.const_get(type)
|
279
|
+
rescue Exeception => e
|
280
|
+
message = "Could not subclass #{@model_klass.model_name} as #{type}. Perhaps #{type} class has not been required. Exception: #{e}"
|
281
|
+
`console.error(#{message})`
|
282
|
+
end if type
|
283
|
+
klass
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module ReactiveRecord
|
2
|
+
|
3
|
+
class Collection
|
4
|
+
|
5
|
+
def initialize(target_klass, owner = nil, association = nil, *vector)
|
6
|
+
if association and (association.macro != :has_many or association.klass != target_klass)
|
7
|
+
message = "unimplemented association #{owner} :#{association.macro} #{association.attribute}"
|
8
|
+
`console.error(#{message})`
|
9
|
+
end
|
10
|
+
@owner = owner # can be nil if this is an outer most scope
|
11
|
+
@association = association
|
12
|
+
@target_klass = target_klass
|
13
|
+
if owner and !owner.id and !owner.vector
|
14
|
+
@synced_collection = @collection = []
|
15
|
+
else
|
16
|
+
@vector = vector.count == 0 ? [target_klass] : vector
|
17
|
+
end
|
18
|
+
@scopes = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def all
|
22
|
+
unless @collection
|
23
|
+
@collection = []
|
24
|
+
if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
|
25
|
+
ids.each do |id|
|
26
|
+
@collection << @target_klass.find_by(@target_klass.primary_key => id)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
ReactiveRecord::Base.load_from_db(*@vector, "*all")
|
30
|
+
@collection << ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
@collection
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def ==(other_collection)
|
38
|
+
if @collection
|
39
|
+
@collection == other_collection.all
|
40
|
+
else
|
41
|
+
!other_collection.instance_variable_get(:@collection)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply_scope(scope)
|
46
|
+
# The value returned is another ReactiveRecordCollection with the scope added to the vector
|
47
|
+
# no additional action is taken
|
48
|
+
@scopes[scope] ||= new(@target_klass, @owner, @association, *vector, scope)
|
49
|
+
end
|
50
|
+
|
51
|
+
def proxy_association
|
52
|
+
@association
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def <<(item)
|
57
|
+
inverse_of = @association.inverse_of
|
58
|
+
if @owner and inverse_of = @association.inverse_of
|
59
|
+
item.attributes[inverse_of].attributes[@association.attribute].delete(item) if item.attributes[inverse_of] and item.attributes[inverse_of].attributes[@association.attribute]
|
60
|
+
item.attributes[inverse_of] = @owner
|
61
|
+
backing_record = item.instance_variable_get(:@backing_record)
|
62
|
+
React::State.set_state(backing_record, inverse_of, @owner) unless backing_record.data_loading?
|
63
|
+
end
|
64
|
+
all << item unless all.include? item
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def replace(new_array)
|
69
|
+
return new_array if @collection == new_array
|
70
|
+
if @collection
|
71
|
+
@collection.dup.each { |item| delete(item) }
|
72
|
+
else
|
73
|
+
@collection = []
|
74
|
+
end
|
75
|
+
new_array.each { |item| self << item }
|
76
|
+
new_array
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete(item)
|
80
|
+
if @owner and inverse_of = @association.inverse_of
|
81
|
+
item.attributes[inverse_of] = nil
|
82
|
+
backing_record = item.instance_variable_get(:@backing_record)
|
83
|
+
React::State.set_state(backing_record, inverse_of, nil) unless backing_record.data_loading?
|
84
|
+
end
|
85
|
+
all.delete(item)
|
86
|
+
end
|
87
|
+
|
88
|
+
def method_missing(method, *args, &block)
|
89
|
+
if [].respond_to? method
|
90
|
+
all.send(method, *args, &block)
|
91
|
+
elsif @target_klass.respond_to? method
|
92
|
+
apply_scope(method)
|
93
|
+
else
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ReactiveRecord
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
include React::IsomorphicHelpers
|
8
|
+
|
9
|
+
before_first_mount do
|
10
|
+
if RUBY_ENGINE != 'opal'
|
11
|
+
@server_data_cache = ReactiveRecord::ServerDataCache.new
|
12
|
+
else
|
13
|
+
@records = Hash.new { |hash, key| hash[key] = [] }
|
14
|
+
if on_opal_client?
|
15
|
+
@pending_fetches = []
|
16
|
+
@last_fetch_at = Time.now
|
17
|
+
JSON.from_object(`window.ReactiveRecordInitialData`).each do |hash|
|
18
|
+
load_from_json hash
|
19
|
+
end unless `typeof window.ReactiveRecordInitialData === 'undefined'`
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def records
|
25
|
+
self.class.instance_variable_get(:@records)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Prerendering db access (returns nil if on client):
|
29
|
+
# at end of prerendering dumps all accessed records in the footer
|
30
|
+
|
31
|
+
isomorphic_method(:fetch_from_db) do |f, vector|
|
32
|
+
# vector must end with either "*all", or be a simple attribute
|
33
|
+
f.send_to_server [vector.shift.name, *vector] if RUBY_ENGINE == 'opal'
|
34
|
+
f.when_on_server { @server_data_cache[*vector] }
|
35
|
+
end
|
36
|
+
|
37
|
+
isomorphic_method(:find_in_db) do |f, klass, attribute, value|
|
38
|
+
f.send_to_server klass.name, attribute, value if RUBY_ENGINE == 'opal'
|
39
|
+
f.when_on_server { @server_data_cache[klass, ["find_by_#{attribute}", value], :id] }
|
40
|
+
end
|
41
|
+
|
42
|
+
prerender_footer do
|
43
|
+
json = @server_data_cache.as_json.to_json # can this just be to_json?
|
44
|
+
@server_data_cache.clear_requests
|
45
|
+
path = ::Rails.application.routes.routes.detect { |route| route.app.app == ReactiveRecord::Engine }.path.spec
|
46
|
+
"<script type='text/javascript'>\n"+
|
47
|
+
"window.ReactiveRecordEnginePath = '#{path}';\n"+
|
48
|
+
"if (typeof window.ReactiveRecordInitialData === 'undefined') { window.ReactiveRecordInitialData = [] }\n" +
|
49
|
+
"window.ReactiveRecordInitialData.push(#{json})\n"+
|
50
|
+
"</script>\n"
|
51
|
+
end if RUBY_ENGINE != 'opal'
|
52
|
+
|
53
|
+
# Client side db access (never called during prerendering):
|
54
|
+
# queue up fetches, and at the end of each rendering cycle fetch the records
|
55
|
+
# notify that loads are pending
|
56
|
+
|
57
|
+
def self.load_from_db(*vector)
|
58
|
+
# only called from the client side
|
59
|
+
# pushes the value of vector onto the a list of vectors that will be loaded from the server when the next
|
60
|
+
# rendering cycle completes.
|
61
|
+
# takes care of informing react that there are things to load, and schedules the loader to run
|
62
|
+
# Note there is no equivilent to find_in_db, because each vector implicitly does a find.
|
63
|
+
return "" if data_loading?
|
64
|
+
ReactiveRecord.loads_pending!
|
65
|
+
ReactiveRecord::WhileLoading.loading! # inform react that the current value is bogus
|
66
|
+
@pending_fetches << vector
|
67
|
+
schedule_fetch
|
68
|
+
""
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.schedule_fetch
|
72
|
+
@fetch_scheduled ||= after(0.001) do
|
73
|
+
last_fetch_at = @last_fetch_at
|
74
|
+
HTTP.post(`window.ReactiveRecordEnginePath`, payload: {pending_fetches: @pending_fetches.uniq}).then do |response|
|
75
|
+
begin
|
76
|
+
ReactiveRecord::Base.load_from_json(response.json)
|
77
|
+
rescue Exception => e
|
78
|
+
message = "Exception raised while loading json from server: #{e}"
|
79
|
+
`console.error(#{message})`
|
80
|
+
end
|
81
|
+
ReactiveRecord.run_blocks_to_load
|
82
|
+
ReactiveRecord::WhileLoading.loaded_at last_fetch_at
|
83
|
+
end if @pending_fetches.count > 0
|
84
|
+
@pending_fetches = []
|
85
|
+
@last_fetch_at = Time.now
|
86
|
+
@fetch_scheduled = nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.get_type_hash(record)
|
91
|
+
{record.class.inheritance_column => record[record.class.inheritance_column]}
|
92
|
+
end
|
93
|
+
|
94
|
+
# save records
|
95
|
+
|
96
|
+
if RUBY_ENGINE == 'opal'
|
97
|
+
|
98
|
+
def save(&block)
|
99
|
+
|
100
|
+
if data_loading?
|
101
|
+
|
102
|
+
sync!
|
103
|
+
|
104
|
+
elsif changed?
|
105
|
+
|
106
|
+
# we want to pass not just the model data to save, but also enough information so that on return from the server
|
107
|
+
# we can update the models on the client
|
108
|
+
|
109
|
+
# input
|
110
|
+
records_to_process = [self] # list of records to process, will grow as we chase associations
|
111
|
+
# outputs
|
112
|
+
models = [] # the actual data to save {id: record.object_id, model: record.model.model_name, attributes: changed_attributes}
|
113
|
+
associations = [] # {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
114
|
+
# used to keep track of records that have been processed for effeciency
|
115
|
+
backing_records = {self.object_id => self} # for quick lookup of records that have been or will be processed [record.object_id] => record
|
116
|
+
|
117
|
+
add_new_association = lambda do |record, attribute, assoc_record|
|
118
|
+
if assoc_record.changed?
|
119
|
+
unless backing_records[assoc_record.object_id]
|
120
|
+
records_to_process << assoc_record
|
121
|
+
backing_records[assoc_record.object_id] = assoc_record
|
122
|
+
end
|
123
|
+
associations << {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
record_index = 0
|
128
|
+
while(record_index < records_to_process.count)
|
129
|
+
record = records_to_process[record_index]
|
130
|
+
output_attributes = {record.model.primary_key => record.id}
|
131
|
+
models << {id: record.object_id, model: record.model.model_name, attributes: output_attributes}
|
132
|
+
record.attributes.each do |attribute, value|
|
133
|
+
if association = record.model.reflect_on_association(attribute)
|
134
|
+
if association.collection?
|
135
|
+
value.each { |assoc| add_new_association.call record, attribute, assoc.instance_variable_get(:@backing_record) }
|
136
|
+
else
|
137
|
+
add_new_association.call record, attribute, value.instance_variable_get(:@backing_record)
|
138
|
+
end
|
139
|
+
elsif record.model.reflect_on_aggregation(attribute)
|
140
|
+
add_new_association.call record, attribute, value.instance_variable_get(:@backing_record)
|
141
|
+
elsif record.changed?(attribute)
|
142
|
+
output_attributes[attribute] = value
|
143
|
+
end
|
144
|
+
end
|
145
|
+
record_index += 1
|
146
|
+
end
|
147
|
+
|
148
|
+
backing_records.each { |id, record| record.saving! }
|
149
|
+
|
150
|
+
promise = Promise.new
|
151
|
+
|
152
|
+
HTTP.post(`window.ReactiveRecordEnginePath`+"/save", payload: {models: models, associations: associations}).then do |response|
|
153
|
+
|
154
|
+
response.json[:saved_models].each do |item|
|
155
|
+
internal_id, klass, attributes = item
|
156
|
+
backing_records[internal_id].sync!(attributes)
|
157
|
+
end
|
158
|
+
yield response.json[:success], response.json[:message] if block
|
159
|
+
promise.resolve response.json[:success], response.json[:message]
|
160
|
+
end
|
161
|
+
promise
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
else
|
166
|
+
|
167
|
+
def self.save_records(models, associations)
|
168
|
+
|
169
|
+
reactive_records = {}
|
170
|
+
|
171
|
+
models.each do |model_to_save|
|
172
|
+
attributes = model_to_save[:attributes]
|
173
|
+
model = Object.const_get(model_to_save[:model])
|
174
|
+
id = attributes[model.primary_key] # if we are saving existing model primary key value will be present
|
175
|
+
reactive_records[model_to_save[:id]] = if id
|
176
|
+
record = model.find(id)
|
177
|
+
keys = record.attributes.keys
|
178
|
+
attributes.each do |key, value|
|
179
|
+
record[key] = value if keys.include? key
|
180
|
+
end
|
181
|
+
record
|
182
|
+
else
|
183
|
+
record = model.new
|
184
|
+
keys = record.attributes.keys
|
185
|
+
attributes.each do |key, value|
|
186
|
+
record[key] = value if keys.include? key
|
187
|
+
end
|
188
|
+
record
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
associations.each do |association|
|
193
|
+
begin
|
194
|
+
if reactive_records[association[:parent_id]].class.reflect_on_aggregation(association[:attribute].to_sym)
|
195
|
+
reactive_records[association[:parent_id]].send("#{association[:attribute]}=", reactive_records[association[:child_id]])
|
196
|
+
elsif reactive_records[association[:parent_id]].class.reflect_on_association(association[:attribute].to_sym).collection?
|
197
|
+
reactive_records[association[:parent_id]].send("#{association[:attribute]}") << reactive_records[association[:child_id]]
|
198
|
+
else
|
199
|
+
reactive_records[association[:parent_id]].send("#{association[:attribute]}=", reactive_records[association[:child_id]])
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end if associations
|
203
|
+
|
204
|
+
saved_models = reactive_records.collect do |reactive_record_id, model|
|
205
|
+
unless model.frozen?
|
206
|
+
saved = model.save
|
207
|
+
[reactive_record_id, model.class.name, model.attributes, saved]
|
208
|
+
end
|
209
|
+
end.compact
|
210
|
+
|
211
|
+
{success: true, saved_models: saved_models || []}
|
212
|
+
|
213
|
+
rescue Exception => e
|
214
|
+
|
215
|
+
{success: false, saved_models: saved_models || [], message: e.message}
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
# destroy records
|
222
|
+
|
223
|
+
if RUBY_ENGINE == 'opal'
|
224
|
+
|
225
|
+
def destroy(&block)
|
226
|
+
|
227
|
+
return if @destroyed
|
228
|
+
|
229
|
+
model.reflect_on_all_associations.each do |association|
|
230
|
+
if association.collection?
|
231
|
+
attributes[association.attribute].replace([]) if attributes[association.attribute]
|
232
|
+
else
|
233
|
+
@ar_instance.send("#{association.attribute}=", nil)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
promise = Promise.new
|
238
|
+
|
239
|
+
if id or vector
|
240
|
+
HTTP.post(`window.ReactiveRecordEnginePath`+"/destroy", payload: {model: ar_instance.model_name, id: id, vector: vector}).then do |response|
|
241
|
+
yield response.json[:success], response.json[:message] if block
|
242
|
+
promise.resolve response.json[:success], response.json[:message]
|
243
|
+
end
|
244
|
+
else
|
245
|
+
yield true, nil if block
|
246
|
+
promise.resolve true, nil
|
247
|
+
end
|
248
|
+
|
249
|
+
@attributes = {}
|
250
|
+
sync!
|
251
|
+
@destroyed = true
|
252
|
+
|
253
|
+
promise
|
254
|
+
end
|
255
|
+
|
256
|
+
else
|
257
|
+
|
258
|
+
def self.destroy_record(model, id, vector)
|
259
|
+
model = Object.const_get(model)
|
260
|
+
record = if id
|
261
|
+
model.find(id)
|
262
|
+
else
|
263
|
+
ServerDataCache.new[*vector]
|
264
|
+
end
|
265
|
+
record.destroy
|
266
|
+
{success: true, attributes: {}}
|
267
|
+
rescue Exception => e
|
268
|
+
{success: false, record: record, message: e.message}
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|