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 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