class Datasource::Base

Attributes

_associations[RW]
_attributes[RW]
_collection_context[RW]
_loader_order[RW]
_loaders[RW]
_update_scope[RW]
default_consumer_adapter[RW]

Should be set by consumer adapter library (e.g. for ActiveModelSerializers)

orm_klass[W]
adapter[R]
expose_associations[R]
expose_attributes[R]
scope[R]

Public Class Methods

_column_attribute_names() click to toggle source
# File lib/datasource/base.rb, line 46
def _column_attribute_names
  column_attributes = _attributes.values.select { |att|
    att[:klass].nil?
  }.map { |att| att[:name] }
end
collection(&block) click to toggle source
# File lib/datasource/base.rb, line 42
def collection(&block)
  _collection_context.class_exec(&block)
end
default_adapter() click to toggle source
# File lib/datasource/base.rb, line 17
def default_adapter
  @adapter ||= begin
    Datasource::Adapters.const_get(Datasource::Adapters.constants.first)
  end
end
inherited(base) click to toggle source
# File lib/datasource/base.rb, line 9
def inherited(base)
  base._attributes = (_attributes || {}).dup
  base._associations = (_associations || {}).dup
  base._loaders = (_loaders || {}).dup
  base._loader_order = (_loader_order || []).dup
  base._collection_context = Class.new(_collection_context || CollectionContext)
end
new(scope, adapter = nil) click to toggle source
# File lib/datasource/base.rb, line 86
def initialize(scope, adapter = nil)
  @adapter = adapter || self.class.default_adapter
  @scope =
    if self.class._update_scope
      self.class._update_scope.call(scope)
    else
      scope
    end
  @expose_attributes = []
  @expose_associations = {}
  @select_all_columns = false
  @params = {}
end
orm_klass() click to toggle source
# File lib/datasource/base.rb, line 23
def orm_klass
  fail Datasource::Error, "Model class not set for #{name}. You should define it:\nclass YourDatasource\n  @orm_klass = MyModelClass\nend"
end
primary_key() click to toggle source
# File lib/datasource/base.rb, line 27
def primary_key
  :id
end
reflection_select(reflection, parent_select, assoc_select) click to toggle source
# File lib/datasource/base.rb, line 31
def reflection_select(reflection, parent_select, assoc_select)
  # append foreign key depending on assoication
  if reflection[:macro] == :belongs_to
    parent_select.push(reflection[:foreign_key])
  elsif [:has_many, :has_one].include?(reflection[:macro])
    assoc_select.push(reflection[:foreign_key])
  else
    fail Datasource::Error, "unsupported association type #{reflection[:macro]} - TODO"
  end
end

Private Class Methods

association(name) click to toggle source
# File lib/datasource/base.rb, line 61
def association(name)
  @_associations[name.to_s] = true
end
associations(*assocs) click to toggle source
# File lib/datasource/base.rb, line 57
def associations(*assocs)
  assocs.each { |name| association(name) }
end
attribute(name, klass = nil) click to toggle source
# File lib/datasource/base.rb, line 65
def attribute(name, klass = nil)
  att = { name: name.to_s, klass: klass }
  @_attributes[att[:name]] = att
end
attributes(*attrs) click to toggle source
# File lib/datasource/base.rb, line 53
def attributes(*attrs)
  attrs.each { |name| attribute(name) }
end
computed(name, *_deps) click to toggle source
# File lib/datasource/attributes/computed_attribute.rb, line 29
def self.computed(name, *_deps)
  deps = _deps.select { |dep| dep.kind_of?(Hash) }
  _deps.reject! { |dep| dep.kind_of?(Hash) }
  unless _deps.empty?
    self_key = default_adapter.get_table_name(orm_klass)
    deps.push(self_key => _deps)
  end

  klass = Class.new(Attributes::ComputedAttribute) do
    depends *deps
  end

  attribute name, klass
end
group_by_column(column, rows, remove_column = false) click to toggle source
# File lib/datasource/base.rb, line 75
def group_by_column(column, rows, remove_column = false)
  rows.inject({}) do |map, row|
    map[row[column]] = row
    row.delete(column) if remove_column
    map
  end
end
loaded(name, _options = {}, &block) click to toggle source
Calls superclass method
# File lib/datasource/attributes/loaded.rb, line 70
def self.loaded(name, _options = {}, &block)
  name = name.to_sym
  datasource_class = self
  loader_class = Class.new(Attributes::Loaded) do
    options(_options.reverse_merge(source: :"load_#{name}"))
  end
  @_loaders[name] = loader_class
  @_loader_order << name

  method_module = Module.new do
    define_method name do |*args, &block|
      if _datasource_loaded
        if _datasource_loaded.key?(name)
          _datasource_loaded[name]
        else
          fail Datasource::Error, "loader #{name} called but was not selected"
        end
      elsif defined?(super)
        super(*args, &block)
      else
        method_missing(name, *args, &block)
      end
    end
  end

  orm_klass.class_eval do
    prepend method_module
  end
  computed name, loader: name
end
query(name, deps = nil, value = nil, &block) click to toggle source
# File lib/datasource/attributes/query_attribute.rb, line 20
def self.query(name, deps = nil, value = nil, &block)
  klass = Class.new(Attributes::QueryAttribute) do
    depends deps if deps

    if block
      define_singleton_method(:select_value, &block)
    else
      define_singleton_method(:select_value) { value }
    end
  end
  attribute name, klass
end
update_scope(&block) click to toggle source
# File lib/datasource/base.rb, line 70
def update_scope(&block)
  # TODO: careful about scope module extension, to_a infinite recursion
  @_update_scope = block
end

Public Instance Methods

attribute_exposed?(name) click to toggle source
# File lib/datasource/base.rb, line 230
def attribute_exposed?(name)
  @expose_attributes.include?(name.to_s)
end
can_upgrade?(records) click to toggle source

assume records have all attributes selected (default ORM record)

# File lib/datasource/base.rb, line 235
def can_upgrade?(records)
  query_attributes = @expose_attributes.select do |name|
    klass = self.class._attributes[name][:klass]
    if klass
      klass.ancestors.include?(Attributes::QueryAttribute)
    end
  end

  return true if query_attributes.empty?
  Array(records).all? do |record|
    query_attributes.all? do |name|
      adapter.has_attribute?(record, name)
    end
  end
end
fail_missing_attributes(names) click to toggle source
# File lib/datasource/base.rb, line 164
def fail_missing_attributes(names)
  message = if names.size > 1
    "attributes or associations #{names.join(', ')} don't exist "
  else
    "attribute or association #{names.first} doesn't exist "
  end
  message += "for #{self.class.orm_klass.name}, "
  message += "did you forget to call \"computed :#{names.first}, <dependencies>\" in your datasource_module?"
  fail Datasource::Error, message
end
get_collection_context(rows) click to toggle source
# File lib/datasource/base.rb, line 255
def get_collection_context(rows)
  self.class._collection_context.new(@scope, rows, self, @params)
end
get_exposed_loaders() click to toggle source
# File lib/datasource/base.rb, line 259
def get_exposed_loaders
  @expose_attributes
  .map { |name|
    self.class._attributes[name]
  }.select { |att|
    att[:klass] && att[:klass].ancestors.include?(Attributes::ComputedAttribute)
  }.flat_map { |att|
    att[:klass]._loader_depends
  }.uniq
  .sort_by { |loader_name|
    self.class._loader_order.index(loader_name)
  }
end
get_select_values() click to toggle source
# File lib/datasource/base.rb, line 199
def get_select_values
  scope_table = adapter.primary_scope_table(self)
  select_values = Set.new

  if @select_all_columns
    select_values.add("#{scope_table}.*")

    self.class._attributes.values.each do |att|
      if att[:klass] && attribute_exposed?(att[:name])
        if att[:klass].ancestors.include?(Attributes::QueryAttribute)
          select_values.add("(#{att[:klass].select_value}) as #{att[:name]}")
        end
      end
    end
  else
    select_values.add("#{scope_table}.#{self.class.primary_key}")

    self.class._attributes.values.each do |att|
      if attribute_exposed?(att[:name])
        if att[:klass] == nil
          select_values.add("#{scope_table}.#{att[:name]}")
        elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
          select_values.add("(#{att[:klass].select_value}) as #{att[:name]}")
        end
      end
    end
  end

  select_values.to_a
end
params(*args) click to toggle source
# File lib/datasource/base.rb, line 100
def params(*args)
  args.each do |arg|
    if arg.kind_of?(Hash)
      @params.deep_merge!(arg.symbolize_keys)
    elsif arg.is_a?(Symbol)
      @params.merge!(arg => true)
    else
      fail Datasource::Error, "unknown parameter type #{arg.class}"
    end
  end

  @params
end
results(rows = nil) click to toggle source
# File lib/datasource/base.rb, line 320
def results(rows = nil)
  rows ||= adapter.get_rows(self)

  collection_context = run_loaders(rows)

  rows.each do |row|
    row._datasource_instance = self
    set_row_loaded_values(collection_context, row) if collection_context
  end

  rows
end
run_loaders(rows) click to toggle source
# File lib/datasource/base.rb, line 273
def run_loaders(rows)
  return if rows.empty?

  # check if loaders have already been ran
  if rows.first._datasource_loaded
    # not frequent, so we can afford to check all rows
    check_list = get_exposed_loaders
    run_list = []
    rows.each do |row|
      if row._datasource_loaded
        check_list.delete_if do |name|
          if !row._datasource_loaded.key?(name)
            run_list << name
            true
          end
        end
        break if check_list.empty?
      else
        run_list.concat(check_list)
        break
      end
    end
  else
    # most frequent case - loaders haven't been ran
    run_list = get_exposed_loaders
  end

  get_collection_context(rows).tap do |collection_context|
    run_list.each do |loader_name|
      loader =
        self.class._loaders[loader_name] or
        fail Datasource::Error, "loader with name :#{loader_name} could not be found"
      Datasource.logger.info { "Running loader #{loader_name} for #{rows.first.try!(:class)}" }
      collection_context.loaded_values[loader_name] = loader.load(collection_context)
    end
  end
end
select(*names) click to toggle source
# File lib/datasource/base.rb, line 130
def select(*names)
  newly_exposed_attributes = []
  missing_attributes = []
  names.each do |name|
    if name.kind_of?(Hash)
      name.each_pair do |assoc_name, assoc_select|
        assoc_name = assoc_name.to_s
        if self.class._associations.key?(assoc_name)
          @expose_associations[assoc_name] ||= []
          @expose_associations[assoc_name].concat(Array(assoc_select))
          @expose_associations[assoc_name].uniq!
        else
          missing_attributes << assoc_name
        end
      end
    else
      name = name.to_s
      if name == "*"
        select_all_columns
      elsif self.class._attributes.key?(name)
        unless @expose_attributes.include?(name)
          @expose_attributes.push(name)
          newly_exposed_attributes.push(name)
        end
      else
        missing_attributes << name
      end
    end
  end
  update_dependencies(newly_exposed_attributes) unless newly_exposed_attributes.empty?
  fail_missing_attributes(missing_attributes) if Datasource.config.raise_error_on_unknown_attribute_select && !missing_attributes.empty?
  self
end
select_all() click to toggle source
# File lib/datasource/base.rb, line 122
def select_all
  attributes = self.class._attributes.keys
  select(*attributes)
  @select_all_columns = true

  attributes
end
select_all_columns() click to toggle source
# File lib/datasource/base.rb, line 114
def select_all_columns
  columns = self.class._column_attribute_names
  select(*columns)
  @select_all_columns = true

  columns
end
set_row_loaded_values(collection_context, row) click to toggle source
# File lib/datasource/base.rb, line 311
def set_row_loaded_values(collection_context, row)
  row._datasource_loaded ||= {}

  primary_key = row.send(self.class.primary_key)
  collection_context.loaded_values.each_pair do |name, values|
    row._datasource_loaded[name] = values[primary_key]
  end
end
update_dependencies(names) click to toggle source
# File lib/datasource/base.rb, line 175
def update_dependencies(names)
  scope_table = adapter.primary_scope_table(self)

  self.class._attributes.values.each do |att|
    next unless names.include?(att[:name])
    next unless att[:klass]

    if att[:klass].ancestors.include?(Attributes::ComputedAttribute)
      att[:klass]._depends.each_pair do |key, value|
        if key.to_s == scope_table
          select(*value)
        else
          select(key => value)
        end
      end
    elsif att[:klass].ancestors.include?(Attributes::QueryAttribute)
      att[:klass]._depends.each do |name|
        next if name == scope_table
        adapter.ensure_table_join!(self, name, att)
      end
    end
  end
end
upgrade_records(records) click to toggle source
# File lib/datasource/base.rb, line 251
def upgrade_records(records)
  adapter.upgrade_records(self, Array(records))
end