module Services class Query include ObjectClass COMMA_REGEX = /\s*,\s*/ TABLE_NAME_REGEX = /\A([A-Za-z0-9_]+)\./ CREATED_BEFORE_AFTER_REGEX = /\Acreated_(before|after)\z/ 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.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 end end