hyper-model 1.0.alpha1.1 → 1.0.alpha1.6

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +0 -1
  4. data/Gemfile +6 -6
  5. data/Rakefile +27 -3
  6. data/hyper-model.gemspec +10 -19
  7. data/lib/active_record_base.rb +101 -33
  8. data/lib/hyper-model.rb +4 -2
  9. data/lib/hyper_model/version.rb +1 -1
  10. data/lib/hyper_react/input_tags.rb +2 -1
  11. data/lib/reactive_record/active_record/associations.rb +130 -34
  12. data/lib/reactive_record/active_record/base.rb +17 -0
  13. data/lib/reactive_record/active_record/class_methods.rb +124 -52
  14. data/lib/reactive_record/active_record/error.rb +2 -0
  15. data/lib/reactive_record/active_record/errors.rb +10 -6
  16. data/lib/reactive_record/active_record/instance_methods.rb +74 -6
  17. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
  18. data/lib/reactive_record/active_record/reactive_record/base.rb +56 -30
  19. data/lib/reactive_record/active_record/reactive_record/collection.rb +219 -70
  20. data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
  21. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
  22. data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
  23. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +73 -46
  24. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
  25. data/lib/reactive_record/active_record/reactive_record/operations.rb +10 -3
  26. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -0
  27. data/lib/reactive_record/active_record/reactive_record/setters.rb +108 -71
  28. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +258 -41
  29. data/lib/reactive_record/broadcast.rb +62 -25
  30. data/lib/reactive_record/interval.rb +3 -3
  31. data/lib/reactive_record/permissions.rb +14 -2
  32. data/lib/reactive_record/scope_description.rb +3 -2
  33. data/lib/reactive_record/server_data_cache.rb +99 -49
  34. data/polymorph-notes.md +143 -0
  35. data/spec_fails.txt +3 -0
  36. metadata +49 -162
  37. data/Gemfile.lock +0 -431
@@ -2,28 +2,39 @@ module ReactiveRecord
2
2
  class Broadcast
3
3
 
4
4
  def self.after_commit(operation, model)
5
+ # Calling public_columns_hash once insures all policies are loaded
6
+ # before the first broadcast.
7
+ @public_columns_hash ||= ActiveRecord::Base.public_columns_hash
5
8
  Hyperstack::InternalPolicy.regulate_broadcast(model) do |data|
9
+ puts "Broadcast aftercommit hook: #{data}" if Hyperstack::Connection.show_diagnostics
10
+
6
11
  if !Hyperstack.on_server? && Hyperstack::Connection.root_path
7
- send_to_server(operation, data) rescue nil # server no longer running so ignore
12
+ send_to_server(operation, data, model.__synchromesh_update_time) rescue nil # fails if server no longer running so ignore
8
13
  else
9
- SendPacket.run(data, operation: operation)
14
+ SendPacket.run(data, operation: operation, updated_at: model.__synchromesh_update_time)
10
15
  end
11
16
  end
12
17
  rescue ActiveRecord::StatementInvalid => e
13
18
  raise e unless e.message == "Could not find table 'hyperstack_connections'"
14
19
  end unless RUBY_ENGINE == 'opal'
15
20
 
16
- def self.send_to_server(operation, data)
21
+ def self.send_to_server(operation, data, updated_at)
17
22
  salt = SecureRandom.hex
18
23
  authorization = Hyperstack.authorization(salt, data[:channel], data[:broadcast_id])
19
24
  raise 'no server running' unless Hyperstack::Connection.root_path
20
- SendPacket.remote(
21
- Hyperstack::Connection.root_path,
22
- data,
23
- operation: operation,
24
- salt: salt,
25
- authorization: authorization
26
- ).tap { |p| raise p.error if p.rejected? }
25
+ Timeout::timeout(Hyperstack.send_to_server_timeout) do
26
+ SendPacket.remote(
27
+ Hyperstack::Connection.root_path,
28
+ data,
29
+ operation: operation,
30
+ updated_at: updated_at,
31
+ salt: salt,
32
+ authorization: authorization
33
+ ).tap { |p| raise p.error if p.rejected? }
34
+ end
35
+ rescue Timeout::Error
36
+ puts "\n********* FAILED TO RECEIVE RESPONSE FROM SERVER WITHIN #{Hyperstack.send_to_server_timeout} SECONDS. CHANGES WILL NOT BE SYNCED ************\n"
37
+ raise 'no server running'
27
38
  end unless RUBY_ENGINE == 'opal'
