hyper-model 1.0.alpha1.3 → 1.0.alpha1.8

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +0 -1
  4. data/Gemfile +6 -5
  5. data/Rakefile +18 -6
  6. data/hyper-model.gemspec +12 -20
  7. data/lib/active_record_base.rb +95 -28
  8. data/lib/enumerable/pluck.rb +3 -2
  9. data/lib/hyper-model.rb +4 -1
  10. data/lib/hyper_model/version.rb +1 -1
  11. data/lib/hyper_react/input_tags.rb +2 -1
  12. data/lib/reactive_record/active_record/associations.rb +125 -35
  13. data/lib/reactive_record/active_record/base.rb +32 -0
  14. data/lib/reactive_record/active_record/class_methods.rb +125 -53
  15. data/lib/reactive_record/active_record/error.rb +2 -0
  16. data/lib/reactive_record/active_record/errors.rb +8 -4
  17. data/lib/reactive_record/active_record/instance_methods.rb +73 -5
  18. data/lib/reactive_record/active_record/public_columns_hash.rb +25 -26
  19. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
  20. data/lib/reactive_record/active_record/reactive_record/base.rb +50 -24
  21. data/lib/reactive_record/active_record/reactive_record/collection.rb +196 -63
  22. data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
  23. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
  24. data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
  25. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +71 -44
  26. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
  27. data/lib/reactive_record/active_record/reactive_record/operations.rb +7 -1
  28. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -6
  29. data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
  30. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +22 -1
  31. data/lib/reactive_record/broadcast.rb +59 -25
  32. data/lib/reactive_record/interval.rb +3 -3
  33. data/lib/reactive_record/permissions.rb +1 -1
  34. data/lib/reactive_record/scope_description.rb +3 -2
  35. data/lib/reactive_record/server_data_cache.rb +78 -48
  36. data/polymorph-notes.md +143 -0
  37. metadata +52 -157
  38. data/Gemfile.lock +0 -440
@@ -0,0 +1,22 @@
1
+ module ReactiveRecord
2
+ class DummyPolymorph
3
+ def initialize(vector)
4
+ @vector = vector
5
+ puts "VECTOR: #{@vector.inspect}"
6
+ Base.load_from_db(nil, *vector, 'id')
7
+ Base.load_from_db(nil, *vector, 'model_name')
8
+ end
9
+
10
+ def nil?
11
+ true
12
+ end
13
+
14
+ def method_missing(*)
15
+ self
16
+ end
17
+
18
+ def self.reflect_on_all_associations(*)
19
+ []
20
+ end
21
+ end
22
+ end
@@ -1,4 +1,4 @@
1
- # add mehods to Object to determine if this is a dummy object or not
1
+ # add methods to Object to determine if this is a dummy object or not
2
2
  class Object
3
3
  def loaded?
4
4
  !loading?
@@ -7,10 +7,6 @@ class Object
7
7
  def loading?
8
8
  false
9
9
  end
10
-
11
- def present?
12
- !!self
13
- end
14
10
  end
15
11
 
16
12
  module ReactiveRecord
@@ -36,6 +32,12 @@ module ReactiveRecord
36
32
  @column_hash[:default] || nil
37
33
  end
38
34
 
35
+ def build_default_value_for_json
36
+ ::JSON.parse(@column_hash[:default]) if @column_hash[:default]
37
+ end
38
+
39
+ alias build_default_value_for_jsonb build_default_value_for_json
40
+
39
41
  def build_default_value_for_datetime
40
42
  if @column_hash[:default]
41
43
  ::Time.parse(@column_hash[:default].gsub(' ','T')+'+00:00')
@@ -55,23 +57,26 @@ module ReactiveRecord
55
57
  end
56
58
  end
57
59
 
