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

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