28
39
 
29
40
  class SendPacket < Hyperstack::ServerOp
@@ -37,6 +48,7 @@ module ReactiveRecord
37
48
  param :record
38
49
  param :operation
39
50
  param :previous_changes
51
+ param :updated_at
40
52
 
41
53
  unless RUBY_ENGINE == 'opal'
42
54
  validate do
@@ -60,11 +72,11 @@ module ReactiveRecord
60
72
  ReactiveRecord::Collection.sync_scopes broadcast.process_previous_changes
61
73
  end
62
74
  end
63
- end
75
+ end if RUBY_ENGINE == 'opal'
64
76
 
65
77
  def self.to_self(record, data = {})
66
78
  # simulate incoming packet after a local save
67
- operation = if record.new?
79
+ operation = if record.new_record?
68
80
  :create
69
81
  elsif record.destroyed?
70
82
  :destroy
@@ -78,7 +90,7 @@ module ReactiveRecord
78
90
 
79
91
  def record_with_current_values
80
92
  ReactiveRecord::Base.load_data do
81
- backing_record = @backing_record || klass.find(record[:id]).backing_record
93
+ backing_record = @backing_record || klass.find(record[klass.primary_key]).backing_record
82
94
  if destroyed?
83
95
  backing_record.ar_instance
84
96
  else
@@ -105,6 +117,10 @@ module ReactiveRecord
105
117
  @destroyed
106
118
  end
107
119
 
120
+ def local?
121
+ @is_local
122
+ end
123
+
108
124
  def klass
109
125
  Object.const_get(@klass)
110
126
  end
@@ -116,6 +132,7 @@ module ReactiveRecord
116
132
  # private
117
133
 
118
134
  attr_reader :record
135
+ attr_reader :updated_at
119
136
 
120
137
  def self.open_channels
121
138
  @open_channels ||= Set.new
@@ -125,7 +142,7 @@ module ReactiveRecord
125
142
  @in_transit ||= Hash.new { |h, k| h[k] = new(k) }
126
143
  end
127
144
 
128
- def initialize(id)
145
+ def initialize(id = nil)
129
146
  @id = id
130
147
  @received = Set.new
131
148
  @record = {}
@@ -134,19 +151,15 @@ module ReactiveRecord
134
151
 
135
152
  def local(operation, record, data)
136
153
  @destroyed = operation == :destroy
154
+ @is_local = true
137
155
  @is_new = operation == :create
138
156
  @klass = record.class.name
139
157
  @record = data
140
158
  record.backing_record.destroyed = false
141
- @record[:id] = record.id if record.id
159
+ @record[record.primary_key] = record.id if record.id
142
160
  record.backing_record.destroyed = @destroyed
143
161
  @backing_record = record.backing_record
144
162
  @previous_changes = record.changes
145
- # attributes = record.attributes
146
- # data.each do |k, v|
147
- # next if klass.reflect_on_association(k) || attributes[k] == v
148
- # @previous_changes[k] = [attributes[k], v]
149
- # end
150
163
  self
151
164
  end
152
165
 
@@ -157,10 +170,35 @@ module ReactiveRecord
157
170
  @klass ||= params.klass
158
171
  @record.merge! params.record
159
172
  @previous_changes.merge! params.previous_changes
173
+ @updated_at = params.updated_at
160
174
  ReactiveRecord::Base.when_not_saving(klass) do
