reactive-record 0.7.22 → 0.7.24
Sign up to get free protection for your applications and to get access to all the features.
- 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
|