oddcamp / active_hash_relation

ActiveHash Relation: Simple gem that allows you to run multiple ActiveRecord::Relation using hash. Perfect for APIs.
https://www.kollegorna.se/
MIT License
118 stars 15 forks source link

Add OR condition #10

Closed RafaelMCarvalho closed 7 years ago

RafaelMCarvalho commented 8 years ago

Would be nice to have something like:

apply_filters(resource, { or: { title: "Ruby", content: "Rails" })

What do you think?

vasilakisfil commented 8 years ago

Yes it's on my list for version 2.0 which will also be JSON API compliant. I am not sure how the API for #or will look like (maybe like you suggest) but it will exploit rails 5 AR #OR method. Probably it will land around December unless you want to send a pull request :)

KRaymundus commented 7 years ago

Great Lib! Thnx!

I've been trying to implement or-filters, here is my result so far: In the column filters I added the or_filters function (down below), and at each null_filters I added a call to or_filters:

module ActiveHashRelation::ColumnFilters
  def filter_primary(resource, column, param)
    resource = resource.where(id: param)
  end

  def filter_integer(resource, column, table_name, param)
    if param.is_a? Array
      n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
      return resource.where("#{table_name}.#{column} IN (#{n_param})")
    elsif param.is_a? Hash
      if !param[:null].nil?
        return null_filters(resource, table_name, column, param)
      elsif param[:or]
        return or_filters(resource, table_name, column, param, __method__)
      else
        return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
      end
    else
      return resource.where("#{table_name}.#{column} = ?", param)
    end
  end

  def filter_float(resource, column, table_name, param)
    filter_integer(resource, column, table_name, param)
  end

  def filter_decimal(resource, column, table_name, param)
    filter_integer(resource, column, table_name, param)
  end

  def filter_string(resource, column, table_name, param)
    if param.is_a? Array
      n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
      return resource.where("#{table_name}.#{column} IN (#{n_param})")
    elsif param.is_a? Hash
      if !param[:null].nil?
        return null_filters(resource, table_name, column, param)
      elsif param[:or]
        return or_filters(resource, table_name, column, param, __method__)
      else
        return apply_like_filters(resource, table_name, column, param)
      end
    else
      return resource.where("#{table_name}.#{column} = ?", param)
    end
  end

  def filter_text(resource, column, param)
    return filter_string(resource, column, param)
  end

  def filter_date(resource, column, table_name, param)
    if param.is_a? Array
      n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
      return resource.where("#{table_name}.#{column} IN (#{n_param})")
    elsif param.is_a? Hash
      if !param[:null].nil?
        return null_filters(resource, table_name, column, param)
      elsif param[:or]
        return or_filters(resource, table_name, column, param, __method__)
      else
        return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
      end
    else
      resource = resource.where(column => param)
    end

    return resource
  end

  def filter_datetime(resource, column, table_name, param)
    if param.is_a? Array
      n_param = param.to_s.gsub("\"","'").gsub("[","").gsub("]","") #fix this!
      return resource = resource.where("#{table_name}.#{column} IN (#{n_param})")
    elsif param.is_a? Hash
      if !param[:null].nil?
        return null_filters(resource, table_name, column, param)
      elsif param[:or]
        return or_filters(resource, table_name, column, param, __method__)
      else
        return apply_leq_geq_le_ge_filters(resource, table_name, column, param)
      end
    else
      resource = resource.where(column => param)
    end

    return resource
  end

  def filter_boolean(resource, column, param)
    b_param = ActiveRecord::Type::Boolean.new.type_cast_from_database(param)

    resource = resource.where(column => b_param)
  end

  private

  def apply_leq_geq_le_ge_filters(resource, table_name, column, param)
    if !param[:leq].blank?
      resource = resource.where("#{table_name}.#{column} <= ?", param[:leq])
    elsif !param[:le].blank?
      resource = resource.where("#{table_name}.#{column} < ?", param[:le])
    end

    if !param[:geq].blank?
      resource = resource.where("#{table_name}.#{column} >= ?", param[:geq])
    elsif !param[:ge].blank?
      resource = resource.where("#{table_name}.#{column} > ?", param[:ge])
    end

    return resource
  end

  def apply_like_filters(resource, table_name, column, param)
    like_method = "LIKE"
    like_method = "ILIKE" if param[:with_ilike]

    if !param[:starts_with].blank?
      resource = resource.where("#{table_name}.#{column} #{like_method} ?", "#{param[:starts_with]}%")
    end

    if !param[:ends_with].blank?
      resource = resource.where("#{table_name}.#{column} #{like_method} ?", "%#{param[:ends_with]}")
    end

    if !param[:like].blank?
      resource = resource.where("#{table_name}.#{column} #{like_method} ?", "%#{param[:like]}%")
    end

    if !param[:eq].blank?
      resource = resource.where("#{table_name}.#{column} = ?", param[:eq])
    end

    return resource
  end

  def null_filters(resource, table_name, column, param)
    if param[:null] == true
      resource = resource.where("#{table_name}.#{column} IS NULL")
    end

    if param[:null] == false
      resource = resource.where("#{table_name}.#{column} IS NOT NULL")
    end

    return resource
  end

  def or_filters(resource, table_name, column, param, filter_method)
    base_resource = resource  # Save base, so to make each or_filter work on the same base-set
    param[:or] = param[:or].map { |k, v| {k => v} } # Convert to array
    param[:or].each_with_index { |p, i|
      or_filter = send(filter_method, base_resource, column, table_name, p)
      if i == 0
        resource = or_filter  # Nothing to 'or' on for the first or filter
      else
        resource = resource.or(or_filter)
      end
    }
    return resource
  end
end

It is to be called as suggested: apply_filters(resource, { or: { title: "Ruby", content: "Rails" }) It also works with null filters.

One thing I don't understand is why I have to overwrite the param[:or] = in my function. If I assign the (to array converted param) to a local variable it doesn't work. Any ideas?

Didn't test well yet, but it might be a good start.

vasilakisfil commented 7 years ago

Thanks for the feedback @KRaymundus. As I said, I am adding tests at the moment in order to move faster without breaking anything and include features such as this one. I will probably finish adding tests end of this week and I will ping you then so we can take a look on your feedback and merge to master.

vasilakisfil commented 7 years ago

@RafaelMCarvalho @KRaymundus thank you for your input. The OR filter has implemented on master. Check the documentation ;). Closing here.