hyper-model 1.0.alpha1.4 → 1.0.alpha1.5

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.
@@ -4,11 +4,11 @@ module ReactiveRecord
4
4
  # the appropriate string. The order of execution is important!
5
5
  module BackingRecordInspector
6
6
  def inspection_details
7
- return error_details unless errors.empty?
8
- return new_details if new?
7
+ return error_details unless errors.empty?
8
+ return new_details if new?
9
9
  return destroyed_details if destroyed
10
- return loading_details unless @attributes.key? primary_key
11
- return dirty_details unless changed_attributes.empty?
10
+ return loading_details unless @attributes.key? primary_key
11
+ return dirty_details unless changed_attributes.empty?
12
12
  "[loaded id: #{id}]"
13
13
  end
14
14
 
@@ -71,7 +71,7 @@ module ReactiveRecord
71
71
  if (id_to_find = attrs[model.primary_key])
72
72
  !new_only && lookup_by_id(model, id_to_find)
73
73
  else
74
- @records[model].detect do |r|
74
+ @records[model.base_class].detect do |r|
75
75
  (r.new? || !new_only) &&
76
76
  !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
77
77
  end
@@ -118,6 +118,9 @@ module ReactiveRecord
118
118
  # this is the equivilent of find but for associations and aggregations
119
119
  # because we are not fetching a specific attribute yet, there is NO communication with the
120
120
  # server. That only happens during find.
121
+
122
+ return DummyPolymorph.new(vector) unless model
123
+
121
124
  model = model.base_class
122
125
 
123
126
  # do we already have a record with this vector? If so return it, otherwise make a new one.
@@ -147,7 +150,7 @@ module ReactiveRecord
147
150
  @attributes = {}
148
151
  @changed_attributes = []
149
152
  @virgin = true
150
- records[model] << self
153
+ records[model.base_class] << self
151
154
  Base.set_object_id_lookup(self)
152
155
  end
153
156
 
@@ -204,7 +207,7 @@ module ReactiveRecord
204
207
  end
205
208
 
206
209
  def errors
207
- @errors ||= ActiveModel::Errors.new(self)
210
+ @errors ||= ActiveModel::Errors.new(ar_instance)
208
211
  end
209
212
 
210
213
  # called when we have a newly created record, to initialize
@@ -250,7 +253,7 @@ module ReactiveRecord
250
253
  return if @create_sync
251
254
  @create_sync = true
252
255
  end
253
- model.unscoped << ar_instance
256
+ model.unscoped._internal_push ar_instance
254
257
  @synced_with_unscoped = !@synced_with_unscoped
255
258
  end
256
259
 
@@ -307,9 +310,11 @@ module ReactiveRecord
307
310
  @saving = true
308
311
  end
309
312
 
310
- def errors!(hash)
313
+ def errors!(hash, saving)
314
+ @errors_at_last_sync = hash if saving
311
315
  notify_waiting_for_save
312
316
  errors.clear && return unless hash
317
+ errors.non_reactive_clear
313
318
  hash.each do |attribute, messages|
314
319
  messages.each do |message|
315
320
  errors.add(attribute, message)
@@ -317,6 +322,11 @@ module ReactiveRecord
317
322
  end
318
323
  end
319
324
 
325
+ def revert_errors!
326
+ puts "#{inspect}.revert_errors! @errors_at_last_sync: #{@errors_at_last_sync}"
327
+ errors!(@errors_at_last_sync)
328
+ end
329
+
320
330
  def saved!(save_only = nil) # sets saving to false AND notifies
321
331
  notify_waiting_for_save
322
332
  return self if save_only
@@ -418,6 +428,7 @@ module ReactiveRecord
418
428
 
419
429
  def destroy_associations
420
430
  @destroyed = false
431
+ @being_destroyed = true
421
432
  model.reflect_on_all_associations.each do |association|
422
433
  if association.collection?
423
434
  @attributes[association.attribute].replace([]) if @attributes[association.attribute]