161
- @backing_record = ReactiveRecord::Base.exists?(klass, params.record[:id])
162
- @is_new = params.operation == :create && !@backing_record
163
- yield complete! if @channels == @received
175
+ @backing_record = ReactiveRecord::Base.exists?(klass, params.record[klass.primary_key])
176
+
177
+ # first check to see if we already destroyed it and if so exit the block
178
+ break if @backing_record&.destroyed
179
+
180
+ # We ignore whether the record is being created or not, and just check and see if in our
181
+ # local copy we have ever loaded this id before. If we have then its not new to us.
182
+ # BUT if we are destroying a record then it can't be treated as new regardless.
183
+ # This is because we might be just doing a count on a scope and so no actual records will
184
+ # exist. Treating a destroyed record as "new" would cause us to first increment the
185
+ # scope counter and then decrement for the destroy, resulting in a nop instead of a -1 on
186
+ # the scope count.
187
+ @is_new = !@backing_record&.id_loaded? && !@destroyed
188
+
189
+ # it is possible that we are recieving data on a record for which we are also waiting
190
+ # on an an inital data load in which case we have not yet set the loaded id, so we
191
+ # set if now.
192
+ @backing_record&.loaded_id = params.record[klass.primary_key]
193
+
194
+ # once we have received all the data from all the channels (applies to create and update only)
195
+ # we yield and process the record
196
+
197
+ # pusher fake can send duplicate records which will result in a nil broadcast
198
+ # so we also check that before yielding
199
+ if @channels == @received && (broadcast = complete!)
200
+ yield broadcast
201
+ end
164
202
  end
165
203
  end
166
204
 
@@ -205,7 +243,7 @@ module ReactiveRecord
205
243
 
206
244
  def merge_current_values(br)
207
245
  current_values = Hash[*@previous_changes.collect do |attr, values|
208
- value = attr == :id ? record[:id] : values.first
246
+ value = attr == klass.primary_key ? record[klass.primary_key] : values.first
209
247
  if br.attributes.key?(attr) &&
210
248
  br.attributes[attr] != br.convert(attr, value) &&
211
249
  br.attributes[attr] != br.convert(attr, values.last)
@@ -216,7 +254,6 @@ module ReactiveRecord
216
254
  end
217
255
  [attr, value]
218
256
  end.compact.flatten(1)]
219
- # TODO: verify - it used to be current_values.merge(br.attributes)
220
257
  klass._react_param_conversion(br.attributes.merge(current_values))
221
258
  end
222
259
  end
@@ -168,12 +168,12 @@ end
168
168
  module Kernel
169
169
  # (see Browser::Window#after)
170
170
  def after(time, &block)
171
- `setTimeout(#{block.to_n}, time * 1000)`
171
+ $window.after(time, &block)
172
172
  end
173
173
 
174
174
  # (see Browser::Window#after!)
175
175
  def after!(time, &block)
176
- `setTimeout(#{block.to_n}, time * 1000)`
176
+ $window.after!(time, &block)
177
177
  end
178
178
  end
179
179
 
@@ -187,4 +187,4 @@ class Proc
187
187
  def after!(time)
188
188
  $window.after!(time, &self)
189
189
  end
190
- end
190
+ end
@@ -90,7 +90,7 @@ class ActiveRecord::Base
90
90
  def belongs_to(attr_name, *args)
91
91
  belongs_to_without_reactive_record_add_is_method(attr_name, *args).tap do
92
92
  define_method "#{attr_name}_is?".to_sym do |model|
93
- self.class.reflections[attr_name].foreign_key == model.id
93
+ attributes[self.class.reflections[attr_name.to_s].foreign_key] == model.id
94
94
  end
95
95
  end
96
96
  end
@@ -103,7 +103,19 @@ class ActiveRecord::Base
103
103
  self.acting_user = old
104
104
  self
105
105
  else
106
- Hyperstack::InternalPolicy.raise_operation_access_violation(:crud_access_violation, "for #{self} - #{permission}(#{args}) acting_user: #{user}")
106
+ acting_user_string =
107
+ if acting_user
108
+ id = user.respond_to?(:id) ? user.id : user
109
+ "not allowed for acting_user: <##{user.class} id: #{user}>"
110
+ else
111
+ "not allowed without acting_user (acting_user = nil)"
112
+ end
113
+
114
+ message = "CRUD access violation: <##{self.class} id: #{self.id}> - #{permission}(#{args}) #{acting_user_string}"
115
+ if permission == :view_permitted?
116
+ details = Hyperstack::PolicyDiagnostics.policy_dump_for(self, user)
117
+ end
118
+ Hyperstack::InternalPolicy.raise_operation_access_violation(message, details || '')
107
119
  end
108
120
  end
109
121
 
