class ReactiveRecord::Base

ActiveRecord column access and conversion helpers

methods to update aggregrations and relations, called from reactive_set!

Attributes

last_fetch_at[R]
outer_scopes[R]
pending_fetches[R]
public_columns_hash[R]
aggregate_attribute[RW]
aggregate_owner[RW]
ar_instance[RW]

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)

changed_attributes[RW]
destroyed[RW]
model[RW]
synced_attributes[RW]
updated_during[RW]
vector[RW]
virgin[RW]

Public Class Methods

add_to_outer_scopes(item) click to toggle source
# 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) { || ... } click to toggle source
# 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) click to toggle source
# 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?() click to toggle source

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

# File lib/reactive_record/active_record/reactive_record/base.rb, line 45
def self.data_loading?
  @data_loading
end
default_scope() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 453
def default_scope
  @class_scopes[:default_scope]
end
define_attribute_methods() click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source

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.

# 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) click to toggle source

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

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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() { || ... } click to toggle source
# 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(*args) click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 501
def load_from_db(*args)
  raise DbRequestMade, args if @catch_db_requests
  pre_synchromesh_load_from_db(*args)
end
Also aliased as: pre_synchromesh_load_from_db
load_from_json(json, target = nil) click to toggle source
# 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(model, hash = {}, ar_instance = nil) click to toggle source
# 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
new_from_vector(model, aggregate_owner, *vector) click to toggle source
# 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_db(*args)
Alias for: load_from_db
save_records(models, associations, acting_user, validate, save) click to toggle source
# 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_fetch() click to toggle source
# 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
unscoped() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 457
def unscoped
  @class_scopes[:unscoped]
end
when_not_saving(model) { |model| ... } click to toggle source

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

# 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

Public Instance Methods

apply_method(method) click to toggle source
# 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
attributes() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 167
def attributes
  @last_access_at = Time.now
  @attributes
end
changed?(*args) click to toggle source
# 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) click to toggle source
# 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_hash() click to toggle source
# File lib/reactive_record/active_record/reactive_record/column_types.rb, line 4
def columns_hash
  model.columns_hash
end
convert(attr, val) click to toggle source
# 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_bigint(val)
Alias for: convert_integer
convert_boolean(val) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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
Also aliased as: convert_time, convert_timestamp
convert_decimal(val)
Alias for: convert_float
convert_float(val) click to toggle source
# File lib/reactive_record/active_record/reactive_record/column_types.rb, line 47
def convert_float(val)
  Float(val)
end
Also aliased as: convert_decimal
convert_integer(val) click to toggle source
# File lib/reactive_record/active_record/reactive_record/column_types.rb, line 41
def convert_integer(val)
  Integer(`parseInt(#{val})`)
end
Also aliased as: convert_bigint
convert_string(val)
Alias for: convert_text
convert_text(val) click to toggle source
# File lib/reactive_record/active_record/reactive_record/column_types.rb, line 53
def convert_text(val)
  val.to_s
end
Also aliased as: convert_string
convert_time(val)
Alias for: convert_datetime
convert_timestamp(val)
Alias for: convert_datetime
data_loading?() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 49
def data_loading?
  self.class.data_loading?
end
deprecation_warning(message) click to toggle source
# 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() { |response, response| ... } click to toggle source
# 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_associations() click to toggle source
# 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) click to toggle source
# 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
errors() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 256
def errors
  @errors ||= ActiveModel::Error.new
end
errors!(errors) click to toggle source
# 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) click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 140
def find(*args)
  self.class.find(*args)
end
find_association(association, id) click to toggle source
# 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) click to toggle source
# 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
id() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 152
def id
  attributes[primary_key]
end
id=(value) click to toggle source
# 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_collections() click to toggle source

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.

# 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?() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 365
def new?
  !id and !vector
end
new_from_vector(*args) click to toggle source
# 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) click to toggle source
# 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_key() click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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
records() click to toggle source
# File lib/reactive_record/active_record/reactive_record/isomorphic_base.rb, line 46
def records
  self.class.instance_variable_get(:@records)
end
revert() click to toggle source
# 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) { |true, nil, []| ... } click to toggle source
# 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) { |response, response, response| ... } click to toggle source
# 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!() click to toggle source
# 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!() click to toggle source
# 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?() click to toggle source
# File lib/reactive_record/active_record/reactive_record/base.rb, line 360
def saving?
  React::State.get_state(self, self)
  @saving
end
sync!(hash = {}) click to toggle source

sync! now will also initialize any nil collections

# 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) click to toggle source
# 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!() click to toggle source

this keeps the unscoped collection up to date. @destroy_sync and @create_sync prevent multiple insertions to collections that just have a count

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) { |mapped_attribute| ... } click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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