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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8ac17cb9431cfb70da2b36ad364d6f6daddabfad
4
- data.tar.gz: 1ce72b88c991bcd5bd7b2b643a64c10b0da1173b
3
+ metadata.gz: 764bdacfc7a0653116882702f4d5ed982cd12cd1
4
+ data.tar.gz: 7b481038117f876a4a002fe6314ed0bbb407cf3f
5
5
  SHA512:
6
- metadata.gz: 4f030a78f2bfb6be443e26f00a3782f33730abbfea6fcdf9897fde842436e14f113cd7c2d152ed2280536ef245e67ec91629c97080f1fbb4e6a882b8a00baabc
7
- data.tar.gz: 8ced444a4e20aeabba7536f3acddfee6aad92c195f9ffbee484955e53ba0714b08b8f6b53c35d71f28694cc32a65d49d6076581b283fc9fd2fcd68f385d754f6
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[params[:pending_fetches], acting_user]
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(params[:models], params[:associations], acting_user, params[:validate])
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(params[:model], params[:id], params[:vector], acting_user)
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
- unless attributes[attribute]
200
- raise "unitialized aggregate attribute - should never happen"
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
- React::State.set_state(self, attribute, value) unless data_loading?
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?) unless data_loading?
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 = @attributes.dup
264
- @synced_attributes.each do |key, value|
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
- @synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
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
- sync_attribute(
352
- method,
353
- if association = @model.reflect_on_association(method)
354
- if association.collection?
355
- Collection.new(association.klass, @ar_instance, association, *vector, method)
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`, payload: {pending_fetches: pending_fetches}).then do |response|
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
- # we want to pass not just the model data to save, but also enough information so that on return from the server
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
- record_index = 0
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
- backing_records.each { |id, record| record.saving! }
291
+ backing_records.each { |id, record| record.saving! }
271
292
 
272
- promise = Promise.new
293
+ promise = Promise.new
273
294
 
274
- HTTP.post(`window.ReactiveRecordEnginePath`+"/save", payload: {models: models, associations: associations, validate: validate}).then do |response|
275
- begin
276
- response.json[:models] = response.json[:saved_models].collect do |item|
277
- backing_records[item[0]].ar_instance
278
- end
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
- if response.json[:success]
281
- response.json[:saved_models].each { | item | backing_records[item[0]].sync!(item[2]) }
282
- else
283
- log("Reactive Record Save Failed: #{response.json[:message]}", :error)
284
- response.json[:saved_models].each do | item |
285
- log(" Model: #{item[1]}[#{item[0]}] Attributes: #{item[2]} Errors: #{item[3]}", :error) if item[3]
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
- response.json[:saved_models].each { | item | backing_records[item[0]].errors! item[3] }
310
+ response.json[:saved_models].each { | item | backing_records[item[0]].errors! item[3] }
290
311
 
291
- yield response.json[:success], response.json[:message], response.json[:models] if block
292
- promise.resolve response.json
312
+ yield response.json[:success], response.json[:message], response.json[:models] if block
313
+ promise.resolve response.json
293
314
 
294
- backing_records.each { |id, record| record.saved! }
315
+ backing_records.each { |id, record| record.saved! }
295
316
 
296
- rescue Exception => e
297
- puts "Save Failed: #{e}"
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.save_records(models, associations, acting_user, validate)
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
- reactive_records[model_to_save[:id]] = if id
322
- record = model.find(id)
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
- else
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
- record
332
- else
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
- else
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
- ActiveRecord::Base.transaction do
350
-
351
- associations.each do |association|
352
- parent = reactive_records[association[:parent_id]]
353
- parent.instance_variable_set("@reactive_record_#{association[:attribute]}_changed", true)
354
- if parent.class.reflect_on_aggregation(association[:attribute].to_sym)
355
- puts ">>>>>>AGGREGATE>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
356
- aggregate = reactive_records[association[:child_id]]
357
- current_attributes = parent.send(association[:attribute]).attributes
358
- puts "current parent attributes = #{current_attributes}"
359
- new_attributes = aggregate.attributes
360
- puts "current child attributes = #{new_attributes}"
361
- merged_attributes = current_attributes.merge(new_attributes) { |k, current_attr, new_attr| aggregate.send("#{k}_changed?") ? new_attr : current_attr}
362
- puts "merged attributes = #{merged_attributes}"
363
- aggregate.assign_attributes(merged_attributes)
364
- puts "aggregate attributes after merge = #{aggregate.attributes}"
365
- parent.send("#{association[:attribute]}=", aggregate)
366
- puts "updated is frozen? #{aggregate.frozen?}, parent attributes = #{parent.send(association[:attribute]).attributes}"
367
- elsif parent.class.reflect_on_association(association[:attribute].to_sym).nil?
368
- raise "Missing association :#{association[:attribute]} for #{parent.class.name}. Was association defined on opal side only?"
369
- elsif parent.class.reflect_on_association(association[:attribute].to_sym).collection?
370
- puts ">>>>>>>>>> #{parent.class.name}.send('#{association[:attribute]}') << #{reactive_records[association[:child_id]]})"
371
- #parent.send("#{association[:attribute]}") << reactive_records[association[:child_id]]
372
- puts "Skipped (should be done by belongs to)"
373
- else
374
- puts ">>>>ASSOCIATION>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
375
- parent.send("#{association[:attribute]}=", reactive_records[association[:child_id]])
376
- puts "updated"
377
- end
378
- end if associations
379
-
380
- puts "!!!!!!!!!!!!associations updated"
381
-
382
- has_errors = false
383
-
384
- saved_models = reactive_records.collect do |reactive_record_id, model|
385
- puts "saving rr_id: #{reactive_record_id} model.object_id: #{model.object_id} frozen? <#{model.frozen?}>"
386
- if model.frozen?
387
- puts "validating frozen model #{model.class.name} #{model} (reactive_record_id = #{reactive_record_id})"
388
- valid = model.valid?
389
- puts "has_errors before = #{has_errors}, validate= #{validate}, !valid= #{!valid} (validate and !valid) #{validate and !valid}"
390
- has_errors ||= (validate and !valid)
391
- puts "validation complete errors = <#{!valid}>, #{model.errors.messages} has_errors #{has_errors}"
392
- [reactive_record_id, model.class.name, model.attributes, (valid ? nil : model.errors.messages)]
393
- elsif !model.id or model.changed?
394
- puts "saving #{model.class.name} #{model} (reactive_record_id = #{reactive_record_id})"
395
- saved = model.check_permission_with_acting_user(acting_user, new_models.include?(model) ? :create_permitted? : :update_permitted?).save(validate: validate)
396
- has_errors ||= !saved
397
- messages = model.errors.messages if (validate and !saved) or (!validate and !model.valid?)
398
- puts "saved complete errors = <#{!saved}>, #{messages} has_errors #{has_errors}"
399
- [reactive_record_id, model.class.name, model.attributes, messages]
400
- end
401
- end.compact
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
- raise "Could not save all models" if has_errors
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
- {success: false, saved_models: saved_models, message: e.message}
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
- apply_method_to_cache(method) { record }
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) { |cache_item| cache_item.value.send(*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 !(target.class < ActiveRecord::Base)
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.attributes[[method]] = value.first
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
 
@@ -1,3 +1,3 @@
1
1
  module ReactiveRecord
2
- VERSION = "0.7.22"
2
+ VERSION = "0.7.24"
3
3
  end
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.22
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-02 00:00:00.000000000 Z
11
+ date: 2015-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails