Class: ReactiveRecord::Base

Inherits:
Object show all
Includes:
React::IsomorphicHelpers
Defined in:
lib/reactive_record/active_record/reactive_record/column_types.rb,
lib/reactive_record/active_record/reactive_record/base.rb,
lib/reactive_record/active_record/reactive_record/dummy_value.rb,
lib/reactive_record/active_record/reactive_record/isomorphic_base.rb,
lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb

Overview

methods to update aggregrations and relations, called from reactive_set!

Defined Under Namespace

Classes: DbRequestMade, DummyValue

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, hash = {}, ar_instance = nil) ⇒ Base

Returns a new instance of Base.



130
131
132
133
134
135
136
137
138
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 130

def initialize(model, hash = {}, ar_instance = nil)
  @model = model
  @ar_instance = ar_instance
  @synced_attributes = {}
  @attributes = {}
  @changed_attributes = []
  @virgin = true
  records[model] << self
end

Class Attribute Details

.last_fetch_atObject (readonly)

Returns the value of attribute last_fetch_at.



131
132
133
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 131

def last_fetch_at
  @last_fetch_at
end

.outer_scopesObject (readonly)

Returns the value of attribute outer_scopes.



451
452
453
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 451

def outer_scopes
  @outer_scopes
end

.pending_fetchesObject (readonly)

Returns the value of attribute pending_fetches.



130
131
132
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 130

def pending_fetches
  @pending_fetches
end

.public_columns_hashObject (readonly)

Returns the value of attribute public_columns_hash.



65
66
67
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 65

def public_columns_hash
  @public_columns_hash
end

Instance Attribute Details

#aggregate_attributeObject

Returns the value of attribute aggregate_attribute.



34
35
36
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 34

def aggregate_attribute
  @aggregate_attribute
end

#aggregate_ownerObject

Returns the value of attribute aggregate_owner.



33
34
35
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 33

def aggregate_owner
  @aggregate_owner
end

#ar_instanceObject

Each method call is either a simple method name or an array in the form [method_name, param, param …] Example [User, [find, 123], todos, active, [due, “1/1/2016”], title] Roughly corresponds to this query: User.find(123).todos.active.due(“1/1/2016”).select(:title)



29
30
31
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 29

def ar_instance
  @ar_instance
end

#changed_attributesObject

Returns the value of attribute changed_attributes.



32
33
34
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 32

def changed_attributes
  @changed_attributes
end

#destroyedObject

Returns the value of attribute destroyed.



35
36
37
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 35

def destroyed
  @destroyed
end

#modelObject

Returns the value of attribute model.



31
32
33
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 31

def model
  @model
end

#synced_attributesObject

Returns the value of attribute synced_attributes.



37
38
39
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 37

def synced_attributes
  @synced_attributes
end

#updated_duringObject

Returns the value of attribute updated_during.



36
37
38
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 36

def updated_during
  @updated_during
end

#vectorObject

Returns the value of attribute vector.



30
31
32
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 30

def vector
  @vector
end

#virginObject

Returns the value of attribute virgin.



38
39
40
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 38

def virgin
  @virgin
end

Class Method Details

.add_to_outer_scopes(item) ⇒ Object



461
462
463
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 461

def add_to_outer_scopes(item)
  @outer_scopes << item
end

.catch_db_requests(return_val = nil) ⇒ Object



489
490
491
492
493
494
495
496
497
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 489

def catch_db_requests(return_val = nil)
  @catch_db_requests = true
  yield
rescue DbRequestMade => e
  puts "Warning request for server side data during scope evaluation: #{e.message}"
  return_val
ensure
  @catch_db_requests = false
end

.class_scopes(model) ⇒ Object



64
65
66
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 64

def self.class_scopes(model)
  @class_scopes[model.base_class]
end

.data_loading?Boolean

While data is being loaded from the server certain internal behaviors need to change for example records all record changes are synced as they happen. This is implemented this way so that the ServerDataCache class can use pure active record methods in its implementation

Returns:

  • (Boolean)


45
46
47
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 45

def self.data_loading?
  @data_loading
end

.default_scopeObject



453
454
455
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 453

def default_scope
  @class_scopes[:default_scope]
end

.define_attribute_methodsObject



68
69
70
71
72
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 68

def self.define_attribute_methods
  public_columns_hash.keys.each do |model|
    Object.const_get(model).define_attribute_methods rescue nil
  end
end

.deprecation_warning(model, message) ⇒ Object



33
34
35
36
37
38
39
40
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 33

def self.deprecation_warning(model, message)
  @deprecation_messages ||= []
  message = "Warning: Deprecated feature used in #{model}. #{message}"
  unless @deprecation_messages.include? message
    @deprecation_messages << message
    log message, :warning
  end
end

.destroy_record(model, id, vector, acting_user) ⇒ Object



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 526

def self.destroy_record(model, id, vector, acting_user)
  model = Object.const_get(model)
  record = if id
    model.find(id)
  else
    ServerDataCache.new(acting_user, {})[*vector]
  end


  record.check_permission_with_acting_user(acting_user, :destroy_permitted?).destroy
  {success: true, attributes: {}}

rescue Exception => e
  ReactiveRecord::Pry.rescued(e)
  {success: false, record: record, message: e}
end

.exists?(model, id) ⇒ Boolean

helper so we can tell if model exists. We need this so we can detect if a record has local changes that are out of sync.

Returns:

  • (Boolean)


327
328
329
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 327

def self.exists?(model, id)
  @records[model].detect { |record| record.attributes[model.primary_key] == id }
end

.find(model, attribute, value) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 74

def self.find(model, attribute, value)
  # will return the unique record with this attribute-value pair
  # value cannot be an association or aggregation

  model = model.base_class
  # already have a record with this attribute-value pair?
  record = @records[model].detect { |record| record.attributes[attribute] == value}
  unless record
    # if not, and then the record may be loaded, but not have this attribute set yet,
    # so find the id of of record with the attribute-value pair, and see if that is loaded.
    # find_in_db returns nil if we are not prerendering which will force us to create a new record
    # because there is no way of knowing the id.
    if attribute != model.primary_key and id = find_in_db(model, attribute, value)
      record = @records[model].detect { |record| record.id == id}
    end
    # if we don't have a record then create one
    (record = new(model)).vector = [model, ["find_by_#{attribute}", value]] unless record
    # and set the value
    record.sync_attribute(attribute, value)
    # and set the primary if we have one
    record.sync_attribute(model.primary_key, id) if id
  end
  # finally initialize and return the ar_instance
  record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
end

.find_by_object_id(model, object_id) ⇒ Object



100
101
102
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 100

def self.find_by_object_id(model, object_id)
  @records[model].detect { |record| record.object_id == object_id }.ar_instance
end

.find_record(model, id, vector, save) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 301

def self.find_record(model, id, vector, save)
  if !save
    found = vector[1..-1].inject(vector[0]) do |object, method|
      if object.nil? # happens if you try to do an all on empty scope followed by more scopes
        object
      elsif method.is_a? Array
        if method[0] == 'new'
          object.new
        else
          object.send(*method)
        end
      elsif method.is_a? String and method[0] == '*'
        object[method.gsub(/^\*/,'').to_i]
      else
        object.send(method)
      end
    end
    if id and (found.nil? or !(found.class <= model) or (found.id and found.id.to_s != id.to_s))
      raise "Inconsistent data sent to server - #{model.name}.find(#{id}) != [#{vector}]"
    end
    found
  elsif id
    model.find(id)
  else
    model.new
  end
end

.gather_records(records_to_process, force, record_being_saved) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 177

def self.gather_records(records_to_process, force, record_being_saved)
  # we want to pass not just the model data to save, but also enough information so that on return from the server
  # we can update the models on the client

  # input
  # list of records to process, will grow as we chase associations
  # outputs
  models = [] # the actual data to save {id: record.object_id, model: record.model.model_name, attributes: changed_attributes}
  associations = [] # {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}

  # used to keep track of records that have been processed for effeciency
  # for quick lookup of records that have been or will be processed [record.object_id] => record
  records_to_process = records_to_process.uniq
  backing_records = Hash[*records_to_process.collect { |record| [record.object_id, record] }.flatten(1)]

  add_new_association = lambda do |record, attribute, assoc_record|
    unless backing_records[assoc_record.object_id]
      records_to_process << assoc_record
      backing_records[assoc_record.object_id] = assoc_record
    end
    associations << {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
  end

  record_index = 0
  while(record_index < records_to_process.count)
    record = records_to_process[record_index]
    if record.id.loading? and record_being_saved
      raise "Attempt to save a model while it or an associated model is still loading: model being saved: #{record_being_saved.model}:#{record_being_saved.id}#{', associated model: '+record.model.to_s if record != record_being_saved}"
    end
    output_attributes = {record.model.primary_key => record.id.loading? ? nil : record.id}
    vector = record.vector || [record.model.model_name, ["new", record.object_id]]
    models << {id: record.object_id, model: record.model.model_name, attributes: output_attributes, vector: vector}
    record.attributes.each do |attribute, value|
      if association = record.model.reflect_on_association(attribute)
        if association.collection?
          # following line changed from .all to .collection on 10/28
          [*value.collection, *value.unsaved_children].each do |assoc|
            add_new_association.call(record, attribute, assoc.backing_record) if assoc.changed?(association.inverse_of) or assoc.new?
          end
        elsif record.new? || record.changed?(attribute) || (record == record_being_saved && force)
          if value.nil?
            output_attributes[attribute] = nil
          else
            add_new_association.call record, attribute, value.backing_record
          end
        end
      elsif aggregation = record.model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
        add_new_association.call record, attribute, value.backing_record unless value.nil?
      elsif aggregation
        new_value = aggregation.serialize(value)
        output_attributes[attribute] = new_value if record.changed?(attribute) or new_value != aggregation.serialize(record.synced_attributes[attribute])
      elsif record.new? or record.changed?(attribute)
        output_attributes[attribute] = value
      end
    end if record.new? || record.changed? || (record == record_being_saved && force)
    record_index += 1
  end
  [models, associations, backing_records]
end

.get_type_hash(record) ⇒ Object



171
172
173
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 171

def self.get_type_hash(record)
  {record.class.inheritance_column => record[record.class.inheritance_column]}
end

.infer_type_from_hash(klass, hash) ⇒ Object



437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 437

def self.infer_type_from_hash(klass, hash)
  klass = klass.base_class
  return klass unless hash
  type = hash[klass.inheritance_column]
  begin
    return Object.const_get(type)
  rescue Exception => e
    message = "Could not subclass #{@model_klass.model_name} as #{type}.  Perhaps #{type} class has not been required. Exception: #{e}"
    `console.error(#{message})`
  end if type
  klass
end

.is_enum?(record, key) ⇒ Boolean

Returns:

  • (Boolean)


330
331
332
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 330

def self.is_enum?(record, key)
  record.class.respond_to?(:defined_enums) && record.class.defined_enums[key]
end

.load_data(&block) ⇒ Object



53
54
55
56
57
58
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 53

def self.load_data(&block)
  current_data_loading, @data_loading = [@data_loading, true]
  yield
ensure
  @data_loading = current_data_loading
end

.load_from_db(record, *vector) ⇒ Object

queue up fetches, and at the end of each rendering cycle fetch the records notify that loads are pending



104
105
106
107
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 104

def load_from_db(*args)
  raise DbRequestMade, args if @catch_db_requests
  pre_synchromesh_load_from_db(*args)
end

.load_from_json(json, target = nil) ⇒ Object



60
61
62
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 60

def self.load_from_json(json, target = nil)
  load_data { ServerDataCache.load_from_json(json, target) }
end

.new_from_vector(model, aggregate_owner, *vector) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 104

def self.new_from_vector(model, aggregate_owner, *vector)
  # this is the equivilent of find but for associations and aggregations
  # because we are not fetching a specific attribute yet, there is NO communication with the
  # server.  That only happens during find.
  model = model.base_class

  # do we already have a record with this vector?  If so return it, otherwise make a new one.

  record = @records[model].detect { |record| record.vector == vector }
  unless record
    record = new model
    record.vector = vector
  end

  record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)

  if aggregate_owner
    record.aggregate_owner = aggregate_owner
    record.aggregate_attribute = vector.last
    aggregate_owner.attributes[vector.last] = record.ar_instance
  end

  record.ar_instance

end

.pre_synchromesh_load_from_dbObject



499
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 499

alias pre_synchromesh_load_from_db load_from_db

.save_records(models, associations, acting_user, validate, save) ⇒ Object



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 334

def self.save_records(models, associations, acting_user, validate, save)
  reactive_records = {}
  vectors = {}
  new_models = []
  saved_models = []
  dont_save_list = []

  models.each do |model_to_save|
    attributes = model_to_save[:attributes]
    model = Object.const_get(model_to_save[:model])
    id = attributes.delete(model.primary_key) if model.respond_to? :primary_key # if we are saving existing model primary key value will be present
    vector = model_to_save[:vector]
    vector = [vector[0].constantize] + vector[1..-1].collect do |method|
      if method.is_a?(Array) and method.first == "find_by_id"
        ["find", method.last]
      else
        method
      end
    end
    reactive_records[model_to_save[:id]] = vectors[vector] = record = find_record(model, id, vector, save)
    if record and record.respond_to?(:id) and record.id
      # we have an already exising activerecord model
      keys = record.attributes.keys
      attributes.each do |key, value|
        if is_enum?(record, key)
          record.send("#{key}=",value)
        elsif keys.include? key
          record[key] = value
        elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key.to_sym) and !(aggregation.klass < ActiveRecord::Base)
          aggregation.mapping.each_with_index do |pair, i|
            record[pair.first] = value[i]
          end
        elsif record.respond_to? "#{key}="
          record.send("#{key}=",value)
        else
          # TODO once reading schema.rb on client is implemented throw an error here
        end
      end
    elsif record
      # either the model is new, or its not even an active record model
      dont_save_list << record unless save
      keys = record.attributes.keys
      attributes.each do |key, value|
        if is_enum?(record, key)
          record.send("#{key}=",value)
        elsif keys.include? key
          record[key] = value
        elsif !value.nil? and aggregation = record.class.reflect_on_aggregation(key) and !(aggregation.klass < ActiveRecord::Base)
          aggregation.mapping.each_with_index do |pair, i|
            record[pair.first] = value[i]
          end
        elsif key.to_s != "id" and record.respond_to?("#{key}=")  # server side methods can get included and we won't be able to write them...
          # for example if you have a server side method foo, that you "get" on a new record, then later that value will get sent to the server
          # we should track better these server side methods so this does not happen
          record.send("#{key}=",value)
        end
      end
      new_models << record
    end
  end

  #puts "!!!!!!!!!!!!!!attributes updated"
  ActiveRecord::Base.transaction do
    associations.each do |association|
      parent = reactive_records[association[:parent_id]]
      next unless parent
      #parent.instance_variable_set("@reactive_record_#{association[:attribute]}_changed", true) remove this????
      if parent.class.reflect_on_aggregation(association[:attribute].to_sym)
        #puts ">>>>>>AGGREGATE>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
        aggregate = reactive_records[association[:child_id]]
        dont_save_list << aggregate
        current_attributes = parent.send(association[:attribute]).attributes
        #puts "current parent attributes = #{current_attributes}"
        new_attributes = aggregate.attributes
        #puts "current child attributes = #{new_attributes}"
        merged_attributes = current_attributes.merge(new_attributes) { |k, current_attr, new_attr| aggregate.send("#{k}_changed?") ? new_attr : current_attr}
        #puts "merged attributes = #{merged_attributes}"
        aggregate.assign_attributes(merged_attributes)
        #puts "aggregate attributes after merge = #{aggregate.attributes}"
        parent.send("#{association[:attribute]}=", aggregate)
        #puts "updated  is frozen? #{aggregate.frozen?}, parent attributes = #{parent.send(association[:attribute]).attributes}"
      elsif parent.class.reflect_on_association(association[:attribute].to_sym).nil?
        raise "Missing association :#{association[:attribute]} for #{parent.class.name}.  Was association defined on opal side only?"
      elsif parent.class.reflect_on_association(association[:attribute].to_sym).collection?
        #puts ">>>>>>>>>> #{parent.class.name}.send('#{association[:attribute]}') << #{reactive_records[association[:child_id]]})"
        dont_save_list.delete(parent)
        if false and parent.new?
          parent.send("#{association[:attribute]}") << reactive_records[association[:child_id]] if parent.new?
          #puts "updated"
        else
          #puts "skipped"
        end
      else
        #puts ">>>>ASSOCIATION>>>> #{parent.class.name}.send('#{association[:attribute]}=', #{reactive_records[association[:child_id]]})"
        parent.send("#{association[:attribute]}=", reactive_records[association[:child_id]])
        dont_save_list.delete(parent)
        #puts "updated"
      end
    end if associations

    #puts "!!!!!!!!!!!!associations updated"

    has_errors = false

    #puts "ready to start saving... dont_save_list = #{dont_save_list}"

    saved_models = reactive_records.collect do |reactive_record_id, model|
      #puts "saving rr_id: #{reactive_record_id} model.object_id: #{model.object_id} frozen? <#{model.frozen?}>"
      if model and (model.frozen? or dont_save_list.include?(model) or model.changed.include?(model.class.primary_key))
        # the above check for changed including the private key happens if you have an aggregate that includes its own id
        #puts "validating frozen model #{model.class.name} #{model} (reactive_record_id = #{reactive_record_id})"
        valid = model.valid?
        #puts "has_errors before = #{has_errors}, validate= #{validate}, !valid= #{!valid}  (validate and !valid) #{validate and !valid}"
        has_errors ||= (validate and !valid)
        #puts "validation complete errors = <#{!valid}>, #{model.errors.messages} has_errors #{has_errors}"
        [reactive_record_id, model.class.name, model.attributes,  (valid ? nil : model.errors.messages)]
      elsif model and (!model.id or model.changed?)
        #puts "saving #{model.class.name} #{model} (reactive_record_id = #{reactive_record_id})"
        saved = model.check_permission_with_acting_user(acting_user, new_models.include?(model) ? :create_permitted? : :update_permitted?).save(validate: validate)
        has_errors ||= !saved
        messages = model.errors.messages if (validate and !saved) or (!validate and !model.valid?)
        #puts "saved complete errors = <#{!saved}>, #{messages} has_errors #{has_errors}"
        [reactive_record_id, model.class.name, model.attributes, messages]
      end
    end.compact

    raise "Could not save all models" if has_errors

    if save

      {success: true, saved_models: saved_models }

    else

      vectors.each { |vector, model| model.reload unless model.nil? or model.new_record? or model.frozen? }
      vectors

    end

  end