@@ -93,8 +93,9 @@ module ReactiveRecord
93
93
  vector = []
94
94
  path.split('.').inject(@model) do |model, attribute|
95
95
  association = model.reflect_on_association(attribute)
96
- raise build_error(path, model, attribute) unless association
97
- vector = [association.inverse_of, *vector]
96
+ inverse_of = association.inverse_of if association
97
+ raise build_error(path, model, attribute) unless inverse_of
98
+ vector = [inverse_of, *vector]
98
99
  @joins[association.klass] << vector
99
100
  association.klass
100
101
  end
@@ -229,6 +229,26 @@ module ReactiveRecord
229
229
  @db_cache.add_item_to_cache self
230
230
  end
231
231
 
232
+ def to_s
233
+ acting_user_string =
234
+ if acting_user
235
+ " - with acting user: <#{acting_user.class.name} id: #{acting_user.id}>"
236
+ else
237
+ ' - with no acting user'
238
+ end
239
+ vector.collect do |e|
240
+ if e.is_a? String
241
+ e
242
+ elsif e.is_a? Array
243
+ e.length > 1 ? "#{e.first}(#{e[1..-1].join(', ')})" : e.first
244
+ else
245
+ e.name
246
+ end
247
+ end.join('.') + acting_user_string
248
+ rescue
249
+ vector.to_s + acting_user_string
250
+ end
251
+
232
252
  def start_timing(&block)
233
253
  ServerDataCache.class.start_timing(&block)
234
254
  end
@@ -240,45 +260,54 @@ module ReactiveRecord
240
260
  def apply_method_to_cache(method)
241
261
  @db_cache.cache.inject(nil) do |representative, cache_item|
242
262
  if cache_item.vector == vector
243
- if method == "*"
244
- # apply_star does the security check if value is present
245
- cache_item.apply_star || representative
246
- elsif method == "*all"
247
- # if we secure the collection then we assume its okay to read the ids
248
- secured_value = cache_item.value.__secure_collection_check(@acting_user)
249
- cache_item.build_new_cache_item(timing(:active_record) { secured_value.collect { |record| record.id } }, method, method)
250
- elsif method == "*count"
251
- secured_value = cache_item.value.__secure_collection_check(@acting_user)
252
- cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.__secure_collection_check(@acting_user).count }, method, method)
253
- elsif preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
254
- # no security check needed since we already evaluated this
255
- cache_item.build_new_cache_item(preloaded_value, method, method)
256
- elsif aggregation = cache_item.aggregation?(method)
257
- # aggregations are not protected
258
- cache_item.build_new_cache_item(aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }, method, method)
259
- else
260
- if !cache_item.value || cache_item.value.is_a?(Array)
261
- # seeing as we just returning representative, no check is needed (its already checked)
262
- representative
263
+ begin
264
+ # error_recovery_method holds the current method that we are attempting to apply
265
+ # in case we throw an exception, and need to give the developer a meaningful message.
266
+ if method == "*"
267
+ # apply_star does the security check if value is present
268
+ cache_item.apply_star || representative
269
+ elsif method == "*all"
270
+ # if we secure the collection then we assume its okay to read the ids
271
+ error_recovery_method = [:all]
272
+ secured_value = cache_item.value.__secure_collection_check(cache_item)
273
+ cache_item.build_new_cache_item(timing(:active_record) { secured_value.collect { |record| record.id } }, method, method)
274
+ elsif method == "*count"
275
+ error_recovery_method = [:count]
276
+ secured_value = cache_item.value.__secure_collection_check(cache_item)
277
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.__secure_collection_check(cache_item).count }, method, method)
278
+ elsif preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
279
+ # no security check needed since we already evaluated this
280
+ cache_item.build_new_cache_item(preloaded_value, method, method)
281
+ elsif aggregation = cache_item.aggregation?(method)
282
+ # aggregations are not protected
283
+ error_recovery_method = [method, :mapping, :all]
284
+ cache_item.build_new_cache_item(aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }, method, method)
263
285
  else
