Class: ReactiveRecord::Base

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

Defined Under Namespace

Classes: 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.



146
147
148
149
150
151
152
153
154
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 146

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.



200
201
202
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 200

def last_fetch_at
  @last_fetch_at
end

.pending_fetchesObject (readonly)

Returns the value of attribute pending_fetches.



199
200
201
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 199

def pending_fetches
  @pending_fetches
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

.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

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



613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 613

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

.find(model, attribute, value) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 90

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



116
117
118
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 116

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



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
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 380

def self.find_record(model, id, vector, save)
  if !save
    found = vector[1..-1].inject(vector[0]) do |object, method|
      if 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



251
252
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
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 251

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}
    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?
          value.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



245
246
247
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 245

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

.infer_type_from_hash(klass, hash) ⇒ Object



448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 448

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)


406
407
408
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 406

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



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 75

def self.load_from_db(record, *vector)
  return nil unless on_opal_client? # this can happen when we are on the server and a nil value is returned for an attribute
  # only called from the client side
  # pushes the value of vector onto the a list of vectors that will be loaded from the server when the next
  # rendering cycle completes.
  # takes care of informing react that there are things to load, and schedules the loader to run
  # Note there is no equivilent to find_in_db, because each vector implicitly does a find.
  raise "attempt to do a find_by_id of nil.  This will return all records, and is not allowed" if vector[1] == ["find_by_id", nil]
  vector = [record.model.model_name, ["new", record.object_id]]+vector[1..-1] if vector[0].nil?
  unless data_loading?
    @pending_fetches << vector
    @pending_records << record if record
    schedule_fetch
  end
  DummyValue.new
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



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 120

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

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



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
483
484
485
486
487
488
489
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
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 410

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



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
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 204

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
      HTTP.post(`window.ReactiveRecordEnginePath`,
        payload: {
          json: {
            models:          models,
            associations:    associations,
            pending_fetches: pending_fetches
          }.to_json
        }
      ).then do |response|
        fetch_time = Time.now
        log("       Fetched in:   #{(fetch_time-start_time).to_i}s")
        begin
          ReactiveRecord::Base.load_from_json(response.json)
        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.json.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)
        ReactiveRecord.run_blocks_to_load(last_fetch_at, response.body)
      end
      @pending_fetches = []
      @pending_records = []
      @fetch_scheduled = nil
    end
  end
end

.sync_blocksObject



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

def self.sync_blocks
  # @sync_blocks[watch_model][sync_model][scope_name][...array of blocks...]
  @sync_blocks ||= Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = [] } } }
end

Instance Method Details

#apply_method(method) ⇒ Object



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
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 406

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
    unless @attributes.has_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 and @attributes.has_key?(method)
    sync_attribute(method, new_value)
  end
end

#attributesObject



183
184
185
186
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 183

def attributes
  @last_access_at = Time.now
  @attributes
end

#changed?(*args) ⇒ Boolean

Returns:

  • (Boolean)


300
301
302
303
304
305
306
307
308
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 300

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

#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

#destroy(&block) ⇒ Object



566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 566

def destroy(&block)

  return if @destroyed

  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

  promise = Promise.new

  if !data_loading? and (id or vector)
    HTTP.post(`window.ReactiveRecordEnginePath`+"/destroy",
      payload: {
        json: {
          model:  ar_instance.model_name,
          id:     id,
          vector: vector
        }.to_json
      }
    ).then do |response|
      sync_scopes
      yield response.json[:success], response.json[:message] if block
      promise.resolve response.json
    end
  else
    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

#errorsObject



310
311
312
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 310

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

#errors!(errors) ⇒ Object



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

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

#find(*args) ⇒ Object



156
157
158
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 156

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

#find_association(association, id) ⇒ Object



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 384

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 if inverse_of and !instance_backing_record_attributes.has_key?(inverse_of)
  instance
end

#idObject



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

def id
  attributes[primary_key]
end

#id=(value) ⇒ Object



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

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

#new?Boolean

Returns:

  • (Boolean)


380
381
382
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 380

def new?
  !id and !vector
end

#new_from_vector(*args) ⇒ Object



160
161
162
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 160

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

#primary_keyObject



164
165
166
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 164

def primary_key
  @model.primary_key
end

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



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 188

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



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
245
246
247
248
249
250
251
252
253
254
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 202