rescue Exception => e
  ReactiveRecord::Pry.rescued(e)
  if save
    {success: false, saved_models: saved_models, message: e}
  else
    {}
  end
end

.schedule_fetchObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 135

def self.schedule_fetch
  @fetch_scheduled ||= after(0) do
    if @pending_fetches.count > 0  # during testing we might reset the context while there are pending fetches otherwise this would never normally happen
      last_fetch_at = @last_fetch_at
      @last_fetch_at = Time.now
      pending_fetches = @pending_fetches.uniq
      models, associations = gather_records(@pending_records, false, nil)
      log(["Server Fetching: %o", pending_fetches.to_n])
      start_time = Time.now
      Operations::Fetch(models: models, associations: associations, pending_fetches: pending_fetches)
        .then do |response|
          fetch_time = Time.now
          log("       Fetched in:   #{(fetch_time-start_time).to_i}s")
          begin
            ReactiveRecord::Base.load_from_json(response)
          rescue Exception => e
            log("Unexpected exception raised while loading json from server: #{e}", :error)
          end
          log("       Processed in: #{(Time.now-fetch_time).to_i}s")
          log(["       Returned: %o", response.to_n])
          ReactiveRecord.run_blocks_to_load last_fetch_at
          ReactiveRecord::WhileLoading.loaded_at last_fetch_at
          ReactiveRecord::WhileLoading.quiet! if @pending_fetches.empty?
        end
        .fail do |response|
          log("Fetch failed", :error)
          # not sure what response was supposed to look like here was response.body before conversion to operations....
          ReactiveRecord.run_blocks_to_load(last_fetch_at, response)
        end
      @pending_fetches = []
      @pending_records = []
      @fetch_scheduled = nil
    end
  end
end

.unscopedObject



457
458
459
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 457

def unscoped
  @class_scopes[:unscoped]
end

.when_not_saving(model) ⇒ Object

when_not_saving will wait until reactive-record is not saving a model. Currently there is no easy way to do this without polling.



468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 468

def self.when_not_saving(model)
  if @records[model].detect(&:saving?)
    poller = every(0.1) do
      unless @records[model].detect(&:saving?)
        poller.stop
        yield model
      end
    end
  else
    yield model
  end
end

Instance Method Details

#apply_method(method) ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 391