264
- begin
265
- secured_method = "__secure_remote_access_to_#{[*method].first}"
266
-
267
- # order is important. This check must be first since scopes can have same name as attributes!
268
- if cache_item.value.respond_to? secured_method
269
- cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(secured_method, cache_item.value, @acting_user, *([*method][1..-1])) }, method, method)
270
- elsif (cache_item.value.class < ActiveRecord::Base) && cache_item.value.attributes.has_key?(method) # TODO: second check is not needed, its built into check_permmissions, check should be does class respond to check_permissions...
271
- cache_item.value.check_permission_with_acting_user(@acting_user, :view_permitted?, method)
272
- cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(*method) }, method, method)
273
- else
274
- raise "method missing"
286
+ if !cache_item.value || cache_item.value.is_a?(Array)
287
+ # seeing as we just returning representative, no check is needed (its already checked)
288
+ representative
289
+ elsif method == 'model_name'
290
+ error_recovery_method = [:model_name]
291
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.model_name }, method, method)
292
+ else
293
+ begin
294
+ secured_method = "__secure_remote_access_to_#{[*method].first}"
295
+ error_recovery_method = [*method]
296
+ # order is important. This check must be first since scopes can have same name as attributes!
297
+ if cache_item.value.respond_to? secured_method
298
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(secured_method, cache_item.value, @acting_user, *([*method][1..-1])) }, method, method)
299
+ elsif (cache_item.value.class < ActiveRecord::Base) && cache_item.value.attributes.has_key?(method) # TODO: second check is not needed, its built into check_permmissions, check should be does class respond to check_permissions...
300
+ cache_item.value.check_permission_with_acting_user(@acting_user, :view_permitted?, method)
301
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(*method) }, method, method)
302
+ else
303
+ raise "Method missing while fetching data: \`#{[*method].first}\` "\
304
+ 'was expected to be an attribute or a method defined using the server_method of finder_method macros.'
305
+ end
275
306
  end
276
- rescue Exception => e # this check may no longer be needed as we are quite explicit now on which methods we apply
277
- # ReactiveRecord::Pry::rescued(e)
278
- ::Rails.logger.debug "\033[0;31;1mERROR: HyperModel exception caught when applying #{method} to db object #{cache_item.value}: #{e}\033[0;30;21m"
279
- raise e, "HyperModel fetching records failed, exception caught when applying #{method} to db object #{cache_item.value}: #{e}", e.backtrace
280
307
  end
281
308
  end
309
+ rescue StandardError => e
310
+ raise e.class, form_error_message(e, cache_item.vector + error_recovery_method), e.backtrace
282
311
  end
283
312
  else
284
313
  representative
@@ -286,6 +315,15 @@ module ReactiveRecord
286
315
  end
287
316
  end
288
317
 
318
+ def form_error_message(original_error, vector)
319
+ expression = vector.collect do |exp|
320
+ next exp unless exp.is_a? Array
321
+ next exp.first if exp.length == 1
322
+ "#{exp.first}(#{exp[1..-1].join(', ')})"
323
+ end.join('.')
324
+ "raised when evaluating #{expression}\n#{original_error}"
325
+ end
326
+
289
327
  def aggregation?(method)
290
328
  if method.is_a?(String) && @value.class.respond_to?(:reflect_on_aggregation)
291
329
  aggregation = @value.class.reflect_on_aggregation(method.to_sym)
@@ -296,7 +334,7 @@ module ReactiveRecord
296
334
  end
297
335
 
298
336
  def apply_star
299
- if @value && @value.__secure_collection_check(@acting_user) && @value.length > 0
337
+ if @value && @value.__secure_collection_check(self) && @value.length > 0
300
338
  i = -1
301
339
  @value.inject(nil) do |representative, current_value|
302
340
  i += 1
@@ -358,6 +396,8 @@ module ReactiveRecord
358
396
  if method == "*"
359
397
  if @value.is_a? Array # this happens when a scope is empty there is test case, but
360
398
  @parent.as_hash({}) # does it work for all edge cases?
399
+ elsif (@value.class < ActiveRecord::Base) && children.is_a?(Hash)
400
+ @parent.as_hash({@value.id => merge_inheritance_column(children)})
361
401
  else
362
402
  @parent.as_hash({@value.id => children})
363
403
  end
@@ -411,17 +451,30 @@ keys:
411
451
  if value is a hash
412
452
  =end
413
453
 
