class ModelApi::ApiContext

Public Class Methods

new(context_parent) click to toggle source
# File lib/model-api/api_context.rb, line 3
def initialize(context_parent)
  @context_parent = context_parent
end

Public Instance Methods

api_query(klass, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 20
def api_query(klass, opts = {})
  opts = prepare_options(opts)
  model_metadata = opts[:model_metadata] || ModelApi::Utils.model_metadata(klass)
  unless klass < ActiveRecord::Base
    fail 'Expected model class to be an ActiveRecord::Base subclass'
  end
  query = ModelApi::Utils.invoke_callback(model_metadata[:base_query], opts) || klass.all
  if (deleted_col = klass.columns_hash['deleted']).present?
    case deleted_col.type
    when :boolean
      query = query.where(deleted: false)
    when :integer, :decimal
      query = query.where(deleted: 0)
    end
  end
  apply_context(query, opts)
end
apply_filter_param(attr_metadata, collection, opts = {}) click to toggle source

rubocop:enable Metrics/ParameterLists

# File lib/model-api/api_context.rb, line 186
def apply_filter_param(attr_metadata, collection, opts = {})
  raw_value = (opts[:attr_values] || params)[attr_metadata[:key]]
  filter_table = opts[:filter_table]
  klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
  if raw_value.is_a?(Hash) && raw_value.include?('0')
    operator_value_pairs = filter_process_param_array(params_array(raw_value), attr_metadata,
        opts)
  else
    operator_value_pairs = filter_process_param(raw_value, attr_metadata, opts)
  end
  if (column = resolve_key_to_column(klass, attr_metadata)).present?
    operator_value_pairs.each do |operator, value|
      if operator == '=' && filter_table.blank?
        collection = collection.where(column => value)
      else
        table_name = (filter_table || klass.table_name).to_s.delete('`')
        column = column.to_s.delete('`')
        if value.is_a?(Array)
          operator = 'IN'
          value = value.map { |_v| format_value_for_query(column, value, klass) }
          value = "(#{value.map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')})"
        else
          value = "'#{value.gsub("'", "''")}'"
        end
        collection = collection.where("`#{table_name}`.`#{column}` #{operator} #{value}")
      end
    end
  elsif (key = attr_metadata[:key]).present?
    opts[:result_filters][key] = operator_value_pairs if opts.include?(:result_filters)
  end
  collection
end
common_object_query(id_attribute, id_value, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 38
def common_object_query(id_attribute, id_value, opts = {})
  klass = opts[:model_class] || model_class
  coll_query = apply_context(api_query(klass, opts), opts)
  query = coll_query.where(id_attribute => id_value)
  if !opts[:admin_user]
    unless opts.include?(:user_filter) && !opts[:user_filter] && opts[:user]
      query = user_query(query, opts[:user], opts.merge(model_class: klass))
    end
  elsif id_attribute != :id && !id_attribute.to_s.ends_with?('.id') &&
      klass.column_names.include?('id') && !query.exists?
    # Admins can optionally use record ID's if the ID field happens to be something else
    query = coll_query.where(id: id_value)
  end
  unless (not_found_error = opts[:not_found_error]).blank? || query.exists?
    not_found_error = not_found_error.call(params[:id]) if not_found_error.respond_to?(:call)
    if not_found_error == true
      not_found_error = "#{klass.model_name.human} '#{id_value}' not found."
    end
    fail ModelApi::NotFoundException.new(opts[:id_param] || id_attribute, not_found_error.to_s)
  end
  query
end
filter_collection(collection, filter_params, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 122
def filter_collection(collection, filter_params, opts = {})
  return [collection, {}] if filter_params.blank? # Don't filter if no filter params
  klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
  assoc_values, metadata, attr_values = process_filter_params(filter_params, klass, opts)
  result_filters = {}
  metadata.values.each do |attr_metadata|
    collection = apply_filter_param(attr_metadata, collection,
        opts.merge(attr_values: attr_values, result_filters: result_filters, class_name: klass))
  end
  assoc_values.each do |assoc, assoc_filter_params|
    ar_assoc = klass.reflect_on_association(assoc)
    next unless ar_assoc.present?
    collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
    collection, assoc_result_filters = filter_collection(collection, assoc_filter_params,
        opts.merge(class: ar_assoc.klass, filter_table: ar_assoc.table_name))
    result_filters[assoc] = assoc_result_filters if assoc_result_filters.present?
  end
  [collection, result_filters]
end
get_updated_object(obj_or_class, operation, request_body, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 94
def get_updated_object(obj_or_class, operation, request_body, opts = {})
  opts = prepare_options(opts.symbolize_keys)
  opts[:operation] = operation
  if obj_or_class.is_a?(Class)
    klass = class_or_sti_subclass(obj_or_class, request_body, operation, opts)
    obj = nil
  elsif obj_or_class.is_a?(ActiveRecord::Base)
    obj = obj_or_class
    klass = obj.class
  elsif obj_or_class.is_a?(ActiveRecord::Relation)
    klass = obj_or_class.klass
    obj = obj_or_class.first
  end
  opts[:api_attr_metadata] = ModelApi::Utils.filtered_attrs(klass, operation, opts)
  opts[:api_model_metadata] = model_metadata = ModelApi::Utils.model_metadata(klass)
  opts[:ignored_fields] = []
  return [nil, opts.merge(bad_payload: true)] if request_body.nil?
  obj = klass.new if obj.nil?
  verify_update_request_body(request_body, opts[:format], opts)
  root_elem = opts[:root] = ModelApi::Utils.model_name(klass).singular
  request_obj = opts[:request_obj] = object_from_req_body(root_elem, request_body,
      opts[:format])
  opts[:request_hash] = ModelApi::Utils.internal_value(request_obj).deep_symbolize_keys
  ModelApi::Utils.apply_updates(obj, request_obj, operation, opts)
  ModelApi::Utils.invoke_callback(model_metadata[:after_initialize], obj, opts)
  [obj, opts]
end
model_class() click to toggle source
# File lib/model-api/api_context.rb, line 7
def model_class
  return @model_class if instance_variable_defined?(:@model_class)
  @model_class = @context_parent.send(:model_class)
end
prepare_options(opts) click to toggle source
# File lib/model-api/api_context.rb, line 12
def prepare_options(opts)
  return opts if opts[:options_initialized]
  if @context_parent.respond_to?(:prepare_options, true)
    return @context_parent.send(:prepare_options, opts)
  end
  opts
end
process_filter_assoc_param(attr, metadata, assoc_values, value, opts) click to toggle source

rubocop:disable Metrics/ParameterLists

# File lib/model-api/api_context.rb, line 163
def process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
  attr_elems = attr.split('.')
  assoc_name = attr_elems[0].strip.to_sym
  assoc_metadata = metadata[assoc_name] ||
      metadata[ModelApi::Utils.ext_query_attr(assoc_name, opts)] || {}
  key = assoc_metadata[:key]
  return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:filter], opts)
  assoc_filter_params = (assoc_values[key] ||= {})
  assoc_filter_params[attr_elems[1..-1].join('.')] = value
end
process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts) click to toggle source
# File lib/model-api/api_context.rb, line 174
def process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
  attr = attr.strip.to_sym
  attr_metadata = metadata[attr] ||
      metadata[ModelApi::Utils.ext_query_attr(attr, opts)] || {}
  key = attr_metadata[:key]
  return unless key.present? && ModelApi::Utils.eval_bool(attr_metadata[:filter], opts)
  filter_metadata[key] = attr_metadata
  attr_values[key] = value
end
process_filter_params(filter_params, klass, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 142
def process_filter_params(filter_params, klass, opts = {})
  assoc_values = {}
  filter_metadata = {}
  attr_values = {}
  metadata = ModelApi::Utils.filtered_ext_attrs(klass, :filter, opts)
  filter_params.each do |attr, value|
    attr = attr.to_s
    if attr.length > 1 && ['>', '<', '!', '='].include?(attr[-1])
      value = "#{attr[-1]}=#{value}" # Effectively allows >= / <= / != / == in query string
      attr = attr[0..-2].strip
    end
    if attr.include?('.')
      process_filter_assoc_param(attr, metadata, assoc_values, value, opts)
    else
      process_filter_attr_param(attr, metadata, filter_metadata, attr_values, value, opts)
    end
  end
  [assoc_values, filter_metadata, attr_values]
end
sort_collection(collection, sort_params, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 219
def sort_collection(collection, sort_params, opts = {})
  return [collection, {}] if sort_params.blank? # Don't filter if no filter params
  klass = opts[:class] || ModelApi::Utils.find_class(collection, opts)
  assoc_sorts, attr_sorts, result_sorts = process_sort_params(sort_params, klass,
      opts.merge(result_sorts: result_sorts))
  sort_table = opts[:sort_table]
  sort_table = sort_table.to_s.delete('`') if sort_table.present?
  attr_sorts.each do |key, sort_order|
    if sort_table.present?
      collection = collection.order("`#{sort_table}`.`#{key.to_s.delete('`')}` " \
            "#{sort_order.to_s.upcase}")
    else
      collection = collection.order(key => sort_order)
    end
  end
  assoc_sorts.each do |assoc, assoc_sort_params|
    ar_assoc = klass.reflect_on_association(assoc)
    next unless ar_assoc.present?
    collection = collection.joins(assoc) unless collection.joins_values.include?(assoc)
    collection, assoc_result_sorts = sort_collection(collection, assoc_sort_params,
        opts.merge(class: ar_assoc.klass, sort_table: ar_assoc.table_name))
    result_sorts[assoc] = assoc_result_sorts if assoc_result_sorts.present?
  end
  [collection, result_sorts]
end
user_query(query, user, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 61
def user_query(query, user, opts = {})
  klass = opts[:model_class] || query.klass
  user_id_col = opts[:user_id_column] || :user_id
  user_assoc = opts[:user_association] || :user
  user_id = user.try(opts[:user_id_attribute] || :id)
  if klass.columns_hash.include?(user_id_col.to_s)
    query = query.where(user_id_col => user_id)
  elsif (assoc = klass.reflect_on_association(user_assoc)).present? &&
      [:belongs_to, :has_one].include?(assoc.macro)
    query = query.joins(user_assoc).where(
        "#{assoc.klass.table_name}.#{assoc.klass.primary_key}" => user_id)
  elsif opts[:user_filter]
    fail "Unable to filter results by user; no '#{user_id_col}' column or " \
            "'#{user_assoc}' association found!"
  end
  query
end
validate_read_operation(obj, operation, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 79
def validate_read_operation(obj, operation, opts = {})
  opts = prepare_options(opts)
  status, errors = ModelApi::Utils.validate_operation(obj, operation,
      opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata]))
  return true if status.nil? && errors.nil?
  if errors.nil? && (status.is_a?(Array) || status.present?)
    return true if (errors = status).blank?
    status = :bad_request
  end
  return true unless errors.present?
  errors = [errors] unless errors.is_a?(Array)
  simple_error(status, errors, opts)
  false
end

Private Instance Methods

apply_context(query, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 247
def apply_context(query, opts = {})
  context = opts[:context]
  return query if context.nil?
  if context.respond_to?(:call)
    query = context.send(*([:call, query, opts][0..context.parameters.size]))
  elsif context.is_a?(Hash)
    context.each { |attr, value| query = query.where(attr => value) }
  end
  query
end
class_or_sti_subclass(klass, req_body, operation, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 388
def class_or_sti_subclass(klass, req_body, operation, opts = {})
  metadata = ModelApi::Utils.filtered_attrs(klass, :create, opts)
  if operation == :create && (attr_metadata = metadata[:type]).is_a?(Hash) &&
      req_body.is_a?(Hash)
    external_attr = ModelApi::Utils.ext_attr(:type, attr_metadata)
    type = req_body[external_attr.to_s]
    begin
      type = ModelApi::Utils.transform_value(type, attr_metadata[:parse], opts.dup)
    rescue Exception => e
      Rails.logger.warn 'Error encountered parsing API input for attribute ' \
              "\"#{external_attr}\" (\"#{e.message}\"): \"#{type.to_s.first(1000)}\" ... " \
              'using raw value instead.'
    end
    if type.present? && (type = type.camelize) != klass.name
      Rails.application.eager_load!
      klass.subclasses.each do |subclass|
        return subclass if subclass.name == type
      end
    end
  end
  klass
end
filter_process_param(raw_value, attr_metadata, opts) click to toggle source

rubocop:enable Metrics/ParameterLists

# File lib/model-api/api_context.rb, line 300
def filter_process_param(raw_value, attr_metadata, opts)
  raw_value = raw_value.to_s.strip
  array = nil
  if raw_value.starts_with?('[') && raw_value.ends_with?(']')
    array = JSON.parse(raw_value) rescue nil
    array = array.is_a?(Array) ? array.map(&:to_s) : nil
  end
  if array.nil?
    if attr_metadata.include?(:filter_delimiter)
      delimiter = attr_metadata[:filter_delimiter]
    else
      delimiter = ','
    end
    array = raw_value.split(delimiter) if raw_value.include?(delimiter)
  end
  return filter_process_param_array(array, attr_metadata, opts) unless array.nil?
  operator, value = parse_filter_operator(raw_value)
  [[operator, ModelApi::Utils.transform_value(value, attr_metadata[:parse], opts)]]
end
filter_process_param_array(array, attr_metadata, opts) click to toggle source
# File lib/model-api/api_context.rb, line 320
def filter_process_param_array(array, attr_metadata, opts)
  operator_value_pairs = []
  equals_values = []
  array.map(&:strip).reject(&:blank?).each do |value|
    operator, value = parse_filter_operator(value)
    value = ModelApi::Utils.transform_value(value.to_s, attr_metadata[:parse], opts)
    if operator == '='
      equals_values << value
    else
      operator_value_pairs << [operator, value]
    end
  end
  operator_value_pairs << ['=', equals_values.uniq] if equals_values.present?
  operator_value_pairs
end
format_value_for_query(column, value, klass) click to toggle source
# File lib/model-api/api_context.rb, line 346
def format_value_for_query(column, value, klass)
  return value.map { |v| format_value_for_query(column, v, klass) } if value.is_a?(Array)
  column_metadata = klass.columns_hash[column.to_s]
  case column_metadata.try(:type)
  when :date, :datetime, :time, :timestamp
    user = current_user
    if user.respond_to?(:time_zone) && (user_time_zone = user.time_zone).present?
      time_zone = ActiveSupport::TimeZone.new(user_time_zone)
    end
    time_zone ||= ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
    return time_zone.parse(value.to_s).try(:to_s, :db)
  when :float, :decimal
    return value.to_d.to_s
  when :integer, :primary_key
    return value.to_d.to_s.sub(/\.0\Z/, '')
  when :boolean
    return value ? 'true' : 'false'
  end
  value.to_s
end
object_from_req_body(root_elem, req_body, format) click to toggle source
# File lib/model-api/api_context.rb, line 411
def object_from_req_body(root_elem, req_body, format)
  if format == :json
    request_obj = req_body
  else
    request_obj = req_body[root_elem]
    if request_obj.blank?
      request_obj = req_body['obj']
      if request_obj.blank? && req_body.size == 1
        request_obj = req_body.values.first
      end
    end
  end
  fail 'Invalid request format' unless request_obj.present?
  request_obj
end
params_array(raw_value) click to toggle source
# File lib/model-api/api_context.rb, line 367
def params_array(raw_value)
  index = 0
  array = []
  while raw_value.include?(index.to_s)
    array << raw_value[index.to_s]
    index += 1
  end
  array
end
parse_filter_operator(value) click to toggle source
# File lib/model-api/api_context.rb, line 336
def parse_filter_operator(value)
  value = value.to_s.strip
  if (operator = value.scan(/\A(>=|<=|!=|<>)[[:space:]]*\w/).flatten.first).present?
    return (operator == '<>' ? '!=' : operator), value[2..-1].strip
  elsif (operator = value.scan(/\A(>|<|=)[[:space:]]*\w/).flatten.first).present?
    return operator, value[1..-1].strip
  end
  ['=', value]
end
process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts) click to toggle source

Intentionally disabling parameter list length check for private / internal method rubocop:disable Metrics/ParameterLists

# File lib/model-api/api_context.rb, line 288
def process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
  attr_elems = attr.split('.')
  assoc_name = attr_elems[0].strip.to_sym
  assoc_metadata = metadata[assoc_name] || {}
  key = assoc_metadata[:key]
  return unless key.present? && ModelApi::Utils.eval_bool(assoc_metadata[:sort], opts)
  assoc_sort_params = (assoc_sorts[key] ||= {})
  assoc_sort_params[attr_elems[1..-1].join('.')] = sort_order
end
process_sort_params(sort_params, klass, opts) click to toggle source
# File lib/model-api/api_context.rb, line 258
def process_sort_params(sort_params, klass, opts)
  metadata = ModelApi::Utils.filtered_ext_attrs(klass, :sort, opts)
  assoc_sorts = {}
  attr_sorts = {}
  result_sorts = {}
  sort_params.each do |attr, sort_order|
    if attr.include?('.')
      process_sort_param_assoc(attr, metadata, sort_order, assoc_sorts, opts)
    else
      attr = attr.strip.to_sym
      attr_metadata = metadata[attr] || {}
      next unless ModelApi::Utils.eval_bool(attr_metadata[:sort], opts)
      sort_order = sort_order.to_sym
      sort_order = :default unless [:asc, :desc].include?(sort_order)
      if sort_order == :default
        sort_order = (attr_metadata[:default_sort_order] || :asc).to_sym
        sort_order = :asc unless [:asc, :desc].include?(sort_order)
      end
      if (column = resolve_key_to_column(klass, attr_metadata)).present?
        attr_sorts[column] = sort_order
      elsif (key = attr_metadata[:key]).present?
        result_sorts[key] = sort_order
      end
    end
  end
  [assoc_sorts, attr_sorts, result_sorts]
end
resolve_key_to_column(klass, attr_metadata) click to toggle source
# File lib/model-api/api_context.rb, line 377
def resolve_key_to_column(klass, attr_metadata)
  return nil unless klass.respond_to?(:columns_hash)
  columns_hash = klass.columns_hash
  key = attr_metadata[:key]
  return key if columns_hash.include?(key.to_s)
  render_method = attr_metadata[:render_method]
  render_method = render_method.to_s if render_method.is_a?(Symbol)
  return nil unless render_method.is_a?(String)
  columns_hash.include?(render_method) ? render_method : nil
end
verify_update_request_body(request_body, format, opts = {}) click to toggle source
# File lib/model-api/api_context.rb, line 427
def verify_update_request_body(request_body, format, opts = {})
  if request_body.is_a?(Array)
    fail 'Expected object, but collection provided'
  elsif !request_body.is_a?(Hash)
    fail 'Expected object'
  end
end