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