414
-
415
454
  def self.load_from_json(tree, target = nil)
416
-
417
455
  # have to process *all before any other items
418
456
  # we leave the "*all" key in just for debugging purposes, and then skip it below
419
457
 
420
458
  if sorted_collection = tree["*all"]
421
- target.replace sorted_collection.collect { |id| target.proxy_association.klass.find(id) }
459
+ loaded_collection = sorted_collection.collect do |id|
460
+ ReactiveRecord::Base.find_by_id(target.proxy_association.klass, id)
461
+ end
462
+ if loaded_collection[0] && target.scope_description&.name == '___hyperstack_internal_scoped_find_by'
463
+ primary_key = target.proxy_association.klass.primary_key
464
+ attrs = target.vector[-1][1].reject { |key, _| key == primary_key }
465
+ loaded_collection[0].backing_record.sync_attributes(attrs)
466
+ end
467
+ target.replace loaded_collection
468
+ # we need to notify any observers of the collection. collection#replace
469
+ # will not notify if we are data_loading (which we are) so we will do it
470
+ # here. BUT we want the notification to occur after the current event
471
+ # completes so we wrap it a bulk_update
472
+ Hyperstack::Internal::State::Mapper.bulk_update do
473
+ Hyperstack::Internal::State::Variable.set(target, :collection, target.collection)
474
+ end
422
475
  end
423
476
 
424
- if id_value = tree["id"] and id_value.is_a? Array
477
+ if (id_value = tree[target.class.try(:primary_key)] || tree[:id]) && id_value.is_a?(Array)
425
478
  target.id = id_value.first
426
479
  end
427
480
  tree.each do |method, value|
@@ -433,17 +486,16 @@ keys:
433
486
  elsif !target
434
487
  load_from_json(value, Object.const_get(method))
435
488
  elsif method == "*count"
436
- target.set_count_state(value.first)
489
+ target.count_state = value.first
437
490
  elsif method.is_a? Integer or method =~ /^[0-9]+$/
438
491
  new_target = target.push_and_update_belongs_to(method)
439
- #target << (new_target = target.proxy_association.klass.find(method))
440
492
  elsif method.is_a? Array
441
493
  if method[0] == "new"
442
494
  new_target = ReactiveRecord::Base.lookup_by_object_id(method[1])
443
495
  elsif !(target.class < ActiveRecord::Base)
444
496
  new_target = target.send(*method)
445
497
  # value is an array if scope returns nil, so we destroy the bogus record
446
- new_target.destroy and new_target = nil if value.is_a? Array
498
+ new_target.destroy && (new_target = nil) if value.is_a? Array
447
499
  else
448
500
  target.backing_record.update_simple_attribute([method], target.backing_record.convert(method, value.first))
449
501
  end
@@ -454,12 +506,11 @@ keys:
454
506
 
455
507
  target.send "#{method}=", value.first
456
508
  elsif value.is_a? Array
457
- # we cannot use target.send "#{method}=" here because it might be a server method, which does not have a setter
458
- # a better fix might be something like target._internal_attribute_hash[method] = ...
459
- target.backing_record.set_attr_value(method, value.first) unless method == :id
460
- elsif value.is_a? Hash and value[:id] and value[:id].first and association = target.class.reflect_on_association(method)
509
+ target.send("_hyperstack_internal_setter_#{method}", value.first) unless method == target.class.primary_key
510
+ elsif value.is_a?(Hash) && value[:id] && value[:id].first && (association = target.class.reflect_on_association(method))
461
511
  # not sure if its necessary to check the id above... is it possible to for the method to be an association but not have an id?
462
- new_target = association.klass.find(value[:id].first)
512
+ klass = value[:model_name] ? Object.const_get(value[:model_name].first) : association.klass
513
+ new_target = ReactiveRecord::Base.find_by_id(klass, value[:id].first)
463
514
  target.send "#{method}=", new_target
464
515
  elsif !(target.class < ActiveRecord::Base)
465
516
  new_target = target.send(*method)
@@ -471,7 +522,6 @@ keys:
471
522
  load_from_json(value, new_target) if new_target
472
523
  end
473
524
  rescue Exception => e
474
- # debugger
475
525
  raise e
476
526
  end
477
527
  end