module Services class Query include ObjectClass class << self delegate :call, to: :new def convert_condition_objects_to_ids(*class_names) @object_to_id_class_names = class_names end def object_to_id_class_names @object_to_id_class_names || [] end end 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.public_send(ActiveRecord::VERSION::MAJOR == 3 ? :scoped : :all) scope = scope.where(object_table_id => ids) unless ids.empty? unless conditions.empty? 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.pluck(: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| case k when :id_not scope = scope.where.not(id: v) when /\Acreated_(before|after)\z/ operator = $1 == 'before' ? '<' : '>' scope = scope.where("#{object_class.table_name}.created_at #{operator} ?", v) when :order next unless v case v when 'random' order = 'RANDOM()' when /\A([A-Za-z0-9_]+)\./ table_name = $1 unless 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 # TODO: In Rails 5, we can use #left_outer_joins # http://blog.bigbinary.com/2016/03/24/support-for-left-outer-joins-in-rails-5.html 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 order = v else order = "#{object_class.table_name}.#{v}" end 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 scope end end end