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

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