@@ -77,6 +77,8 @@ module ReactiveRecord
77
77
  (@collection.length..index).each do |i|
78
78
  new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
79
79
  new_dummy_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association?
80
+ # HMT-TODO: the above needs to be looked into... if we are a hmt then don't we need to create a dummy on the joins collection as well?
81
+ # or maybe this just does not work for HMT?
80
82
  @collection << new_dummy_record
81
83
  end
82
84
  end
@@ -212,7 +214,7 @@ To determine this sync_scopes first asks if the record being changed is in the s
212
214
  return [] unless attrs[@association.inverse_of] == @owner
213
215
  if !@association.through_association
214
216
  [record]
215
- elsif (source = attrs[@association.source])
217
+ elsif (source = attrs[@association.source]) && source.is_a?(@target_klass)
216
218
  [source]
217
219
  else
218
220
  []
@@ -233,7 +235,8 @@ To determine this sync_scopes first asks if the record being changed is in the s
233
235
  end
234
236
 
235
237
  def in_this_collection(related_records)
236
- return related_records unless @association
238
+ # HMT-TODO: I don't think we can get a set of related records here with a through association unless they are part of the collection
239
+ return related_records if !@association || @association.through_association?
237
240
  related_records.select do |r|
238
241
  r.backing_record.attributes[@association.inverse_of] == @owner
239
242
  end
@@ -406,7 +409,11 @@ To determine this sync_scopes first asks if the record being changed is in the s
406
409
  def set_belongs_to(child)
407
410
  if @owner
408
411
  # TODO this is major broken...current
409
- child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
412
+ if (through_association = @association.through_association)
413
+ # HMT-TODO: create a new record with owner and child
414
+ else
415
+ child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
416
+ end
410
417
  elsif @parent
411
418
  @parent.set_belongs_to(child)
412
419
  end
@@ -421,19 +428,40 @@ To determine this sync_scopes first asks if the record being changed is in the s
421
428
 
422
429
  def update_child(item)
423
430
  backing_record = item.backing_record
424
- if backing_record && @owner && @association && !@association.through_association? && item.attributes[@association.inverse_of] != @owner
431
+ # HMT TODO: The following && !association.through_association was commented out, causing wrong class items to be added to
432
+ # associations
433
+ # Why was it commented out.
434
+ if backing_record && @owner && @association && item.attributes[@association.inverse_of] != @owner && !@association.through_association?
425
435
  inverse_of = @association.inverse_of
426
- current_association = item.attributes[inverse_of]
436
+ current_association_value = item.attributes[inverse_of]
427
437
  backing_record.virgin = false unless backing_record.data_loading?
438
+ # next line was commented out and following line was active.
428
439
  backing_record.update_belongs_to(inverse_of, @owner)
429
- if current_association && current_association.attributes[@association.attribute]
430
- current_association.attributes[@association.attribute].delete(item)
431
- end
440
+ #backing_record.set_belongs_to_via_has_many(@association, @owner)
441
+ # following is handled by update_belongs_to and is redundant
442
+ # unless current_association_value.nil? # might be a dummy value which responds to nil
443
+ # current_association = @association.inverse.inverse(current_association_value)
444
+ # current_association_attribute = current_association.attribute
445
+ # if current_association.collection? && current_association_value.attributes[current_association_attribute]
446
+ # current_association.attributes[current_association_attribute].delete(item)
447
+ # end
448
+ # end
432
449
  @owner.backing_record.sync_has_many(@association.attribute)
433
450
  end
434
451
  end
435
452
 
436
453
  def push(item)
454
+ if (through_association = @association&.through_association)
455
+ through_association.klass.create(@association.inverse_of => @owner, @association.source => item)
456
+ self
457
+ else
458
+ _internal_push(item)
459
+ end
460
+ end
461
+
462
+ alias << push
463
+
464
+ def _internal_push(item)
437
465
  item.itself # force get of at least the id
438
466
  if collection
439
467
  self.force_push item
@@ -449,8 +477,6 @@ To determine this sync_scopes first asks if the record being changed is in the s
449
477
  self
