reactive-record 0.7.22 → 0.7.24
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 +4 -4
- data/app/controllers/reactive_record/reactive_record_controller.rb +19 -3
- data/lib/reactive_record/active_record/aggregations.rb +24 -0
- data/lib/reactive_record/active_record/class_methods.rb +28 -7
- data/lib/reactive_record/active_record/instance_methods.rb +7 -2
- data/lib/reactive_record/active_record/reactive_record/base.rb +80 -33
- data/lib/reactive_record/active_record/reactive_record/collection.rb +2 -2
- data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +214 -137
- data/lib/reactive_record/serializers.rb +3 -3
- data/lib/reactive_record/server_data_cache.rb +43 -18
- data/lib/reactive_record/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 764bdacfc7a0653116882702f4d5ed982cd12cd1
|
4
|
+
data.tar.gz: 7b481038117f876a4a002fe6314ed0bbb407cf3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a545ede62898bf66cc953aa86b05ff559dd9b4f2f356aa9c93efbed79697b2da6ceb6cec8167889d2dc9d822ead2cd8e195b2971d8b7c1cea69731be87574c5d
|
7
|
+
data.tar.gz: b4a4136d8ae962647c427d49f239a593d2404da3195e49ff69b637c8bfeae57f11914946d2a687d431bada263624d1574989b3d54b0e929ba20ef4a71d869931
|
@@ -5,16 +5,32 @@ module ReactiveRecord
|
|
5
5
|
class ReactiveRecordController < ::ApplicationController
|
6
6
|
|
7
7
|
def fetch
|
8
|
-
render :json => ReactiveRecord::ServerDataCache[
|
8
|
+
render :json => ReactiveRecord::ServerDataCache[
|
9
|
+
(params[:models] || []).map(&:with_indifferent_access),
|
10
|
+
(params[:associations] || []).map(&:with_indifferent_access),
|
11
|
+
params[:pending_fetches],
|
12
|
+
acting_user
|
13
|
+
]
|
9
14
|
end
|
10
15
|
|
11
16
|
|
12
17
|
def save
|
13
|
-
render :json => ReactiveRecord::Base.save_records(
|
18
|
+
render :json => ReactiveRecord::Base.save_records(
|
19
|
+
(params[:models] || []).map(&:with_indifferent_access),
|
20
|
+
(params[:associations] || []).map(&:with_indifferent_access),
|
21
|
+
acting_user,
|
22
|
+
params[:validate],
|
23
|
+
true
|
24
|
+
)
|
14
25
|
end
|
15
26
|
|
16
27
|
def destroy
|
17
|
-
render :json => ReactiveRecord::Base.destroy_record(
|
28
|
+
render :json => ReactiveRecord::Base.destroy_record(
|
29
|
+
params[:model],
|
30
|
+
params[:id],
|
31
|
+
params[:vector],
|
32
|
+
acting_user
|
33
|
+
)
|
18
34
|
end
|
19
35
|
|
20
36
|
end
|
@@ -19,10 +19,16 @@ module ActiveRecord
|
|
19
19
|
attr_reader :klass_name
|
20
20
|
attr_reader :attribute
|
21
21
|
attr_reader :mapped_attributes
|
22
|
+
attr_reader :constructor
|
23
|
+
|
24
|
+
def construct(args)
|
25
|
+
|
26
|
+
end
|
22
27
|
|
23
28
|
def initialize(owner_class, macro, name, options = {})
|
24
29
|
owner_class.reflect_on_all_aggregations << self
|
25
30
|
@owner_class = owner_class
|
31
|
+
@constructor = options[:constructor] || :new
|
26
32
|
@klass_name = options[:class_name] || name.camelize
|
27
33
|
@attribute = name
|
28
34
|
if options[:mapping].respond_to? :collect
|
@@ -37,6 +43,24 @@ module ActiveRecord
|
|
37
43
|
@klass ||= Object.const_get(@klass_name)
|
38
44
|
end
|
39
45
|
|
46
|
+
def serialize(object)
|
47
|
+
if object.nil?
|
48
|
+
object # return dummy value if that is what we got
|
49
|
+
else
|
50
|
+
@mapped_attributes.collect { |attr| object.send(attr) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def deserialize(array)
|
55
|
+
if array.nil?
|
56
|
+
array # return dummy value if that is what we got
|
57
|
+
elsif @constructor.respond_to?(:call)
|
58
|
+
@constructor.call(*array)
|
59
|
+
else
|
60
|
+
klass.send(@constructor, *array)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
40
64
|
end
|
41
65
|
|
42
66
|
end
|
@@ -79,6 +79,27 @@ module ActiveRecord
|
|
79
79
|
ReactiveRecord::Base.class_scopes(self)[:all] = collection
|
80
80
|
end
|
81
81
|
|
82
|
+
# def server_methods(*methods)
|
83
|
+
# methods.each do |method|
|
84
|
+
# define_method(method) do |*args|
|
85
|
+
# if args.count == 0
|
86
|
+
# @backing_record.reactive_get!(method, :initialize)
|
87
|
+
# else
|
88
|
+
# @backing_record.reactive_get!([[method]+args], :initialize)
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
# define_method("#{method}!") do |*args|
|
92
|
+
# if args.count == 0
|
93
|
+
# @backing_record.reactive_get!(method, :force)
|
94
|
+
# else
|
95
|
+
# @backing_record.reactive_get!([[method]+args], :force)
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# alias_method :server_method, :server_methods
|
102
|
+
|
82
103
|
[:belongs_to, :has_many, :has_one].each do |macro|
|
83
104
|
define_method(macro) do |*args| # is this a bug in opal? saying name, scope=nil, opts={} does not work!
|
84
105
|
name = args.first
|
@@ -106,11 +127,11 @@ module ActiveRecord
|
|
106
127
|
|
107
128
|
def _react_param_conversion(param, opt = nil)
|
108
129
|
# defines how react will convert incoming json to this ActiveRecord model
|
109
|
-
times = {start: Time.now.to_f, json_start: 0, json_end: 0, db_load_start: 0, db_load_end: 0}
|
130
|
+
#TIMING times = {start: Time.now.to_f, json_start: 0, json_end: 0, db_load_start: 0, db_load_end: 0}
|
110
131
|
param_is_native = !param.respond_to?(:is_a?) rescue true
|
111
|
-
times[:json_start] = Time.now.to_f
|
132
|
+
#TIMING times[:json_start] = Time.now.to_f
|
112
133
|
param = JSON.from_object param if param_is_native
|
113
|
-
times[:json_end] = Time.now.to_f
|
134
|
+
#TIMING times[:json_end] = Time.now.to_f
|
114
135
|
result = if param.is_a? self
|
115
136
|
param
|
116
137
|
elsif param.is_a? Hash
|
@@ -123,16 +144,16 @@ module ActiveRecord
|
|
123
144
|
else
|
124
145
|
target = new
|
125
146
|
end
|
126
|
-
times[:db_load_start] = Time.now.to_f
|
147
|
+
#TIMING times[:db_load_start] = Time.now.to_f
|
127
148
|
ReactiveRecord::Base.load_from_json(Hash[param.collect { |key, value| [key, [value]] }], target)
|
128
|
-
times[:db_load_end] = Time.now.to_f
|
149
|
+
#TIMING times[:db_load_end] = Time.now.to_f
|
129
150
|
target
|
130
151
|
end
|
131
152
|
else
|
132
153
|
nil
|
133
154
|
end
|
134
|
-
times[:end] = Time.now.to_f
|
135
|
-
#puts "times - total: #{'%.04f' % (times[:end]-times[:start])}, native conversion: #{'%.04f' % (times[:json_end]-times[:json_start])}, loading: #{'%.04f' % (times[:db_load_end]-times[:db_load_start])}"
|
155
|
+
#TIMING times[:end] = Time.now.to_f
|
156
|
+
#TIMING puts "times - total: #{'%.04f' % (times[:end]-times[:start])}, native conversion: #{'%.04f' % (times[:json_end]-times[:json_start])}, loading: #{'%.04f' % (times[:db_load_end]-times[:db_load_start])}"
|
136
157
|
result
|
137
158
|
end
|
138
159
|
|
@@ -9,6 +9,7 @@ module ActiveRecord
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize(hash = {})
|
12
|
+
|
12
13
|
if hash.is_a? ReactiveRecord::Base
|
13
14
|
@backing_record = hash
|
14
15
|
else
|
@@ -63,15 +64,19 @@ module ActiveRecord
|
|
63
64
|
end
|
64
65
|
|
65
66
|
def method_missing(name, *args, &block)
|
67
|
+
if name =~ /\!$/
|
68
|
+
name = name.gsub(/\!$/,"")
|
69
|
+
force_update = true
|
70
|
+
end
|
66
71
|
if name =~ /_changed\?$/
|
67
72
|
@backing_record.changed?(name.gsub(/_changed\?$/,""))
|
68
73
|
elsif args.count == 1 && name =~ /=$/ && !block
|
69
74
|
attribute_name = name.gsub(/=$/,"")
|
70
75
|
@backing_record.reactive_set!(attribute_name, args[0])
|
71
76
|
elsif args.count == 0 && !block
|
72
|
-
@backing_record.reactive_get!(name)
|
77
|
+
@backing_record.reactive_get!(name, force_update)
|
73
78
|
elsif !block
|
74
|
-
@backing_record.reactive_get!([[name]+args])
|
79
|
+
@backing_record.reactive_get!([[name]+args], force_update)
|
75
80
|
else
|
76
81
|
super
|
77
82
|
end
|
@@ -33,6 +33,8 @@ module ReactiveRecord
|
|
33
33
|
attr_accessor :aggregate_owner
|
34
34
|
attr_accessor :aggregate_attribute
|
35
35
|
attr_accessor :destroyed
|
36
|
+
attr_accessor :updated_during
|
37
|
+
attr_accessor :synced_attributes
|
36
38
|
|
37
39
|
# While data is being loaded from the server certain internal behaviors need to change
|
38
40
|
# for example records all record changes are synced as they happen.
|
@@ -89,6 +91,10 @@ module ReactiveRecord
|
|
89
91
|
record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
|
90
92
|
end
|
91
93
|
|
94
|
+
def self.find_by_object_id(model, object_id)
|
95
|
+
@records[model].detect { |record| record.object_id == object_id }.ar_instance
|
96
|
+
end
|
97
|
+
|
92
98
|
def self.new_from_vector(model, aggregate_owner, *vector)
|
93
99
|
# this is the equivilent of find but for associations and aggregations
|
94
100
|
# because we are not fetching a specific attribute yet, there is NO communication with the
|
@@ -121,6 +127,7 @@ module ReactiveRecord
|
|
121
127
|
@synced_attributes = {}
|
122
128
|
@attributes = {}
|
123
129
|
@changed_attributes = []
|
130
|
+
@virgin = true
|
124
131
|
records[model] << self
|
125
132
|
end
|
126
133
|
|
@@ -148,6 +155,7 @@ module ReactiveRecord
|
|
148
155
|
@ar_instance.instance_variable_set(:@backing_record, existing_record)
|
149
156
|
existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
|
150
157
|
end
|
158
|
+
value
|
151
159
|
end
|
152
160
|
|
153
161
|
def attributes
|
@@ -155,10 +163,12 @@ module ReactiveRecord
|
|
155
163
|
@attributes
|
156
164
|
end
|
157
165
|
|
158
|
-
def reactive_get!(attribute)
|
166
|
+
def reactive_get!(attribute, reload = nil)
|
167
|
+
@virgin = false unless data_loading?
|
159
168
|
unless @destroyed
|
160
169
|
if @attributes.has_key? attribute
|
161
170
|
attributes[attribute].notify if @attributes[attribute].is_a? DummyValue
|
171
|
+
apply_method(attribute) if reload
|
162
172
|
else
|
163
173
|
apply_method(attribute)
|
164
174
|
end
|
@@ -168,6 +178,7 @@ module ReactiveRecord
|
|
168
178
|
end
|
169
179
|
|
170
180
|
def reactive_set!(attribute, value)
|
181
|
+
@virgin = false unless data_loading?
|
171
182
|
unless @destroyed or (!(attributes[attribute].is_a? DummyValue) and attributes.has_key?(attribute) and attributes[attribute] == value)
|
172
183
|
if association = @model.reflect_on_association(attribute)
|
173
184
|
if association.collection?
|
@@ -194,10 +205,12 @@ module ReactiveRecord
|
|
194
205
|
attributes[attribute].attributes[inverse_of] = nil
|
195
206
|
end
|
196
207
|
end
|
197
|
-
elsif aggregation = @model.reflect_on_aggregation(attribute)
|
208
|
+
elsif aggregation = @model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
|
198
209
|
|
199
|
-
|
200
|
-
|
210
|
+
if new?
|
211
|
+
attributes[attribute] ||= aggregation.klass.new
|
212
|
+
elsif !attributes[attribute]
|
213
|
+
raise "uninitialized aggregate attribute - should never happen"
|
201
214
|
end
|
202
215
|
|
203
216
|
aggregate_record = attributes[attribute].backing_record
|
@@ -219,6 +232,17 @@ module ReactiveRecord
|
|
219
232
|
|
220
233
|
def update_attribute(attribute, *args)
|
221
234
|
value = args[0]
|
235
|
+
if args.count != 0 and data_loading?
|
236
|
+
if aggregation = model.reflect_on_aggregation(attribute) and !(aggregation.klass < ActiveRecord::Base)
|
237
|
+
@synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
|
238
|
+
else
|
239
|
+
@synced_attributes[attribute] = value
|
240
|
+
end
|
241
|
+
end
|
242
|
+
if @virgin
|
243
|
+
attributes[attribute] = value if args.count != 0
|
244
|
+
return
|
245
|
+
end
|
222
246
|
changed = if args.count == 0
|
223
247
|
if association = @model.reflect_on_association(attribute) and association.collection?
|
224
248
|
attributes[attribute] != @synced_attributes[attribute]
|
@@ -236,10 +260,16 @@ module ReactiveRecord
|
|
236
260
|
elsif !changed_attributes.include?(attribute)
|
237
261
|
changed_attributes << attribute
|
238
262
|
end
|
263
|
+
current_value = attributes[attribute]
|
239
264
|
attributes[attribute] = value if args.count != 0
|
240
|
-
|
265
|
+
if !data_loading?
|
266
|
+
React::State.set_state(self, attribute, value)
|
267
|
+
elsif on_opal_client? and current_value.loaded? and current_value != value # this is to handle changes in already loaded server side methods
|
268
|
+
#after(0.001) { React::State.set_state(self, attribute, value) }
|
269
|
+
React::State.set_state(self, attribute, value, true)
|
270
|
+
end
|
241
271
|
if empty_before != changed_attributes.empty?
|
242
|
-
React::State.set_state(self, "!CHANGED!", !changed_attributes.empty
|
272
|
+
React::State.set_state(self, "!CHANGED!", !changed_attributes.empty?, true) unless on_opal_server? or data_loading?
|
243
273
|
aggregate_owner.update_attribute(aggregate_attribute) if aggregate_owner
|
244
274
|
end
|
245
275
|
end
|
@@ -260,16 +290,8 @@ module ReactiveRecord
|
|
260
290
|
|
261
291
|
def sync!(hash = {}) # does NOT notify (see saved! for notification)
|
262
292
|
@attributes.merge! hash
|
263
|
-
@synced_attributes =
|
264
|
-
@synced_attributes.each
|
265
|
-
if value.is_a? Collection
|
266
|
-
@synced_attributes[key] = value.dup_for_sync
|
267
|
-
elsif aggregation = model.reflect_on_aggregation(key)
|
268
|
-
value.backing_record.sync!
|
269
|
-
elsif !model.reflect_on_association(key)
|
270
|
-
@synced_attributes[key] = JSON.parse(value.to_json)
|
271
|
-
end
|
272
|
-
end
|
293
|
+
@synced_attributes = {}
|
294
|
+
@synced_attributes.each { |attribute, value| sync_attribute(key, value) }
|
273
295
|
@changed_attributes = []
|
274
296
|
@saving = false
|
275
297
|
@errors = nil
|
@@ -279,8 +301,21 @@ module ReactiveRecord
|
|
279
301
|
end
|
280
302
|
|
281
303
|
def sync_attribute(attribute, value)
|
304
|
+
|
282
305
|
@synced_attributes[attribute] = attributes[attribute] = value
|
283
|
-
|
306
|
+
|
307
|
+
#@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
|
308
|
+
|
309
|
+
if value.is_a? Collection
|
310
|
+
@synced_attributes[attribute] = value.dup_for_sync
|
311
|
+
elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
|
312
|
+
value.backing_record.sync!
|
313
|
+
elsif aggregation
|
314
|
+
@synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
|
315
|
+
elsif !model.reflect_on_association(attribute)
|
316
|
+
@synced_attributes[attribute] = JSON.parse(value.to_json)
|
317
|
+
end
|
318
|
+
|
284
319
|
@changed_attributes.delete(attribute)
|
285
320
|
value
|
286
321
|
end
|
@@ -347,31 +382,43 @@ module ReactiveRecord
|
|
347
382
|
|
348
383
|
def apply_method(method)
|
349
384
|
# Fills in the value returned by sending "method" to the corresponding server side db instance
|
385
|
+
if on_opal_server? and changed?
|
386
|
+
log("Warning fetching virtual attributes (#{model.name}.#{method}) during prerendering on a changed or new model is not implemented.", :warning)
|
387
|
+
# to implement this we would have to sync up any changes during prererendering with a set the cached models (see server_data_cache)
|
388
|
+
# right now server_data cache is read only, BUT we could change this. However it seems like a tails case. Why would we create or update
|
389
|
+
# a model during prerendering???
|
390
|
+
end
|
350
391
|
if !new?
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
else
|
357
|
-
find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
|
358
|
-
end
|
359
|
-
elsif aggregation = @model.reflect_on_aggregation(method)
|
360
|
-
new_from_vector(aggregation.klass, self, *vector, method)
|
361
|
-
elsif id and id != ""
|
362
|
-
self.class.fetch_from_db([@model, [:find, id], method]) || self.class.load_from_db(*vector, method)
|
363
|
-
else # its a attribute in an aggregate or we are on the client and don't know the id
|
364
|
-
self.class.fetch_from_db([*vector, method]) || self.class.load_from_db(*vector, method)
|
392
|
+
new_value = if association = @model.reflect_on_association(method)
|
393
|
+
if association.collection?
|
394
|
+
Collection.new(association.klass, @ar_instance, association, *vector, method)
|
395
|
+
else
|
396
|
+
find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
|
365
397
|
end
|
366
|
-
)
|
398
|
+
elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
|
399
|
+
new_from_vector(aggregation.klass, self, *vector, method)
|
400
|
+
elsif id and id != ""
|
401
|
+
self.class.fetch_from_db([@model, [:find, id], *method]) || self.class.load_from_db(self, *vector, method)
|
402
|
+
else # its a attribute in an aggregate or we are on the client and don't know the id
|
403
|
+
self.class.fetch_from_db([*vector, *method]) || self.class.load_from_db(self, *vector, method)
|
404
|
+
end
|
405
|
+
new_value = @attributes[method] if new_value.is_a? DummyValue and @attributes.has_key?(method)
|
406
|
+
sync_attribute(method, new_value)
|
367
407
|
elsif association = @model.reflect_on_association(method) and association.collection?
|
368
408
|
@attributes[method] = Collection.new(association.klass, @ar_instance, association)
|
369
|
-
elsif aggregation = @model.reflect_on_aggregation(method)
|
409
|
+
elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
|
370
410
|
@attributes[method] = aggregation.klass.new.tap do |aggregate|
|
371
411
|
backing_record = aggregate.backing_record
|
372
412
|
backing_record.aggregate_owner = self
|
373
413
|
backing_record.aggregate_attribute = method
|
374
414
|
end
|
415
|
+
elsif !aggregation and method != model.primary_key
|
416
|
+
unless @attributes.has_key?(method)
|
417
|
+
log("Warning: reading from new #{model.name}.#{method} before assignment. Will fetch value from server. This may not be what you expected!!", :warning)
|
418
|
+
end
|
419
|
+
new_value = self.class.load_from_db(self, *vector, method)
|
420
|
+
new_value = @attributes[method] if new_value.is_a? DummyValue and @attributes.has_key?(method)
|
421
|
+
sync_attribute(method, new_value)
|
375
422
|
end
|
376
423
|
end
|
377
424
|
|
@@ -35,7 +35,7 @@ module ReactiveRecord
|
|
35
35
|
@collection << @target_klass.find_by(@target_klass.primary_key => id)
|
36
36
|
end
|
37
37
|
else
|
38
|
-
@dummy_collection = ReactiveRecord::Base.load_from_db(*@vector, "*all")
|
38
|
+
@dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
|
39
39
|
@dummy_record = self[0]
|
40
40
|
end
|
41
41
|
end
|
@@ -73,7 +73,7 @@ module ReactiveRecord
|
|
73
73
|
elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
|
74
74
|
@count
|
75
75
|
else
|
76
|
-
ReactiveRecord::Base.load_from_db(*@vector, "*count")
|
76
|
+
ReactiveRecord::Base.load_from_db(nil, *@vector, "*count")
|
77
77
|
@count = 1
|
78
78
|
end
|
79
79
|
end
|
@@ -8,13 +8,14 @@ module ReactiveRecord
|
|
8
8
|
|
9
9
|
before_first_mount do |context|
|
10
10
|
if RUBY_ENGINE != 'opal'
|
11
|
-
@server_data_cache = ReactiveRecord::ServerDataCache.new(context.controller.acting_user)
|
11
|
+
@server_data_cache = ReactiveRecord::ServerDataCache.new(context.controller.acting_user, {})
|
12
12
|
else
|
13
13
|
@fetch_scheduled = nil
|
14
14
|
@records = Hash.new { |hash, key| hash[key] = [] }
|
15
15
|
@class_scopes = Hash.new { |hash, key| hash[key] = {} }
|
16
16
|
if on_opal_client?
|
17
17
|
@pending_fetches = []
|
18
|
+
@pending_records = []
|
18
19
|
@last_fetch_at = Time.now
|
19
20
|
unless `typeof window.ReactiveRecordInitialData === 'undefined'`
|
20
21
|
log(["Reactive record prerendered data being loaded: %o", `window.ReactiveRecordInitialData`])
|
@@ -71,7 +72,7 @@ module ReactiveRecord
|
|
71
72
|
# queue up fetches, and at the end of each rendering cycle fetch the records
|
72
73
|
# notify that loads are pending
|
73
74
|
|
74
|
-
def self.load_from_db(*vector)
|
75
|
+
def self.load_from_db(record, *vector)
|
75
76
|
return nil unless on_opal_client? # this can happen when we are on the server and a nil value is returned for an attribute
|
76
77
|
# only called from the client side
|
77
78
|
# pushes the value of vector onto the a list of vectors that will be loaded from the server when the next
|
@@ -79,8 +80,10 @@ module ReactiveRecord
|
|
79
80
|
# takes care of informing react that there are things to load, and schedules the loader to run
|
80
81
|
# Note there is no equivilent to find_in_db, because each vector implicitly does a find.
|
81
82
|
raise "attempt to do a find_by_id of nil. This will return all records, and is not allowed" if vector[1] == ["find_by_id", nil]
|
83
|
+
vector = [record.model.model_name, ["new", record.object_id]]+vector[1..-1] if vector[0].nil?
|
82
84
|
unless data_loading?
|
83
85
|
@pending_fetches << vector
|
86
|
+
@pending_records << record if record
|
84
87
|
schedule_fetch
|
85
88
|
end
|
86
89
|
DummyValue.new
|
@@ -177,15 +180,15 @@ module ReactiveRecord
|
|
177
180
|
end
|
178
181
|
|
179
182
|
def self.schedule_fetch
|
180
|
-
#ReactiveRecord.loads_pending!
|
181
|
-
#ReactiveRecord::WhileLoading.loading!
|
182
183
|
@fetch_scheduled ||= after(0.01) do
|
183
|
-
if @pending_fetches.count > 0 # during testing we might reset the context while there are pending fetches
|
184
|
+
if @pending_fetches.count > 0 # during testing we might reset the context while there are pending fetches otherwise this would never normally happen
|
184
185
|
last_fetch_at = @last_fetch_at
|
185
186
|
pending_fetches = @pending_fetches.uniq
|
187
|
+
models, associations = gather_records(@pending_records, false, nil)
|
186
188
|
log(["Server Fetching: %o", pending_fetches.to_n])
|
187
189
|
start_time = Time.now
|
188
|
-
HTTP.post(`window.ReactiveRecordEnginePath`,
|
190
|
+
HTTP.post(`window.ReactiveRecordEnginePath`,
|
191
|
+
payload: {models: models, associations: associations, pending_fetches: pending_fetches}).then do |response|
|
189
192
|
fetch_time = Time.now
|
190
193
|
log(" Fetched in: #{(fetch_time-start_time).to_i}s")
|
191
194
|
begin
|
@@ -203,6 +206,7 @@ module ReactiveRecord
|
|
203
206
|
ReactiveRecord.run_blocks_to_load(response.body)
|
204
207
|
end
|
205
208
|
@pending_fetches = []
|
209
|
+
@pending_records = []
|
206
210
|
@last_fetch_at = Time.now
|
207
211
|
@fetch_scheduled = nil
|
208
212
|
end
|
@@ -217,6 +221,61 @@ module ReactiveRecord
|
|
217
221
|
|
218
222
|
if RUBY_ENGINE == 'opal'
|
219
223
|
|
224
|
+
def self.gather_records(records_to_process, force, record_being_saved)
|
225
|
+
# we want to pass not just the model data to save, but also enough information so that on return from the server
|
226
|
+
# we can update the models on the client
|
227
|
+
|
228
|
+
# input
|
229
|
+
# list of records to process, will grow as we chase associations
|
230
|
+
# outputs
|
231
|
+
models = [] # the actual data to save {id: record.object_id, model: record.model.model_name, attributes: changed_attributes}
|
232
|
+
associations = [] # {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
233
|
+
|
234
|
+
# used to keep track of records that have been processed for effeciency
|
235
|
+
# for quick lookup of records that have been or will be processed [record.object_id] => record
|
236
|
+
records_to_process = records_to_process.uniq
|
237
|
+
backing_records = Hash[*records_to_process.collect { |record| [record.object_id, record] }.flatten(1)]
|
238
|
+
|
239
|
+
add_new_association = lambda do |record, attribute, assoc_record|
|
240
|
+
unless backing_records[assoc_record.object_id]
|
241
|
+
records_to_process << assoc_record
|
242
|
+
backing_records[assoc_record.object_id] = assoc_record
|
243
|
+
end
|
244
|
+
associations << {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
245
|
+
end
|
246
|
+
|
247
|
+
record_index = 0
|
248
|
+
while(record_index < records_to_process.count)
|
249
|
+
record = records_to_process[record_index]
|
250
|
+
if record.id.loading? and record_being_saved
|
251
|
+
raise "Attempt to save a model while it or an associated model is still loading: model being saved: #{record_being_saved.model}:#{record_being_saved.id}#{', associated model: '+record.model.to_s if record != record_being_saved}"
|
252
|
+
end
|
253
|
+
output_attributes = {record.model.primary_key => record.id}
|
254
|
+
vector = record.vector || [record.model.model_name, ["new", record.object_id]]
|
255
|
+
models << {id: record.object_id, model: record.model.model_name, attributes: output_attributes, vector: vector}
|
256
|
+
record.attributes.each do |attribute, value|
|
257
|
+
if association = record.model.reflect_on_association(attribute)
|
258
|
+
if association.collection?
|
259
|
+
value.each { |assoc| add_new_association.call record, attribute, assoc.backing_record }
|
260
|
+
elsif value
|
261
|
+
add_new_association.call record, attribute, value.backing_record
|
262
|
+
else
|
263
|
+
output_attributes[attribute] = nil
|
264
|
+
end
|
265
|
+
elsif aggregation = record.model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
|
266
|
+
add_new_association.call record, attribute, value.backing_record
|
267
|
+
elsif aggregation
|
268
|
+
new_value = aggregation.serialize(value)
|
269
|
+
output_attributes[attribute] = new_value if record.changed?(attribute) or new_value != aggregation.serialize(record.synced_attributes[attribute])
|
270
|
+
elsif record.changed?(attribute)
|
271
|
+
output_attributes[attribute] = value
|
272
|
+
end
|
273
|
+
end if record.changed? || (record == record_being_saved && force)
|
274
|
+
record_index += 1
|
275
|
+
end
|
276
|
+
[models, associations, backing_records]
|
277
|
+
end
|
278
|
+
|
220
279
|
def save(validate, force, &block)
|
221
280
|
|
222
281
|
if data_loading?
|
@@ -225,82 +284,50 @@ module ReactiveRecord
|
|
225
284
|
|
226
285
|
elsif force or changed?
|
227
286
|
|
228
|
-
|
229
|
-
# we can update the models on the client
|
230
|
-
|
231
|
-
# input
|
232
|
-
records_to_process = [self] # list of records to process, will grow as we chase associations
|
233
|
-
# outputs
|
234
|
-
models = [] # the actual data to save {id: record.object_id, model: record.model.model_name, attributes: changed_attributes}
|
235
|
-
associations = [] # {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
236
|
-
# used to keep track of records that have been processed for effeciency
|
237
|
-
backing_records = {self.object_id => self} # for quick lookup of records that have been or will be processed [record.object_id] => record
|
238
|
-
|
239
|
-
add_new_association = lambda do |record, attribute, assoc_record|
|
240
|
-
unless backing_records[assoc_record.object_id]
|
241
|
-
records_to_process << assoc_record
|
242
|
-
backing_records[assoc_record.object_id] = assoc_record
|
243
|
-
end
|
244
|
-
associations << {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
|
245
|
-
end
|
287
|
+
begin
|
246
288
|
|
247
|
-
|
248
|
-
while(record_index < records_to_process.count)
|
249
|
-
record = records_to_process[record_index]
|
250
|
-
output_attributes = {record.model.primary_key => record.id}
|
251
|
-
models << {id: record.object_id, model: record.model.model_name, attributes: output_attributes}
|
252
|
-
record.attributes.each do |attribute, value|
|
253
|
-
if association = record.model.reflect_on_association(attribute)
|
254
|
-
if association.collection?
|
255
|
-
value.each { |assoc| add_new_association.call record, attribute, assoc.backing_record }
|
256
|
-
elsif value
|
257
|
-
add_new_association.call record, attribute, value.backing_record
|
258
|
-
else
|
259
|
-
output_attributes[attribute] = nil
|
260
|
-
end
|
261
|
-
elsif record.model.reflect_on_aggregation(attribute)
|
262
|
-
add_new_association.call record, attribute, value.backing_record
|
263
|
-
elsif record.changed?(attribute)
|
264
|
-
output_attributes[attribute] = value
|
265
|
-
end
|
266
|
-
end if record.changed? || (record == self && force)
|
267
|
-
record_index += 1
|
268
|
-
end
|
289
|
+
models, associations, backing_records = self.class.gather_records([self], force, self)
|
269
290
|
|
270
|
-
|
291
|
+
backing_records.each { |id, record| record.saving! }
|
271
292
|
|
272
|
-
|
293
|
+
promise = Promise.new
|
273
294
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
295
|
+
HTTP.post(`window.ReactiveRecordEnginePath`+"/save", payload: {models: models, associations: associations, validate: validate}).then do |response|
|
296
|
+
begin
|
297
|
+
response.json[:models] = response.json[:saved_models].collect do |item|
|
298
|
+
backing_records[item[0]].ar_instance
|
299
|
+
end
|
279
300
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
301
|
+
if response.json[:success]
|
302
|
+
response.json[:saved_models].each { | item | backing_records[item[0]].sync!(item[2]) }
|
303
|
+
else
|
304
|
+
log("Reactive Record Save Failed: #{response.json[:message]}", :error)
|
305
|
+
response.json[:saved_models].each do | item |
|
306
|
+
log(" Model: #{item[1]}[#{item[0]}] Attributes: #{item[2]} Errors: #{item[3]}", :error) if item[3]
|
307
|
+
end
|
286
308
|
end
|
287
|
-
end
|
288
309
|
|
289
|
-
|
310
|
+
response.json[:saved_models].each { | item | backing_records[item[0]].errors! item[3] }
|
290
311
|
|
291
|
-
|
292
|
-
|
312
|
+
yield response.json[:success], response.json[:message], response.json[:models] if block
|
313
|
+
promise.resolve response.json
|
293
314
|
|
294
|
-
|
315
|
+
backing_records.each { |id, record| record.saved! }
|
295
316
|
|
296
|
-
|
297
|
-
|
317
|
+
rescue Exception => e
|
318
|
+
puts "Save Failed: #{e}"
|
319
|
+
end
|
298
320
|
end
|
321
|
+
promise
|
322
|
+
rescue Exception => e
|
323
|
+
log("Exception raised while saving - #{e}", :error)
|
324
|
+
yield false, e.message, [] if block
|
325
|
+
promise.resolve({success: false, message: e.message, models: []})
|
326
|
+
promise
|
299
327
|
end
|
300
|
-
promise
|
301
328
|
else
|
302
329
|
promise = Promise.new
|
303
|
-
yield true, nil if block
|
330
|
+
yield true, nil, [] if block
|
304
331
|
promise.resolve({success: true})
|
305
332
|
promise
|
306
333
|
end
|
@@ -308,109 +335,159 @@ module ReactiveRecord
|
|
308
335
|
|
309
336
|
else
|
310
337
|
|
311
|
-
def self.
|
338
|
+
def self.find_record(model, id, vector, save)
|
339
|
+
if !save
|
340
|
+
found = vector[1..-1].inject(vector[0]) do |object, method|
|
341
|
+
if method.is_a? Array
|
342
|
+
if method[0] == "new"
|
343
|
+
object.new
|
344
|
+
else
|
345
|
+
object.send(*method)
|
346
|
+
end
|
347
|
+
elsif method.is_a? String and method[0] == "*"
|
348
|
+
object[method.gsub(/^\*/,"").to_i]
|
349
|
+
else
|
350
|
+
object.send(method)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
if id and (found.nil? or !(found.class <= model) or found.id != id)
|
354
|
+
raise "Inconsistent data sent to server - #{model.name}.find(#{id}) != [#{vector}]"
|
355
|
+
end
|
356
|
+
found
|
357
|
+
elsif id
|
358
|
+
model.find(id)
|
359
|
+
else
|
360
|
+
model.new
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def self.save_records(models, associations, acting_user, validate, save)
|
312
365
|
|
313
366
|
reactive_records = {}
|
367
|
+
vectors = {}
|
314
368
|
new_models = []
|
315
369
|
saved_models = []
|
316
370
|
|
317
371
|
models.each do |model_to_save|
|
318
372
|
attributes = model_to_save[:attributes]
|
319
373
|
model = Object.const_get(model_to_save[:model])
|
320
|
-
id = attributes.delete(model.primary_key) # if we are saving existing model primary key value will be present
|
321
|
-
|
322
|
-
|
374
|
+
id = attributes.delete(model.primary_key) if model.respond_to? :primary_key # if we are saving existing model primary key value will be present
|
375
|
+
vector = model_to_save[:vector]
|
376
|
+
vector[0] = vector[0].constantize
|
377
|
+
reactive_records[model_to_save[:id]] = vectors[vector] = record = find_record(model, id, vector, save)
|
378
|
+
if record and record.respond_to?(:id) and record.id
|
379
|
+
# we have an already exising activerecord model
|
323
380
|
keys = record.attributes.keys
|
324
381
|
attributes.each do |key, value|
|
325
382
|
if keys.include? key
|
326
383
|
record[key] = value
|
327
|
-
|
384
|
+
elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key.to_sym) and !(aggregation.klass < ActiveRecord::Base)
|
385
|
+
aggregation.mapping.each_with_index do |pair, i|
|
386
|
+
record[pair.first] = value[i]
|
387
|
+
end
|
388
|
+
elsif record.respond_to? "#{key}="
|
328
389
|
record.send("#{key}=",value)
|
390
|
+
else
|
391
|
+
# TODO once reading schema.rb on client is implemented throw an error here
|
329
392
|
end
|
330
393
|
end
|
331
|
-
|
332
|
-
|
333
|
-
record = model.new
|
394
|
+
elsif record
|
395
|
+
# either the model is new, or its not even an active record model
|
334
396
|
keys = record.attributes.keys
|
335
397
|
attributes.each do |key, value|
|
336
398
|
if keys.include? key
|
337
399
|
record[key] = value
|
338
|
-
|
400
|
+
elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key) and !(aggregation.klass < ActiveRecord::Base)
|
401
|
+
aggregation.mapping.each_with_index do |pair, i|
|
402
|
+
record[pair.first] = value[i]
|
403
|
+
end
|
404
|
+
elsif key.to_s != "id"
|
339
405
|
record.send("#{key}=",value)
|
340
406
|
end
|
341
407
|
end
|
342
408
|
new_models << record
|
343
|
-
record
|
344
409
|
end
|
345
410
|
end
|
346
411
|
|
347
412
|
puts "!!!!!!!!!!!!!!attributes updated"
|
348
413
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
puts "
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
414
|
+
associations.each do |association|
|
415
|
+
parent = reactive_records[association[:parent_id]]
|
416
|
+
next unless parent
|
417
|
+
#parent.instance_variable_set("@reactive_record_#{association[:attribute]}_changed", true) remove this????
|
418
|
+
if parent.class.reflect_on_aggregation(association[:attribute].to_sym)
|
419
|
+
puts ">>>>>>AGGREGATE>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
|
420
|
+
aggregate = reactive_records[association[:child_id]]
|
421
|
+
current_attributes = parent.send(association[:attribute]).attributes
|
422
|
+
puts "current parent attributes = #{current_attributes}"
|
423
|
+
new_attributes = aggregate.attributes
|
424
|
+
puts "current child attributes = #{new_attributes}"
|
425
|
+
merged_attributes = current_attributes.merge(new_attributes) { |k, current_attr, new_attr| aggregate.send("#{k}_changed?") ? new_attr : current_attr}
|
426
|
+
puts "merged attributes = #{merged_attributes}"
|
427
|
+
aggregate.assign_attributes(merged_attributes)
|
428
|
+
puts "aggregate attributes after merge = #{aggregate.attributes}"
|
429
|
+
parent.send("#{association[:attribute]}=", aggregate)
|
430
|
+
puts "updated is frozen? #{aggregate.frozen?}, parent attributes = #{parent.send(association[:attribute]).attributes}"
|
431
|
+
elsif parent.class.reflect_on_association(association[:attribute].to_sym).nil?
|
432
|
+
raise "Missing association :#{association[:attribute]} for #{parent.class.name}. Was association defined on opal side only?"
|
433
|
+
elsif parent.class.reflect_on_association(association[:attribute].to_sym).collection?
|
434
|
+
puts ">>>>>>>>>> #{parent.class.name}.send('#{association[:attribute]}') << #{reactive_records[association[:child_id]]})"
|
435
|
+
puts "Skipped (should be done by belongs to)"
|
436
|
+
else
|
437
|
+
puts ">>>>ASSOCIATION>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
|
438
|
+
parent.send("#{association[:attribute]}=", reactive_records[association[:child_id]])
|
439
|
+
puts "updated"
|
440
|
+
end
|
441
|
+
end if associations
|
442
|
+
|
443
|
+
puts "!!!!!!!!!!!!associations updated"
|
444
|
+
|
445
|
+
if save
|
446
|
+
|
447
|
+
ActiveRecord::Base.transaction do
|
448
|
+
|
449
|
+
has_errors = false
|
450
|
+
|
451
|
+
saved_models = reactive_records.collect do |reactive_record_id, model|
|
452
|
+
puts "saving rr_id: #{reactive_record_id} model.object_id: #{model.object_id} frozen? <#{model.frozen?}>"
|
453
|
+
if model.frozen?
|
454
|
+
puts "validating frozen model #{model.class.name} #{model} (reactive_record_id = #{reactive_record_id})"
|
455
|
+
valid = model.valid?
|
456
|
+
puts "has_errors before = #{has_errors}, validate= #{validate}, !valid= #{!valid} (validate and !valid) #{validate and !valid}"
|
457
|
+
has_errors ||= (validate and !valid)
|
458
|
+
puts "validation complete errors = <#{!valid}>, #{model.errors.messages} has_errors #{has_errors}"
|
459
|
+
[reactive_record_id, model.class.name, model.attributes, (valid ? nil : model.errors.messages)]
|
460
|
+
elsif !model.id or model.changed?
|
461
|
+
puts "saving #{model.class.name} #{model} (reactive_record_id = #{reactive_record_id})"
|
462
|
+
saved = model.check_permission_with_acting_user(acting_user, new_models.include?(model) ? :create_permitted? : :update_permitted?).save(validate: validate)
|
463
|
+
has_errors ||= !saved
|
464
|
+
messages = model.errors.messages if (validate and !saved) or (!validate and !model.valid?)
|
465
|
+
puts "saved complete errors = <#{!saved}>, #{messages} has_errors #{has_errors}"
|
466
|
+
[reactive_record_id, model.class.name, model.attributes, messages]
|
467
|
+
end
|
468
|
+
end.compact
|
402
469
|
|
403
|
-
|
470
|
+
raise "Could not save all models" if has_errors
|
471
|
+
|
472
|
+
end
|
473
|
+
|
474
|
+
{success: true, saved_models: saved_models }
|
475
|
+
|
476
|
+
else
|
477
|
+
|
478
|
+
vectors
|
404
479
|
|
405
480
|
end
|
406
481
|
|
407
|
-
{success: true, saved_models: saved_models }
|
408
482
|
|
409
483
|
rescue Exception => e
|
410
484
|
puts "exception #{e}"
|
411
485
|
puts e.backtrace.join("\n")
|
412
|
-
|
413
|
-
|
486
|
+
if save
|
487
|
+
{success: false, saved_models: saved_models, message: e.message}
|
488
|
+
else
|
489
|
+
{}
|
490
|
+
end
|
414
491
|
|
415
492
|
end
|
416
493
|
|
@@ -434,7 +511,7 @@ module ReactiveRecord
|
|
434
511
|
|
435
512
|
promise = Promise.new
|
436
513
|
|
437
|
-
if id or vector
|
514
|
+
if !data_loading? and (id or vector)
|
438
515
|
HTTP.post(`window.ReactiveRecordEnginePath`+"/destroy", payload: {model: ar_instance.model_name, id: id, vector: vector}).then do |response|
|
439
516
|
yield response.json[:success], response.json[:message] if block
|
440
517
|
promise.resolve response.json
|
@@ -463,7 +540,7 @@ module ReactiveRecord
|
|
463
540
|
record = if id
|
464
541
|
model.find(id)
|
465
542
|
else
|
466
|
-
ServerDataCache.new(acting_user)[*vector]
|
543
|
+
ServerDataCache.new(acting_user, {})[*vector]
|
467
544
|
end
|
468
545
|
record.check_permission_with_acting_user(acting_user, :destroy_permitted?).destroy
|
469
546
|
{success: true, attributes: {}}
|
@@ -1,7 +1,7 @@
|
|
1
|
-
ActiveRecord::Base.send(:define_method, :react_serializer) do
|
1
|
+
ActiveRecord::Base.send(:define_method, :react_serializer) do
|
2
2
|
serializable_hash.merge(ReactiveRecord::Base.get_type_hash(self))
|
3
3
|
end
|
4
4
|
|
5
5
|
ActiveRecord::Relation.send(:define_method, :react_serializer) do
|
6
|
-
all.react_serializer
|
7
|
-
end
|
6
|
+
all.to_a.react_serializer
|
7
|
+
end
|
@@ -87,16 +87,17 @@ module ReactiveRecord
|
|
87
87
|
|
88
88
|
class ServerDataCache
|
89
89
|
|
90
|
-
def initialize(acting_user)
|
90
|
+
def initialize(acting_user, preloaded_records)
|
91
91
|
@acting_user = acting_user
|
92
92
|
@cache = []
|
93
93
|
@requested_cache_items = []
|
94
|
+
@preloaded_records = preloaded_records
|
94
95
|
end
|
95
96
|
|
96
97
|
if RUBY_ENGINE != 'opal'
|
97
98
|
|
98
99
|
def [](*vector)
|
99
|
-
root = CacheItem.new(@cache, @acting_user, vector[0])
|
100
|
+
root = CacheItem.new(@cache, @acting_user, vector[0], @preloaded_records)
|
100
101
|
vector[1..-1].inject(root) { |cache_item, method| cache_item.apply_method method if cache_item }
|
101
102
|
vector[0] = vector[0].constantize
|
102
103
|
new_items = @cache.select { | cache_item | cache_item.root == root }
|
@@ -104,8 +105,8 @@ module ReactiveRecord
|
|
104
105
|
new_items.last.value if new_items.last
|
105
106
|
end
|
106
107
|
|
107
|
-
def self.[](vectors, acting_user)
|
108
|
-
cache = new(acting_user)
|
108
|
+
def self.[](models, associations, vectors, acting_user)
|
109
|
+
cache = new(acting_user, ReactiveRecord::Base.save_records(models, associations, acting_user, false, false))
|
109
110
|
vectors.each { |vector| cache[*vector] }
|
110
111
|
cache.as_json
|
111
112
|
end
|
@@ -129,6 +130,7 @@ module ReactiveRecord
|
|
129
130
|
class CacheItem
|
130
131
|
|
131
132
|
attr_reader :vector
|
133
|
+
attr_reader :absolute_vector
|
132
134
|
attr_reader :record_chain
|
133
135
|
attr_reader :root
|
134
136
|
attr_reader :acting_user
|
@@ -141,7 +143,7 @@ module ReactiveRecord
|
|
141
143
|
vector.last
|
142
144
|
end
|
143
145
|
|
144
|
-
def self.new(db_cache, acting_user, klass)
|
146
|
+
def self.new(db_cache, acting_user, klass, preloaded_records)
|
145
147
|
klass_constant = klass.constantize
|
146
148
|
if existing = db_cache.detect { |cached_item| cached_item.vector == [klass_constant] }
|
147
149
|
return existing
|
@@ -149,19 +151,20 @@ module ReactiveRecord
|
|
149
151
|
super
|
150
152
|
end
|
151
153
|
|
152
|
-
def initialize(db_cache, acting_user, klass)
|
154
|
+
def initialize(db_cache, acting_user, klass, preloaded_records)
|
153
155
|
klass = klass.constantize
|
154
156
|
@db_cache = db_cache
|
155
157
|
@acting_user = acting_user
|
156
|
-
@vector = [klass]
|
158
|
+
@vector = @absolute_vector = [klass]
|
157
159
|
@ar_object = klass
|
158
160
|
@record_chain = []
|
159
161
|
@parent = nil
|
160
162
|
@root = self
|
163
|
+
@preloaded_records = preloaded_records
|
161
164
|
db_cache << self
|
162
165
|
end
|
163
166
|
|
164
|
-
def apply_method_to_cache(method, &block)
|
167
|
+
def apply_method_to_cache(method, absolute_method, &block)
|
165
168
|
@db_cache.inject(nil) do | representative, cache_item |
|
166
169
|
if cache_item.vector == vector
|
167
170
|
if @ar_object.class < ActiveRecord::Base and @ar_object.attributes.has_key?(method)
|
@@ -171,6 +174,7 @@ module ReactiveRecord
|
|
171
174
|
new_ar_object = yield cache_item
|
172
175
|
cache_item.clone.instance_eval do
|
173
176
|
@vector = @vector + [method] # don't push it on since you need a new vector!
|
177
|
+
@absolute_vector = @absolute_vector + [absolute_method]
|
174
178
|
@ar_object = new_ar_object
|
175
179
|
@db_cache << self
|
176
180
|
@parent = cache_item
|
@@ -199,19 +203,31 @@ module ReactiveRecord
|
|
199
203
|
|
200
204
|
def build_new_instances(method)
|
201
205
|
if method == "*all"
|
202
|
-
apply_method_to_cache("*all") { |cache_item| cache_item.value.collect { |record| record.id } }
|
206
|
+
apply_method_to_cache("*all", "*all") { |cache_item| cache_item.value.collect { |record| record.id } }
|
203
207
|
elsif method == "*count"
|
204
|
-
apply_method_to_cache("*count") { |cache_item| cache_item.value.count }
|
208
|
+
apply_method_to_cache("*count", "*count") { |cache_item| cache_item.value.count }
|
205
209
|
elsif method == "*"
|
206
210
|
if @ar_object and @ar_object.length > 0
|
211
|
+
i = -1
|
207
212
|
@ar_object.inject(nil) do | value, record | # just using inject so we will return the last value
|
208
|
-
|
213
|
+
i += 1
|
214
|
+
apply_method_to_cache(method, "*#{i}") { |cache_item| @preloaded_records[cache_item.absolute_vector + ["*#{i}"]] || record }
|
209
215
|
end
|
210
216
|
else
|
211
|
-
apply_method_to_cache(method) {[]}
|
217
|
+
apply_method_to_cache(method, method) {[]}
|
212
218
|
end
|
213
219
|
else
|
214
|
-
apply_method_to_cache(method)
|
220
|
+
apply_method_to_cache(method, method) do |cache_item|
|
221
|
+
if preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
|
222
|
+
preloaded_value
|
223
|
+
elsif method.is_a? String and cache_item.value.class.respond_to?(:reflect_on_aggregation) and
|
224
|
+
aggregation = cache_item.value.class.reflect_on_aggregation(method.to_sym) and !(aggregation.klass < ActiveRecord::Base) and
|
225
|
+
cache_item.value.send(method)
|
226
|
+
aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }
|
227
|
+
else
|
228
|
+
cache_item.value.send(*method)
|
229
|
+
end
|
230
|
+
end
|
215
231
|
end
|
216
232
|
end
|
217
233
|
|
@@ -225,7 +241,7 @@ module ReactiveRecord
|
|
225
241
|
def as_hash(children = nil)
|
226
242
|
unless children
|
227
243
|
return {} if @ar_object.is_a?(Class) and (@ar_object < ActiveRecord::Base)
|
228
|
-
children = [@ar_object]
|
244
|
+
children = [@ar_object.is_a?(BigDecimal) ? @ar_object.to_f : @ar_object]
|
229
245
|
end
|
230
246
|
if @parent
|
231
247
|
if method == "*"
|
@@ -263,6 +279,10 @@ module ReactiveRecord
|
|
263
279
|
target.replace sorted_collection.collect { |id| target.proxy_association.klass.find(id) }
|
264
280
|
end
|
265
281
|
|
282
|
+
if id_value = tree["id"] and id_value.is_a? Array
|
283
|
+
target.id = id_value.first
|
284
|
+
end
|
285
|
+
|
266
286
|
tree.each do |method, value|
|
267
287
|
method = JSON.parse(method) rescue method
|
268
288
|
new_target = nil
|
@@ -275,15 +295,20 @@ module ReactiveRecord
|
|
275
295
|
elsif method.is_a? Integer or method =~ /^[0-9]+$/
|
276
296
|
target << (new_target = target.proxy_association.klass.find(method))
|
277
297
|
elsif method.is_a? Array
|
278
|
-
if
|
298
|
+
if method[0] == "new"
|
299
|
+
new_target = ReactiveRecord::Base.find_by_object_id(target.base_class, method[1])
|
300
|
+
elsif !(target.class < ActiveRecord::Base)
|
279
301
|
new_target = target.send *method
|
280
302
|
# value is an array if scope returns nil, so we destroy the bogus record
|
281
303
|
new_target.destroy and new_target = nil if value.is_a? Array
|
282
304
|
else
|
283
|
-
target.
|
305
|
+
target.backing_record.update_attribute([method], value.first)
|
284
306
|
end
|
307
|
+
elsif target.class.respond_to?(:reflect_on_aggregation) and aggregation = target.class.reflect_on_aggregation(method) and
|
308
|
+
!(aggregation.klass < ActiveRecord::Base)
|
309
|
+
target.send "#{method}=", aggregation.deserialize(value.first)
|
285
310
|
elsif value.is_a? Array
|
286
|
-
target.send "#{method}=", value.first
|
311
|
+
target.send "#{method}=", value.first unless method == "id" # we handle ids first so things sync nicely
|
287
312
|
elsif value.is_a? Hash and value[:id] and value[:id].first #and
|
288
313
|
association = target.class.reflect_on_association(method)
|
289
314
|
new_target = association.klass.find(value[:id].first)
|
@@ -293,7 +318,7 @@ module ReactiveRecord
|
|
293
318
|
end
|
294
319
|
load_from_json(value, new_target) if new_target
|
295
320
|
end
|
296
|
-
target.save if target.respond_to? :save
|
321
|
+
#target.save if target.respond_to? :save
|
297
322
|
end
|
298
323
|
|
299
324
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reactive-record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.24
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mitch VanDuyn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|