def reactive_set!(attribute, value)
  @virgin = false unless data_loading?
  unless @destroyed or (!(attributes[attribute].is_a? DummyValue) and attributes.has_key?(attribute) and attributes[attribute] == value)
    if association = @model.reflect_on_association(attribute)
      if association.collection?
        collection = Collection.new(association.klass, @ar_instance, association)
        collection.replace(value || [])
        value = collection
      else
        inverse_of = association.inverse_of
        inverse_association = association.klass.reflect_on_association(inverse_of)
        if inverse_association.collection?
          if value.nil?
            attributes[attribute].attributes[inverse_of].delete(@ar_instance) unless attributes[attribute].nil?
          elsif value.attributes[inverse_of]
            value.attributes[inverse_of] << @ar_instance
          else
            value.attributes[inverse_of] = Collection.new(@model, value, inverse_association)
            value.attributes[inverse_of].replace [@ar_instance]
          end
        elsif !value.nil?
          attributes[attribute].attributes[inverse_of] = nil unless attributes[attribute].nil?
          value.attributes[inverse_of] = @ar_instance
          React::State.set_state(value.backing_record, inverse_of, @ar_instance) unless data_loading?
        elsif attributes[attribute]
          attributes[attribute].attributes[inverse_of] = nil
        end
      end
    elsif aggregation = @model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)

      if new?
        attributes[attribute] ||= aggregation.klass.new
      elsif !attributes[attribute]
        raise "uninitialized aggregate attribute - should never happen"
      end

      aggregate_record = attributes[attribute].backing_record
      aggregate_record.virgin = false

      if value
        value_attributes = value.backing_record.attributes
        aggregation.mapped_attributes.each { |mapped_attribute| aggregate_record.update_attribute(mapped_attribute, value_attributes[mapped_attribute])}
      else
        aggregation.mapped_attributes.each { |mapped_attribute| aggregate_record.update_attribute(mapped_attribute, nil) }
      end

      return attributes[attribute]

    end
    update_attribute(attribute, value)
  end
  value
end

#recordsObject



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

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

#revertObject



346
347
348
349
350
351
352
353
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 346

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

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



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
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
# File 'lib/reactive_record/active_record/reactive_record/isomorphic_base.rb', line 310

def save(validate, force, &block)

  if data_loading?

    sync!

  elsif force or changed?

    begin

      models, associations, backing_records = self.class.gather_records([self], force, self)

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

      promise = Promise.new

      HTTP.post(`window.ReactiveRecordEnginePath`+"/save",
        payload: {
          json: {
            models:       models,
            associations: associations,
            validate:     validate
          }.to_json
        }
      ).then do |response|
        begin
          response.json[:models] = response.json[:saved_models].collect do |item|
            backing_records[item[0]].ar_instance
          end

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

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

          yield response.json[:success], response.json[:message], response.json[:models]  if block
          promise.resolve response.json

          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
  else
    promise = Promise.new
    yield true, nil, [] if block
    promise.resolve({success: true})
    promise
  end
end

#saved!Object

sets saving to false AND notifies



365
366
367
368
369
370
371
372
373
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 365

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



355
356
357
358
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 355

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

#saving?Boolean

Returns:

  • (Boolean)


375
376
377
378
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 375

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

#sync!(hash = {}) ⇒ Object

does NOT notify (see saved! for notification)



314
315
316
317
318
319
320
321
322
323
324
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 314

def sync!(hash = {})  # does NOT notify (see saved! for notification)
  @attributes.merge! hash
  @synced_attributes = {}
  @synced_attributes.each { |attribute, value| sync_attribute(key, value) }
  @changed_attributes = []
  @saving = false
  @errors = nil
  # set the vector - this only happens when a new record is saved
  @vector = [@model, ["find_by_#{@model.primary_key}", id]] if (!vector or vector.empty?) and id and id != ""
  self
end

#sync_attribute(attribute, value) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 326

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_scopesObject



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 73

def sync_scopes
  self.class.sync_blocks[self.model].each do |watching_class, scopes|
    scopes.each do |scope_name, blocks|
      blocks.each do |block|
        if block.arity > 0
          block.call watching_class.send(scope_name), @ar_instance
        elsif @ar_instance.instance_eval &block
          watching_class.send(scope_name) << @ar_instance
        else
          watching_class.send(scope_name).delete(@ar_instance)
        end
      end
    end
  end
  model.all << @ar_instance if ReactiveRecord::Base.class_scopes(model)[:all] # add this only if model.all has been fetched already
end

#update_attribute(attribute, *args) ⇒ Object



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
298
# File 'lib/reactive_record/active_record/reactive_record/base.rb', line 256

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