450
478
  end
451
479
 
452
- alias << push
453
-
454
480
  def sort!(*args, &block)
455
481
  replace(sort(*args, &block))
456
482
  end
@@ -514,19 +540,19 @@ To determine this sync_scopes first asks if the record being changed is in the s
514
540
  @dummy_collection.notify
515
541
  array = new_array.is_a?(Collection) ? new_array.collection : new_array
516
542
  @collection.each_with_index do |r, i|
517
- r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/
543
+ r.id = new_array[i].id if array[i] and array[i].id and !r.new_record? and r.backing_record.vector.last =~ /^\*[0-9]+$/
518
544
  end
519
545
  end
520
-
521
- @collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think
546
+ # the following makes sure that the existing elements are properly removed from the collection
547
+ @collection.dup.each { |item| delete(item) } if @collection
522
548
  @collection = []
523
549
  if new_array.is_a? Collection
524
550
  @dummy_collection = new_array.dummy_collection
525
551
  @dummy_record = new_array.dummy_record
526
- new_array.collection.each { |item| self << item } if new_array.collection
552
+ new_array.collection.each { |item| _internal_push item } if new_array.collection
527
553
  else
528
554
  @dummy_collection = @dummy_record = nil
529
- new_array.each { |item| self << item }
555
+ new_array.each { |item| _internal_push item }
530
556
  end
531
557
  notify_of_change new_array
532
558
  end
@@ -534,9 +560,9 @@ To determine this sync_scopes first asks if the record being changed is in the s
534
560
  def delete(item)
535
561
  unsaved_children.delete(item)
536
562
  notify_of_change(
537
- if @owner && @association && !@association.through_association?
563
+ if @owner && @association
538
564
  inverse_of = @association.inverse_of
539
- if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner
565
+ if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner && !@association.through_association?
540
566
  # the if prevents double update if delete is being called from << (see << above)
541
567
  backing_record.update_belongs_to(inverse_of, nil)
542
568
  end
@@ -562,20 +588,8 @@ To determine this sync_scopes first asks if the record being changed is in the s
562
588
  @dummy_collection.loading?
563
589
  end
564
590
 
565
- def loaded?
566
- false && @collection && (!@dummy_collection || !@dummy_collection.loading?) && (!@owner || @owner.id || @vector.length > 1)
567
- end
568
-
569
- def empty?
570
- # should be handled by method missing below, but opal-rspec does not deal well
571
- # with method missing, so to test...
572
- all.empty?
573
- end
574
-
575
591
  def find_by(attrs)
576
592
  attrs = @target_klass.__hyperstack_preprocess_attrs(attrs)
577
- # r = @collection&.detect { |lr| lr.new_record? && !attrs.detect { |k, v| lr.attributes[k] != v } }
578
- # return r if r
579
593
  (r = __hyperstack_internal_scoped_find_by(attrs)) || return
580
594
  r.backing_record.sync_attributes(attrs).set_ar_instance!
581
595
  end
@@ -596,6 +610,16 @@ To determine this sync_scopes first asks if the record being changed is in the s
596
610
  found
597
611
  end
598
612
 
613
+ # to avoid fetching the entire collection array we check empty and any against the count
614
+
615
+ def empty?
616
+ count.zero?
617
+ end
618
+
619
+ def any?
620
+ !count.zero?
621
+ end
622
+
599
623
  def method_missing(method, *args, &block)
600
624
  if args.count == 1 && method.start_with?('find_by_')
601
625
  find_by(method.sub(/^find_by_/, '') => args[0])
@@ -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
@@ -72,6 +72,7 @@ module ReactiveRecord
72
72
  alias build_default_value_for_bigint build_default_value_for_integer
73
73
 
74
74
  def build_default_value_for_string
75
+ return @column_hash[:default] if @column_hash[:serialized?]
75
76
  @column_hash[:default] || ''
76
77
  end
77
78
 
@@ -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 = fetch_by_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
@@ -104,18 +108,26 @@ module ReactiveRecord
104
108
  else
105
109
  virtual_fetch_on_server_warning(attribute) if on_opal_server? && changed?
106
110
  yield false, attribute
107
- end.tap { |value| Hyperstack::Internal::State::Variable.get(self, attribute) unless data_loading? }
111
+ end.tap { Hyperstack::Internal::State::Variable.get(self, attribute) unless data_loading? }
108
112
  end
109
113
 
110
- def find_association(association, id)
111
- inverse_of = association.inverse_of
114
+ def find_association(association, id, klass)
112
115
  instance = if id
113
- find(association.klass, association.klass.primary_key => id)
116
+ find(klass, klass.primary_key => id)
117
+ elsif association.polymorphic?
118
+ new_from_vector(nil, nil, *vector, association.attribute)
114
119
  else
115
120
  new_from_vector(association.klass, nil, *vector, association.attribute)
116
121
  end
122
+ return instance if instance.is_a? DummyPolymorph
123
+ inverse_of = association.inverse_of(instance)
117
124
  instance_backing_record_attributes = instance.attributes
118
125
  inverse_association = association.klass.reflect_on_association(inverse_of)
126
+ # HMT-TODO: don't we need to do something with the through association case.
127
+ # perhaps we never hit this point...
128
+ if association.through_association?
129
+ IsomorphicHelpers.log "*********** called #{ar_instance}.find_association(#{association.attribute}) which is has many through!!!!!!!", :error
130
+ end
119
131
  if inverse_association.collection?
120
132
  instance_backing_record_attributes[inverse_of] = if id and id != ""
121
133
  Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
@@ -135,7 +135,6 @@ module ReactiveRecord
135
135
  model.columns_hash[method_name] || model.server_methods[method_name]
136
136
  end
137
137
 
138
-
139
138
  class << self
140
139
 
141
140
  attr_reader :pending_fetches
@@ -234,7 +233,7 @@ module ReactiveRecord
234
233
  if association.collection?
235
234
  # following line changed from .all to .collection on 10/28
236
235
  [*value.collection, *value.unsaved_children].each do |assoc|
237
- add_new_association.call(record, attribute, assoc.backing_record) if assoc.changed?(association.inverse_of) or assoc.new?
236
+ add_new_association.call(record, attribute, assoc.backing_record) if assoc.changed?(association.inverse_of(assoc)) or assoc.new_record?
238
237
  end
239
238
  elsif record.new? || record.changed?(attribute) || (record == record_being_saved && force)
240
239
  if value.nil?
@@ -243,7 +242,7 @@ module ReactiveRecord
243
242
  add_new_association.call record, attribute, value.backing_record
244
243
  end
245
244
  end
246
- elsif aggregation = record.model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
245
+ elsif (aggregation = record.model.reflect_on_aggregation(attribute)) && (aggregation.klass < ActiveRecord::Base)
247
246
  add_new_association.call record, attribute, value.backing_record unless value.nil?
248
247
  elsif aggregation
249
248
  new_value = aggregation.serialize(value)
@@ -264,8 +263,17 @@ module ReactiveRecord
264
263
  HyperMesh.load do
265
264
  ReactiveRecord.loads_pending! unless self.class.pending_fetches.empty?
266
265
  end.then { send_save_to_server(save, validate, force, &block) }
267
- #save_to_server(validate, force, &block)
268
266
  else
267
+ if validate
268
+ # Handles the case where a model is valid, then some attribute is
269
+ # updated, and model.validate is called updating the error data.
270
+ # Now lets say the attribute changes back to the last synced value. In
271
+ # this case we need to revert the error records.
272
+ models, _, backing_records = self.class.gather_records([self], true, self)
273
+ models.each do |item|
274
+ backing_records[item[:id]].revert_errors!
275
+ end
276
+ end
269
277
  promise = Promise.new
270
278
  yield true, nil, [] if block
271
279
  promise.resolve({success: true})
@@ -302,14 +310,13 @@ module ReactiveRecord
302
310
 
303
311
  response[:saved_models].each do | item |
304
312
  backing_records[item[0]].sync_unscoped_collection! if save
305
- backing_records[item[0]].errors! item[3]
313
+ backing_records[item[0]].errors! item[3], save
306
314
  end
307
315
 
308
316
  yield response[:success], response[:message], response[:models] if block
309
317
  promise.resolve response # TODO this could be problematic... there was no .json here, so .... what's to do?
310
318
 
311
319
  rescue Exception => e
312
- # debugger
313
320
  log("Exception raised while saving - #{e}", :error)
314
321
  ensure
315
322
  backing_records.each { |_id, record| record.saved! rescue nil } if save
@@ -454,6 +461,11 @@ module ReactiveRecord
454
461
  elsif parent.class.reflect_on_association(association[:attribute].to_sym).collection?
455
462
  #puts ">>>>>>>>>> #{parent.class.name}.send('#{association[:attribute]}') << #{reactive_records[association[:child_id]]})"
456
463
  dont_save_list.delete(parent)
464
+
465
+
466
+ # if reactive_records[association[:child_id]]&.new_record?
467
+ # dont_save_list << reactive_records[association[:child_id]]
468
+ # end
457
469
  #if false and parent.new?
458
470
  #parent.send("#{association[:attribute]}") << reactive_records[association[:child_id]]
459
471
  # puts "updated"
@@ -464,10 +476,13 @@ module ReactiveRecord
464
476
  #puts ">>>>ASSOCIATION>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
465
477
  parent.send("#{association[:attribute]}=", reactive_records[association[:child_id]])
466
478
  dont_save_list.delete(parent)
467
- #puts "updated"
479
+
480
+ # if parent.class.reflect_on_association(association[:attribute].to_sym).macro == :has_one &&
481
+ # reactive_records[association[:child_id]]&.new_record?
482
+ # dont_save_list << reactive_records[association[:child_id]]
483
+ # end
468
484
  end
469
485
  end if associations
470
-
471
486
  # get rid of any records that don't require further processing, as a side effect
472
487
  # we also save any records that need to be saved (these may be rolled back later.)
473
488
 
@@ -476,7 +491,9 @@ module ReactiveRecord
476
491
  next true if record.frozen? # skip (but process later) frozen records
477
492
  next true if dont_save_list.include?(record) # skip if the record is on the don't save list
478
493
  next true if record.changed.include?(record.class.primary_key) # happens on an aggregate
479
- next false if record.id && !record.changed? # throw out any existing records with no changes
494
+ #next true if record.persisted? # record may be have been saved as result of has_one assignment
495
+ # next false if record.id && !record.changed? # throw out any existing records with no changes
496
+ next record.persisted? if record.id && !record.changed?
480
497
  # if we get to here save the record and return true to keep it
481
498
  op = new_models.include?(record) ? :create_permitted? : :update_permitted?
482
499
  record.check_permission_with_acting_user(acting_user, op).save(validate: false) || true
@@ -492,14 +509,15 @@ module ReactiveRecord
492
509
  # the all the error messages during a save so we can dump them to the server log.
493
510
 
494
511
  all_messages = []
512
+ attributes = nil
495
513
 
496
514
  saved_models = reactive_records.collect do |reactive_record_id, model|
497
515
  messages = model.errors.messages if validate && !model.valid?
498
516
  all_messages << [model, messages] if save && messages
499
517
  attributes = model.__hyperstack_secure_attributes(acting_user)
518
+ attributes[model.class.primary_key] = model[model.class.primary_key]
500
519
  [reactive_record_id, model.class.name, attributes, messages]
501
520
  end
502
-
503
521
  # if we are not saving (i.e. just validating) then we rollback the transaction
504
522
 
505
523
  raise ActiveRecord::Rollback, 'This Rollback is intentional!' unless save
@@ -514,7 +532,6 @@ module ReactiveRecord
514
532
  end
515
533
  raise 'HyperModel saving records failed!'
516
534
  end
517
-
518
535
  end
519
536
 
520
537
  { success: true, saved_models: saved_models }