def apply_method(method)
  # Fills in the value returned by sending "method" to the corresponding server side db instance
  if on_opal_server? and changed?
    log("Warning fetching virtual attributes (#{model.name}.#{method}) during prerendering on a changed or new model is not implemented.", :warning)
    # to implement this we would have to sync up any changes during prererendering with a set the cached models (see server_data_cache)
    # right now server_data cache is read only, BUT we could change this.  However it seems like a tails case.  Why would we create or update
    # a model during prerendering???
  end
  if !new?
    new_value = if association = @model.reflect_on_association(method)
      if association.collection?
        Collection.new(association.klass, @ar_instance, association, *vector, method)
      else
        find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
      end
    elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
      new_from_vector(aggregation.klass, self, *vector, method)
    elsif id and id != ''
      self.class.fetch_from_db([@model, [:find, id], *method]) || self.class.load_from_db(self, *(vector ? vector : [nil]), method)
    else  # its a attribute in an aggregate or we are on the client and don't know the id
      self.class.fetch_from_db([*vector, *method]) || self.class.load_from_db(self, *(vector ? vector : [nil]), method)
    end
    new_value = @attributes[method] if new_value.is_a? DummyValue and @attributes.has_key?(method)
    sync_attribute(method, new_value)
  elsif association = @model.reflect_on_association(method) and association.collection?
    @attributes[method] = Collection.new(association.klass, @ar_instance, association)
  elsif aggregation = @model.reflect_on_aggregation(method) and (aggregation.klass < ActiveRecord::Base)
    @attributes[method] = aggregation.klass.new.tap do |aggregate|
      backing_record = aggregate.backing_record
      backing_record.aggregate_owner = self
      backing_record.aggregate_attribute = method
    end
  elsif !aggregation and method != model.primary_key
    if model.columns_hash[method]
      new_value = convert(method, model.columns_hash[method][:default])
    else
      unless @attributes.key?(method)
        log("Warning: reading from new #{model.name}.#{method} before assignment.  Will fetch value from server.  This may not be what you expected!!", :warning)
      end
      new_value = self.class.load_from_db(self, *(vector ? vector : [nil]), method)
      new_value = @attributes[method] if new_value.is_a?(DummyValue) && @attributes.key?(method)
    end
    sync_attribute(method, new_value)
  end
end

#attributesObject



167
168
169
170
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 167

def attributes
  @last_access_at = Time.now
  @attributes
end

#changed?(*args) ⇒ Boolean

Returns:

  • (Boolean)


246
247
248
249
250
251
252
253
254
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 246

def changed?(*args)
  if args.count == 0
    React::State.get_state(self, "!CHANGED!")
    !changed_attributes.empty?
  else
    React::State.get_state(self, args[0])
    changed_attributes.include? args[0]
  end
end

#column_type(attr) ⇒ Object



8
9
10
11
12
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 8

def column_type(attr)
  column_hash = columns_hash[attr]
  return nil unless column_hash
  column_hash[:sql_type_metadata] && column_hash[:sql_type_metadata][:type]
end

#columns_hashObject



4
5
6
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 4

def columns_hash
  model.columns_hash
end

#convert(attr, val) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 59

def convert(attr, val)
  column_type = column_type(attr)
  return val if !column_type || val.loading? || (!val && column_type != :boolean)
  conversion_method = "convert_#{column_type}"
  return send(conversion_method, val) if respond_to? conversion_method
  val
end

#convert_boolean(val) ⇒ Object



37
38
39
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 37

def convert_boolean(val)
  !['false', false, nil, 0].include?(val)
end

#convert_date(val) ⇒ Object



27
28
29
30
31
32
33
34
35
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 27

def convert_date(val)
  if val.is_a?(Time)
    Date.parse(val.strftime('%d/%m/%Y'))
  elsif val.is_a?(Date)
    val
  else
    Date.parse(val)
  end
end

#convert_datetime(val) ⇒ Object Also known as: convert_time, convert_timestamp



14
15
16
17
18
19
20
21
22
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 14

def convert_datetime(val)
  if val.is_a?(Numeric)
    Time.at(val)
  elsif val.is_a?(Time)
    val
  else
    Time.parse(val)
  end
end

#convert_float(val) ⇒ Object Also known as: convert_decimal



47
48
49
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 47

def convert_float(val)
  Float(val)
end

#convert_integer(val) ⇒ Object Also known as: convert_bigint



41
42
43
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 41

def convert_integer(val)
  Integer(`parseInt(#{val})`)
end

#convert_text(val) ⇒ Object Also known as: convert_string



53
54
55
# File 'lib/reactive_record/active_record/reactive_record/column_types.rb', line 53

def convert_text(val)
  val.to_s
end

#data_loading?Boolean

Returns:

  • (Boolean)


49
50
51
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 49

def data_loading?
  self.class.data_loading?
end

#deprecation_warning(message) ⇒ Object



42
43
44
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 42

def deprecation_warning(message)
  self.class.deprecation_warning(model, message)
end

#destroy(&block) ⇒ Object



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 490

def destroy(&block)

  return if @destroyed

  #destroy_associations

  promise = Promise.new

  if !data_loading? and (id or vector)
    Operations::Destroy(model: ar_instance.model_name, id: id, vector: vector)
    .then do |response|
      Broadcast.to_self ar_instance
      yield response[:success], response[:message] if block
      promise.resolve response
    end
  else
    destroy_associations
    # sync_unscoped_collection! # ? should we do this here was NOT being done before hypermesh integration
    yield true, nil if block
    promise.resolve({success: true})
  end

  # DO NOT CLEAR ATTRIBUTES.  Records that are not found, are destroyed, and if they are searched for again, we want to make
  # sure to find them.  We may want to change this, and provide a separate flag called not_found.  In this case you
  # would put these lines here:
  # @attributes = {}
  # sync!
  # and modify server_data_cache so that it does NOT call destroy

  @destroyed = true

  promise
end

#destroy_associationsObject



507
508
509
510
511
512
513
514
515
516
517
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 507

def destroy_associations
  @destroyed = false
  model.reflect_on_all_associations.each do |association|
    if association.collection?
      attributes[association.attribute].replace([]) if attributes[association.attribute]
    else
      @ar_instance.send("#{association.attribute}=", nil)
    end
  end
  @destroyed = true
end

#dont_update_attribute?(attribute, value) ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
198
199
200
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 195

def dont_update_attribute?(attribute, value)
  return false if attributes[attribute].is_a?(DummyValue)
  return false unless attributes.key?(attribute)
  return false if attributes[attribute] != value
  true
end

#errorsObject



256
257
258
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 256

def errors
  @errors ||= ActiveModel::Error.new
end

#errors!(errors) ⇒ Object



345
346
347
348
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 345

def errors!(errors)
  @saving = false
  @errors = errors and ActiveModel::Error.new(errors)
end

#find(*args) ⇒ Object



140
141
142
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 140

def find(*args)
  self.class.find(*args)
end

#find_association(association, id) ⇒ Object



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 369

def find_association(association, id)
  inverse_of = association.inverse_of
  instance = if id
    find(association.klass, association.klass.primary_key, id)
  else
    new_from_vector(association.klass, nil, *vector, association.attribute)
  end
  instance_backing_record_attributes = instance.backing_record.attributes
  inverse_association = association.klass.reflect_on_association(inverse_of)
  if inverse_association.collection?
    instance_backing_record_attributes[inverse_of] = if id and id != ""
      Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
    else
      Collection.new(@model, instance, inverse_association, *vector, association.attribute, inverse_of)
    end unless instance_backing_record_attributes[inverse_of]
    instance_backing_record_attributes[inverse_of].replace [@ar_instance]
  else
    instance_backing_record_attributes[inverse_of] = @ar_instance
  end unless association.through_association? || instance_backing_record_attributes.key?(inverse_of)
  instance
end

#get_columns_info_for_vector(vector) ⇒ Object



121
122
123
124
125
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 121

def get_columns_info_for_vector(vector)
  method_name = vector.last
  method_name = method_name.first if method_name.is_a? Array
  model.columns_hash[method_name] || model.server_methods[method_name]
end

#idObject



152
153
154
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 152

def id
  attributes[primary_key]
end

#id=(value) ⇒ Object



156
157
158
159
160
161
162
163
164
165
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 156

def id=(value)
  # value can be nil if we are loading an aggregate otherwise check if it already exists
  if !(value and existing_record = records[@model].detect { |record| record.attributes[primary_key] == value})
    attributes[primary_key] = value
  else
    @ar_instance.instance_variable_set(:@backing_record, existing_record)
    existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
  end
  value
end

#initialize_collectionsObject

called when we have a newly created record, to initialize any nil collections to empty arrays. We can do this because if its a brand new record, then any collections that are still nil must not have any children.



264
265
266
267
268
269
270
271
272
273
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 264

def initialize_collections
  if (!vector || vector.empty?) && id && id != ''
    @vector = [@model, ["find_by_#{@model.primary_key}", id]]
  end
  @model.reflect_on_all_associations.each do |assoc|
    if assoc.collection? && attributes[assoc.attribute].nil?
      ar_instance.send("#{assoc.attribute}=", [])
    end
  end
end

#new?Boolean

Returns:

  • (Boolean)


365
366
367
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 365

def new?
  !id and !vector
end

#new_from_vector(*args) ⇒ Object



144
145
146
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 144

def new_from_vector(*args)
  self.class.new_from_vector(*args)
end

#overwrite_has_many_collection(association, value) ⇒ Object



47
48
49
50
51
52
53
54
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 47

def overwrite_has_many_collection(association, value)
  # create a new collection to hold value, shove it in, and return the new collection
  # the replace method will take care of updating the inverse belongs_to links as
  # the collection is overwritten
  Collection.new(association.klass, @ar_instance, association).tap do |collection|
    collection.replace(value || [])
  end
end

#primary_keyObject



148
149
150
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 148

def primary_key
  @model.primary_key
end

#push_onto_collection(model, association, ar_instance) ⇒ Object



93
94
95
96
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 93

def push_onto_collection(model, association, ar_instance)
  attributes[association.attribute] ||= Collection.new(model, @ar_instance, association)
  attributes[association.attribute] << ar_instance
end

#reactive_get!(attribute, reload = nil) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 172

def reactive_get!(attribute, reload = nil)
  @virgin = false unless data_loading?
  unless @destroyed
    if @attributes.has_key? attribute
      attributes[attribute].notify if @attributes[attribute].is_a? DummyValue
      apply_method(attribute) if reload
    else
      apply_method(attribute)
    end
    React::State.get_state(self, attribute) unless data_loading?
    attributes[attribute]
  end
end

#reactive_set!(attribute, value) ⇒ Object



186
187
188
189
190
191
192
193
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 186

def reactive_set!(attribute, value)
  @virgin = false unless data_loading?
  return value if @destroyed || dont_update_attribute?(attribute, value)
  return attributes[attribute] if update_aggregate(attribute, value)
  value = update_relationships(attribute, value)
  update_attribute(attribute, value)
  value
end

#recordsObject



46
47
48
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 46

def records
  self.class.instance_variable_get(:@records)
end

#revertObject



331
332
333
334
335
336
337
338
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 331

def revert
  @changed_attributes.dup.each do |attribute|
    @ar_instance.send("#{attribute}=", @synced_attributes[attribute])
    @attributes.delete(attribute) unless @synced_attributes.key?(attribute)
  end
  @changed_attributes = []
  @errors = nil
end

#save(validate, force, &block) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 237

def save(validate, force, &block)
  if data_loading?
    sync!
  elsif force or changed?
    HyperMesh.load do
      ReactiveRecord.loads_pending! unless self.class.pending_fetches.empty?
    end.then { save_to_server(validate, force, &block) }
    #save_to_server(validate, force, &block)
  else
    promise = Promise.new
    yield true, nil, [] if block
    promise.resolve({success: true})
    promise
  end
end

#save_to_server(validate, force, &block) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 253

def save_to_server(validate, force, &block)
  models, associations, backing_records = self.class.gather_records([self], force, self)

  backing_records.each { |id, record| record.saving! }

  promise = Promise.new
  Operations::Save(models: models, associations: associations, validate: validate)
  .then do |response|
    begin
      response[:models] = response[:saved_models].collect do |item|
        backing_records[item[0]].ar_instance
      end

      if response[:success]
        response[:saved_models].each do | item |
          Broadcast.to_self backing_records[item[0]].ar_instance, item[2]
        end
      else
        log("Reactive Record Save Failed: #{response[:message]}", :error)
        response[:saved_models].each do | item |
          log("  Model: #{item[1]}[#{item[0]}]  Attributes: #{item[2]}  Errors: #{item[3]}", :error) if item[3]
        end
      end

      response[:saved_models].each do | item |
        backing_records[item[0]].sync_unscoped_collection!
        backing_records[item[0]].errors! item[3]
      end

      yield response[:success], response[:message], response[:models]  if block
      promise.resolve response  # TODO this could be problematic... there was no .json here, so .... what's to do?

      backing_records.each { |id, record| record.saved! }

    rescue Exception => e
      log("Exception raised while saving - #{e}", :error)
    end
  end
  promise
rescue Exception => e
  log("Exception raised while saving - #{e}", :error)
  yield false, e.message, [] if block
  promise.resolve({success: false, message: e.message, models: []})
  promise
end

#saved!Object

sets saving to false AND notifies



350
351
352
353
354
355
356
357
358
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 350

def saved!  # sets saving to false AND notifies
  @saving = false
  if !@errors or @errors.empty?
    React::State.set_state(self, self, :saved)
  elsif !data_loading?
    React::State.set_state(self, self, :error)
  end
  self
end

#saving!Object



340
341
342
343
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 340

def saving!
  React::State.set_state(self, self, :saving) unless data_loading?
  @saving = true
end

#saving?Boolean

Returns:

  • (Boolean)


360
361
362
363
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 360

def saving?
  React::State.get_state(self, self)
  @saving
end

#sync!(hash = {}) ⇒ Object

sync! now will also initialize any nil collections



276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 276

def sync!(hash = {}) # does NOT notify (see saved! for notification)
  hash.each do |attr, value|
    @attributes[attr] = convert(attr, value)
  end
  @synced_attributes = {}
  @synced_attributes.each { |attribute, value| sync_attribute(key, value) }
  @changed_attributes = []
  @saving = false
  @errors = nil
  # set the vector and clear collections - this only happens when a new record is saved
  initialize_collections if (!vector || vector.empty?) && id && id != ''
  self
end

#sync_attribute(attribute, value) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 305

def sync_attribute(attribute, value)

  @synced_attributes[attribute] = attributes[attribute] = value

  #@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection

  if value.is_a? Collection
    @synced_attributes[attribute] = value.dup_for_sync
  elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
    value.backing_record.sync!
  elsif aggregation
    @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
  elsif !model.reflect_on_association(attribute)
    @synced_attributes[attribute] = JSON.parse(value.to_json)
  end

  @changed_attributes.delete(attribute)
  value
end

#sync_unscoped_collection!Object

this keeps the unscoped collection up to date. to collections that just have a count



293
294
295
296
297
298
299
300
301
302
303
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 293

def sync_unscoped_collection!
  if destroyed
    return if @destroy_sync
    @destroy_sync = true
  else
    return if @create_sync
    @create_sync = true
  end
  model.unscoped << ar_instance
  @synced_with_unscoped = !@synced_with_unscoped
end

#update_aggregate(attribute, value) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 4

def update_aggregate(attribute, value)
  # if attribute is an aggregate then
  # match and update all fields in the aggregate from value and return true
  # otherwise return false
  aggregation = @model.reflect_on_aggregation(attribute)
  return false unless aggregation && (aggregation.klass < ActiveRecord::Base)
  if value
    value_attributes = value.backing_record.attributes
    update_mapped_attributes(aggregation) { |attr| value_attributes[attr] }
  else
    update_mapped_attributes(aggregation) { nil }
  end
  true
end

#update_attribute(attribute, *args) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 202

def update_attribute(attribute, *args)
  value = args[0]
  if args.count != 0 and data_loading?
    if (aggregation = model.reflect_on_aggregation(attribute)) and !(aggregation.klass < ActiveRecord::Base)
      @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
    else
      @synced_attributes[attribute] = value
    end
  end
  if @virgin
    attributes[attribute] = value if args.count != 0
    return
  end
  changed = if args.count == 0
    if (association = @model.reflect_on_association(attribute)) and association.collection?
      attributes[attribute] != @synced_attributes[attribute]
    else
      !attributes[attribute].backing_record.changed_attributes.empty?
    end
  elsif (association = @model.reflect_on_association(attribute)) and association.collection?
    value != @synced_attributes[attribute]
  else
    !@synced_attributes.has_key?(attribute) or @synced_attributes[attribute] != value
  end
  empty_before = changed_attributes.empty?
  if !changed
    changed_attributes.delete(attribute)
  elsif !changed_attributes.include?(attribute)
    changed_attributes << attribute
  end
  had_key = attributes.has_key? attribute
  current_value = attributes[attribute]
  attributes[attribute] = value if args.count != 0
  if !data_loading?
    React::State.set_state(self, attribute, value)
  elsif on_opal_client? and had_key and current_value.loaded? and current_value != value and args.count > 0  # this is to handle changes in already loaded server side methods
    React::State.set_state(self, attribute, value, true)
  end
  if empty_before != changed_attributes.empty?
    React::State.set_state(self, "!CHANGED!", !changed_attributes.empty?, true) unless on_opal_server? or data_loading?
    aggregate_owner.update_attribute(aggregate_attribute) if aggregate_owner
  end
end

#update_belongs_to_association(association, value) ⇒ Object



56
57
58
59
60
61
62
63
64
65
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 56

def update_belongs_to_association(association, value)
  # either update update the inverse has_many collection or individual belongs_to
  # inverse values
  if association.inverse.collection?
    update_has_many_through_associations(association, value)
    update_inverse_collections(association, value)
  else
    update_inverse_attribute(association, value)
  end
end

#update_has_many_through_associations(association, value) ⇒ Object



98
99
100
101
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 98

def update_has_many_through_associations(association, value)
  association.through_associations.each { |ta| update_through_association(ta, value) }
  association.source_associations.each { |sa| update_source_association(sa, value) }
end

#update_inverse_attribute(association, value) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 67

def update_inverse_attribute(association, value)
  # when updating the inverse attribute of a belongs_to that is itself a belongs_to
  # (i.e. 1-1 relationship) we clear the existing inverse value and then
  # write the current record to the new value
  current_value = attributes[association.attribute]
  inverse_attr = association.inverse.attribute
  current_value.attributes[inverse_attr] = nil unless current_value.nil?
  return if value.nil?
  value.attributes[inverse_attr] = @ar_instance
  return if data_loading?
  React::State.set_state(value.backing_record, inverse_attr, @ar_instance)
end

#update_inverse_collections(association, value) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 80

def update_inverse_collections(association, value)
  # when updating an inverse attribute of a belongs_to that is a has_many (i.e. a collection)
  # we need to first remove the current associated value (if non-nil), then add the new
  # value to the collection.  If the inverse collection is not yet initialized we do it here.
  current_value = attributes[association.attribute]
  inverse_attr = association.inverse.attribute
  if value.nil?
    current_value.attributes[inverse_attr].delete(@ar_instance) unless current_value.nil?
  else
    value.backing_record.push_onto_collection(@model, association.inverse, @ar_instance)
  end
end

#update_mapped_attributes(aggregation) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 19

def update_mapped_attributes(aggregation)
  # insure the aggregate attr is initialized, clear the virt flag, the caller
  # will yield each of the matching attribute values
  attr = aggregation.attribute
  attributes[attr] ||= aggregation.klass.new if new?
  aggregate_record = attributes[attr]
  raise 'uninitialized aggregate attribute - should never happen' unless aggregate_record
  aggregate_backing_record = aggregate_record.backing_record
  aggregate_backing_record.virgin = false
  aggregation.mapped_attributes.each do |mapped_attribute|
    aggregate_backing_record.update_attribute(mapped_attribute, yield(mapped_attribute))
  end
end

#update_relationships(attr, value) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 33

def update_relationships(attr, value)
  # update the inverse relationship, and any through relationships
  # return either the value, or in the case of updating a collection
  # return the new collection after value is overwritten into it.
  association = @model.reflect_on_association(attr)
  return value unless association
  if association.collection?
    overwrite_has_many_collection(association, value)
  else
    update_belongs_to_association(association, value)
    value
  end
end

#update_source_association(sa, new_source_value) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 118

def update_source_association(sa, new_source_value)
  # appointment.patient = patient_value (i.e. source is changing)
  # means appointment.doctor.patients.delete(appointment.patient)
  # means appointment.doctor.patients << patient_value
  belongs_to_value = attributes[sa.inverse.attribute]
  current_source_value = attributes[sa.source]
  return unless belongs_to_value
  unless belongs_to_value.attributes[sa.attribute].nil? || current_source_value.nil?
    belongs_to_value.attributes[sa.attribute].delete(current_source_value)
  end
  return unless new_source_value
  belongs_to_value.attributes[sa.attribute] ||= Collection.new(sa.klass, belongs_to_value, sa)
  belongs_to_value.attributes[sa.attribute] << new_source_value
end

#update_through_association(ta, new_belongs_to_value) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb', line 103

def update_through_association(ta, new_belongs_to_value)
  # appointment.doctor = doctor_new_value (i.e. through association is changing)
  # means appointment.doctor_new_value.patients << appointment.patient
  # and we have to appointment.doctor_current_value.patients.delete(appointment.patient)
  source_value = attributes[ta.source]
  current_belongs_to_value = attributes[ta.inverse.attribute]
  return unless source_value
  unless current_belongs_to_value.nil? || current_belongs_to_value.attributes[ta.attribute].nil?
    current_belongs_to_value.attributes[ta.attribute].delete(source_value)
  end
  return unless new_belongs_to_value
  new_belongs_to_value.attributes[ta.attribute] ||= Collection.new(ta.klass, new_belongs_to_value, ta)
  new_belongs_to_value.attributes[ta.attribute] << source_value
end