60
+ FALSY_VALUES = [false, nil, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
61
+
58
62
  def build_default_value_for_boolean
59
- @column_hash[:default] || false
63
+ !FALSY_VALUES.include?(@column_hash[:default])
60
64
  end
61
65
 
62
66
  def build_default_value_for_float
63
- @column_hash[:default] || Float(0.0)
67
+ @column_hash[:default]&.to_f || Float(0.0)
64
68
  end
65
69
 
66
70
  alias build_default_value_for_decimal build_default_value_for_float
67
71
 
68
72
  def build_default_value_for_integer
69
- @column_hash[:default] || Integer(0)
73
+ @column_hash[:default]&.to_i || Integer(0)
70
74
  end
71
75
 
72
76
  alias build_default_value_for_bigint build_default_value_for_integer
73
77
 
74
78
  def build_default_value_for_string
79
+ return @column_hash[:default] if @column_hash[:serialized?]
75
80
  @column_hash[:default] || ''
76
81
  end
77
82
 
@@ -91,10 +96,6 @@ module ReactiveRecord
91
96
  false
92
97
  end
93
98
 
94
- def present?
95
- false
96
- end
97
-
98
99
  def nil?
99
100
  true
100
101
  end
@@ -103,6 +104,11 @@ module ReactiveRecord
103
104
  true
104
105
  end
105
106
 
107
+ def class
108
+ notify
109
+ @object.class
110
+ end
111
+
106
112
  def method_missing(method, *args, &block)
107
113
  if method.start_with?("build_default_value_for_")
108
114
  nil
@@ -157,7 +163,13 @@ module ReactiveRecord
157
163
 
158
164
  alias inspect to_s
159
165
 
160
- `#{self}.$$proto.toString = Opal.Object.$$proto.toString`
166
+ %x{
167
+ if (Opal.Object.$$proto) {
168
+ #{self}.$$proto.toString = Opal.Object.$$proto.toString
169
+ } else {
170
+ #{self}.$$prototype.toString = Opal.Object.$$prototype.toString
171
+ }
172
+ }
161
173
 
162
174
  def to_f
163
175
  notify
@@ -215,10 +227,10 @@ module ReactiveRecord
215
227
  # to convert it to a string, for rendering
216
228
  # advantage over a try(:method) is, that it doesnt raise und thus is faster
217
229
  # which is important during render
218
- def respond_to?(method)
230
+ def respond_to?(method, all = false)
219
231
  return true if method == :acts_as_string?
220
232
  return true if %i[inspect to_date to_f to_i to_numeric to_number to_s to_time].include? method
221
- return @object.respond_to? if @object
233
+ return @object.respond_to?(method, all) if @object
222
234
  false
223
235
  end
224
236
 
@@ -4,9 +4,13 @@ module ReactiveRecord
4
4
  module Getters
5
5
  def get_belongs_to(assoc, reload = nil)
6
6
  getter_common(assoc.attribute, reload) do |has_key, attr|
7
- return if new?
8
- value = Base.fetch_from_db([@model, [:find, id], attr, @model.primary_key]) if id.present?
9
- value = find_association(assoc, value)
7
+ next if new?
8
+ if id.present?
9
+ value = fetch_by_id(attr, @model.primary_key)
10
+ klass = fetch_by_id(attr, 'model_name')
11
+ klass &&= Object.const_get(klass)
12
+ end
13
+ value = find_association(assoc, value, klass)
10
14
  sync_ignore_dummy attr, value, has_key
11
15
  end&.cast_to_current_sti_type
12
16
  end
@@ -33,7 +37,8 @@ module ReactiveRecord
33
37
 
34
38
  def get_server_method(attr, reload = nil)
35
39
  non_relationship_getter_common(attr, reload) do |has_key|
36
- sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
40
+ # SPLAT BUG: was sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
41
+ sync_ignore_dummy attr, Base.load_from_db(self, *(vector || [nil]), *attr), has_key
37
42
  end
38
43
  end
39
44
 
@@ -75,10 +80,16 @@ module ReactiveRecord
75
80
  if new?
76
81
  yield has_key if block
77
82
  elsif on_opal_client?
78
- sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
83
+ # SPLAT BUG: was sync_ignore_dummy attr, Base.load_from_db(self, *(vector ? vector : [nil]), attr), has_key
84
+ sync_ignore_dummy attr, Base.load_from_db(self, *(vector || [nil]), *attr), has_key
79
85
  elsif id.present?
80
- sync_attribute attr, Base.fetch_from_db([@model, [:find, id], attr])
86
+ sync_attribute attr, fetch_by_id(attr)
81
87
  else
88
+ # Not sure how to test this branch, it may never execute this line?
89
+ # If we are on opal_server then we should always be getting an id before getting here
90
+ # but if we do vector might not be set up properly to fetch the attribute
91
+ puts "*** Syncing attribute in getters.rb without an id. This may cause problems. ***"
92
+ puts "*** Report this to hyperstack.org if you see this message: vector = #{[*vector, attr]}"
82
93
  sync_attribute attr, Base.fetch_from_db([*vector, attr])
83
94
  end
84
95
  end
@@ -99,18 +110,26 @@ module ReactiveRecord
99
110
  else
100
111
  virtual_fetch_on_server_warning(attribute) if on_opal_server? && changed?
101
112
  yield false, attribute
102
- end.tap { |value| Hyperstack::Internal::State::Variable.get(self, attribute) unless data_loading? }
113
+ end.tap { Hyperstack::Internal::State::Variable.get(self, attribute) unless data_loading? }
103
114
  end
104
115
 
105
- def find_association(association, id)
106
- inverse_of = association.inverse_of
116
+ def find_association(association, id, klass)
107
117
  instance = if id
108
- find(association.klass, association.klass.primary_key => id)
118
+ find(klass, klass.primary_key => id)
119
+ elsif association.polymorphic?
120
+ new_from_vector(nil, nil, *vector, association.attribute)
109
121
  else
110
122
  new_from_vector(association.klass, nil, *vector, association.attribute)
111
123
  end
124
+ return instance if instance.is_a? DummyPolymorph
125
+ inverse_of = association.inverse_of(instance)
112
126
  instance_backing_record_attributes = instance.attributes
113
127
  inverse_association = association.klass.reflect_on_association(inverse_of)
128
+ # HMT-TODO: don't we need to do something with the through association case.
129
+ # perhaps we never hit this point...
130
+ if association.through_association?
131
+ IsomorphicHelpers.log "*********** called #{ar_instance}.find_association(#{association.attribute}) which is has many through!!!!!!!", :error
132
+ end
114
133
  if inverse_association.collection?
115
134
  instance_backing_record_attributes[inverse_of] = if id and id != ""
116
135
  Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
@@ -129,5 +148,9 @@ module ReactiveRecord
129
148
  self.aggregate_attribute = attr
130
149
  @ar_instance
131
150
  end
151
+
152
+ def fetch_by_id(*vector)
153
+ Base.fetch_from_db([@model, *find_by_vector(@model.primary_key => id), *vector])
154
+ end
132
155
  end
133
156
  end
@@ -1,5 +1,3 @@
1
- require 'json'
2
-
3
1
  module ReactiveRecord
4
2
 
5
3
  class Base
@@ -47,6 +45,15 @@ module ReactiveRecord
47
45
  self.class.instance_variable_get(:@records)
48
46
  end
49
47
 
48
+ # constructs vector for find_by
49
+ def self.find_by_vector(attrs)
50
+ [:all, [:___hyperstack_internal_scoped_find_by, attrs], '*0']
51
+ end
52
+
53
+ def find_by_vector(attrs)
54
+ self.class.find_by_vector(attrs)
55
+ end
56
+
50
57
  # Prerendering db access (returns nil if on client):
51
58
  # at end of prerendering dumps all accessed records in the footer
52
59
 
@@ -58,7 +65,9 @@ module ReactiveRecord
58
65
 
59
66
  isomorphic_method(:find_in_db) do |f, klass, attrs|
60
67
  f.send_to_server klass.name, attrs if RUBY_ENGINE == 'opal'
61
- f.when_on_server { @server_data_cache[klass, ['find_by', attrs], 'id'] }
68
+ f.when_on_server do
69
+ @server_data_cache[klass, *find_by_vector(attrs), 'id']
70
+ end
62
71
  end
63
72
 
64
73
  class << self
@@ -124,7 +133,6 @@ module ReactiveRecord
124
133
  model.columns_hash[method_name] || model.server_methods[method_name]
125
134
  end
126
135
 
127
-
128
136
  class << self
129
137
 
130
138
  attr_reader :pending_fetches
@@ -223,7 +231,7 @@ module ReactiveRecord
223
231
  if association.collection?
224
232
  # following line changed from .all to .collection on 10/28
225
233
  [*value.collection, *value.unsaved_children].each do |assoc|
226
- add_new_association.call(record, attribute, assoc.backing_record) if assoc.changed?(association.inverse_of) or assoc.new?
234
+ add_new_association.call(record, attribute, assoc.backing_record) if assoc.changed?(association.inverse_of(assoc)) or assoc.new_record?
227
235
  end
228
236
  elsif record.new? || record.changed?(attribute) || (record == record_being_saved && force)
229
237
  if value.nil?
@@ -232,7 +240,7 @@ module ReactiveRecord
232
240
  add_new_association.call record, attribute, value.backing_record
233
241
  end
234
242
  end
235
- elsif aggregation = record.model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
243
+ elsif (aggregation = record.model.reflect_on_aggregation(attribute)) && (aggregation.klass < ActiveRecord::Base)
236
244
  add_new_association.call record, attribute, value.backing_record unless value.nil?
237
245
  elsif aggregation
238
246
  new_value = aggregation.serialize(value)
@@ -253,8 +261,17 @@ module ReactiveRecord
253
261
  HyperMesh.load do
254
262
  ReactiveRecord.loads_pending! unless self.class.pending_fetches.empty?
255
263
  end.then { send_save_to_server(save, validate, force, &block) }
256
- #save_to_server(validate, force, &block)
257
264
  else
265
+ if validate
266
+ # Handles the case where a model is valid, then some attribute is
267
+ # updated, and model.validate is called updating the error data.
268
+ # Now lets say the attribute changes back to the last synced value. In
269
+ # this case we need to revert the error records.
270
+ models, _, backing_records = self.class.gather_records([self], true, self)
271
+ models.each do |item|
272
+ backing_records[item[:id]].revert_errors!
273
+ end
274
+ end
258
275
  promise = Promise.new
259
276
  yield true, nil, [] if block
260
277
  promise.resolve({success: true})
@@ -291,14 +308,13 @@ module ReactiveRecord
291
308
 
292
309
  response[:saved_models].each do | item |
293
310
  backing_records[item[0]].sync_unscoped_collection! if save
294
- backing_records[item[0]].errors! item[3]
311
+ backing_records[item[0]].errors! item[3], save
295
312
  end
296
313
 
297
314
  yield response[:success], response[:message], response[:models] if block
298
315
  promise.resolve response # TODO this could be problematic... there was no .json here, so .... what's to do?
299
316
 
300
317
  rescue Exception => e
301
- # debugger
302
318
  log("Exception raised while saving - #{e}", :error)
303
319
  ensure
304
320
  backing_records.each { |_id, record| record.saved! rescue nil } if save
@@ -325,8 +341,6 @@ module ReactiveRecord
325
341
  elsif method.is_a? Array #__secure_remote_access_to_
326
342
  if method[0] == 'new'
327
343
  object.new
328
- elsif method[0] == 'find_by'
329
- object.send(*method)
330
344
  else
331
345
  object.send(:"__secure_remote_access_to_#{method[0]}", object, acting_user, *method[1..-1])
332
346
  end
@@ -337,7 +351,11 @@ module ReactiveRecord
337
351
  end
338
352
  end
339
353
  if id and (found.nil? or !(found.class <= model) or (found.id and found.id.to_s != id.to_s))
340
- raise "Inconsistent data sent to server - #{model.name}.find(#{id}) != [#{vector}]"
354
+ # TODO: the one case that this is okay is when we are doing a find(some_id) which
355
+ # does not exist. So the above check needs to deal with that if possible,
356
+ # otherwise we can just skip this check, as it was put in sometime back for
357
+ # debugging purposes, and is perhaps not necessary anymore
358
+ #raise "Inconsistent data sent to server - #{model.name}.find(#{id}) != [#{vector}]"
341
359
  end
342
360
  found
343
361
  elsif id
@@ -437,24 +455,29 @@ module ReactiveRecord
437
455
  parent.send("#{association[:attribute]}=", aggregate)
438
456
  #puts "updated is frozen? #{aggregate.frozen?}, parent attributes = #{parent.send(association[:attribute]).attributes}"
439
457
  elsif parent.class.reflect_on_association(association[:attribute].to_sym).nil?
440
- raise "Missing association :#{association[:attribute]} for #{parent.class.name}. Was association defined on opal side only?"
458
+ raise "Missing association :#{association[:attribute]} for #{parent.class.name}. Was association defined on opal side only?"
441
459
  elsif parent.class.reflect_on_association(association[:attribute].to_sym).collection?
442
460
  #puts ">>>>>>>>>> #{parent.class.name}.send('#{association[:attribute]}') << #{reactive_records[association[:child_id]]})"
443
461
  dont_save_list.delete(parent)
444
- #if false and parent.new?
445
- #parent.send("#{association[:attribute]}") << reactive_records[association[:child_id]]
446
- # puts "updated"
447
- #else
448
- #puts "skipped"
449
- #end
462
+
463
+ # if reactive_records[association[:child_id]]&.new_record?
464
+ # dont_save_list << reactive_records[association[:child_id]]
465
+ # end
466
+
467
+ if parent.new_record?
468
+ parent.send("#{association[:attribute]}") << reactive_records[association[:child_id]]
469
+ end
450
470
  else
451
471
  #puts ">>>>ASSOCIATION>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
452
472
  parent.send("#{association[:attribute]}=", reactive_records[association[:child_id]])
453
473
  dont_save_list.delete(parent)
454
- #puts "updated"
474
+
475
+ # if parent.class.reflect_on_association(association[:attribute].to_sym).macro == :has_one &&
476
+ # reactive_records[association[:child_id]]&.new_record?
477
+ # dont_save_list << reactive_records[association[:child_id]]
478
+ # end
455
479
  end
456
480
  end if associations
457
-
458
481
  # get rid of any records that don't require further processing, as a side effect
459
482
  # we also save any records that need to be saved (these may be rolled back later.)
460
483
 
@@ -463,10 +486,13 @@ module ReactiveRecord
463
486
  next true if record.frozen? # skip (but process later) frozen records
464
487
  next true if dont_save_list.include?(record) # skip if the record is on the don't save list
465
488
  next true if record.changed.include?(record.class.primary_key) # happens on an aggregate
466
- next false if record.id && !record.changed? # throw out any existing records with no changes
489
+ #next true if record.persisted? # record may be have been saved as result of has_one assignment
490
+ # next false if record.id && !record.changed? # throw out any existing records with no changes
491
+ next record.persisted? if record.id && !record.changed?
467
492
  # if we get to here save the record and return true to keep it
468
493
  op = new_models.include?(record) ? :create_permitted? : :update_permitted?
469
- record.check_permission_with_acting_user(acting_user, op).save(validate: false) || true
494
+
495
+ record.check_permission_with_acting_user(acting_user, op).save(validate: validate) || true
470
496
  end
471
497
 
472
498
  # if called from ServerDataCache then save and validate are both false, and we just return the
@@ -479,14 +505,15 @@ module ReactiveRecord
479
505
  # the all the error messages during a save so we can dump them to the server log.
480
506
 
481
507
  all_messages = []
508
+ attributes = nil
482
509
 
483
510
  saved_models = reactive_records.collect do |reactive_record_id, model|
484
511
  messages = model.errors.messages if validate && !model.valid?
485
512
  all_messages << [model, messages] if save && messages
486
513
  attributes = model.__hyperstack_secure_attributes(acting_user)
514
+ attributes[model.class.primary_key] = model[model.class.primary_key]
487
515
  [reactive_record_id, model.class.name, attributes, messages]
488
516
  end
489
-
490
517
  # if we are not saving (i.e. just validating) then we rollback the transaction
491
518
 
492
519
  raise ActiveRecord::Rollback, 'This Rollback is intentional!' unless save
@@ -501,7 +528,6 @@ module ReactiveRecord
501
528
  end
502
529
  raise 'HyperModel saving records failed!'
503
530
  end
504
-
505
531
  end
506
532
 
507
533
  { success: true, saved_models: saved_models }
@@ -521,25 +547,31 @@ module ReactiveRecord
521
547
  if RUBY_ENGINE == 'opal'
522
548
 
523
549
  def destroy(&block)
550
+ return if @destroyed || @being_destroyed
524
551
 
525
- return if @destroyed
526
-
527
- #destroy_associations
552
+ # destroy_associations
528
553
 
529
554
  promise = Promise.new
530
-
531
- if !data_loading? and (id or vector)
555
+ if !data_loading? && (id || vector)
532
556
  Operations::Destroy.run(model: ar_instance.model_name.to_s, id: id, vector: vector)
533
557
  .then do |response|
534
- Broadcast.to_self ar_instance
558
+ #[reactive_record_id, model.class.name, attributes, messages] model.errors.messages
559
+
560
+ if response[:success]
561
+ @destroyed = true
562
+ Broadcast.to_self ar_instance
563
+ else
564
+ errors! response[:messages]
565
+ end
535
566
  yield response[:success], response[:message] if block
536
567
  promise.resolve response
537
568
  end
538
569
  else
539
570
  destroy_associations
540
571
  # sync_unscoped_collection! # ? should we do this here was NOT being done before hypermesh integration
572
+ @destroyed = true
541
573
  yield true, nil if block
542
- promise.resolve({success: true})
574
+ promise.resolve(success: true)
543
575
  end
544
576
 
545
577
  # DO NOT CLEAR ATTRIBUTES. Records that are not found, are destroyed, and if they are searched for again, we want to make
@@ -549,8 +581,6 @@ module ReactiveRecord
549
581
  # sync!
550
582
  # and modify server_data_cache so that it does NOT call destroy
551
583
 
552
- @destroyed = true
553
-
554
584
  promise
555
585
  end
556
586
 
@@ -559,18 +589,15 @@ module ReactiveRecord
559
589
  def self.destroy_record(model, id, vector, acting_user)
560
590
  model = Object.const_get(model)
561
591
  record = if id
562
- model.find(id)
563
- else
564
- ServerDataCache.new(acting_user, {})[*vector]
565
- end
566
-
567
-
568
- record.check_permission_with_acting_user(acting_user, :destroy_permitted?).destroy
569
- {success: true, attributes: {}}
592
+ model.find(id)
593
+ else
594
+ ServerDataCache.new(acting_user, {})[*vector].value
595
+ end
570
596
 
597
+ success = record.check_permission_with_acting_user(acting_user, :destroy_permitted?).destroy
598
+ { success: success, attributes: {}, messages: record.errors.messages }
571
599
  rescue Exception => e
572
- #ReactiveRecord::Pry.rescued(e)
573
- {success: false, record: record, message: e}
600
+ { success: false, record: record, message: e }
574
601
  end
575
602
  end
576
603
  end