class Services::Query

Constants

COMMA_REGEX
CREATED_BEFORE_AFTER_REGEX
TABLE_NAME_REGEX

Public Class Methods

convert_condition_objects_to_ids(*class_names) click to toggle source
# File lib/services/query.rb, line 12
def convert_condition_objects_to_ids(*class_names)
  @object_to_id_class_names = class_names
end
object_to_id_class_names() click to toggle source
# File lib/services/query.rb, line 16
def object_to_id_class_names
  @object_to_id_class_names || []
end

Public Instance Methods

call(ids_or_conditions = {}, _conditions = {}) click to toggle source
# File lib/services/query.rb, line 21
def call(ids_or_conditions = {}, _conditions = {})
  ids, conditions = if ids_or_conditions.is_a?(Hash)
    if _conditions.any?
      fail ArgumentError, 'If conditions are passed as first argument, there must not be a second argument.'
    end
    [[], ids_or_conditions.symbolize_keys]
  else
    if ids_or_conditions.nil?
      fail ArgumentError, 'IDs must not be nil.'
    end
    [Array(ids_or_conditions), _conditions.symbolize_keys]
  end

  object_table_id = "#{object_class.table_name}.id"

  unless conditions.key?(:order)
    conditions[:order] = object_table_id
  end

  scope = conditions.delete(:scope).try(:dup) || object_class.all
  if ids.any?
    scope = scope.where(object_table_id => ids)
  end

  if conditions.any?
    self.class.object_to_id_class_names.each do |class_name|
      if object_or_objects = conditions.delete(class_name)
        ids = case object_or_objects
        when Array
          object_or_objects.map(&:id)
        when ActiveRecord::Relation
          object_or_objects.select(:id)
        else
          [object_or_objects.id]
        end
        conditions[:"#{class_name}_id"] = ids.size == 1 ? ids.first : ids
      end
    end

    conditions.each do |k, v|
      if new_scope = process(scope, k, v)
        conditions.delete k
        scope = new_scope
      end
    end

    # If a JOIN is involved, use a subquery to make sure we get DISTINCT records.
    if scope.to_sql =~ / join /i
      scope = object_class.where(id: scope.select("DISTINCT #{object_table_id}"))
    end
  end

  conditions.each do |k, v|
    allowed_class_methods = Services.configuration.allowed_class_methods_in_queries[object_class.to_s]
    if allowed_class_methods&.key?(k)
      arity = allowed_class_methods[k] || object_class.method(k).arity
      case arity
      when 0
        unless v == true
          raise ArgumentError, "Method #{k} of class #{self} takes no arguments, so `true` must be passed as the value for this param, not #{v} (#{v.class})."
        end
        scope = scope.public_send(k)
      when 1, -1
        scope = scope.public_send(k, v)
      else
        unless v.is_a?(Array)
          raise ArgumentError, "Method #{k} of class #{self} takes more than one argument, so an array must be passed as the value for this param, not #{v} (#{v.class})."
        end
        scope = scope.public_send(k, *v)
      end
    else
      case k
      when :id_not
        scope = scope.where.not(id: v)
      when CREATED_BEFORE_AFTER_REGEX
        operator = $1 == 'before' ? '<' : '>'
        scope = scope.where("#{object_class.table_name}.created_at #{operator} ?", v)
      when :order
        next unless v

        order = v.split(COMMA_REGEX).map do |order_part|
          table_name = order_part[TABLE_NAME_REGEX, 1]
          case
          when table_name && table_name != object_class.table_name
            unless reflection = object_class.reflections.values.detect { |reflection| reflection.table_name == table_name }
              fail "Reflection on class #{object_class} with table name #{table_name} not found."
            end

            if ActiveRecord::VERSION::MAJOR >= 5
              scope = scope.left_outer_joins(reflection.name)
            else
              join_conditions = "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{reflection.foreign_key} = #{object_class.table_name}.id"
              if reflection.type
                join_conditions << " AND #{table_name}.#{reflection.type} = '#{object_class}'"
              end
              scope = scope.joins(join_conditions)
            end
          when !table_name
            order_part.prepend "#{object_class.table_name}."
          end
          order_part
        end.join(', ')

        scope = scope.order(order)
      when :limit
        scope = scope.limit(v)
      when :page
        scope = scope.page(v)
      when :per_page
        scope = scope.per(v)
      else
        raise ArgumentError, "Unexpected condition: #{k}"
      end
    end
  end

  scope
end