Skip to content

Commit

Permalink
Merge pull request #24 from kollegorna/feat/or_filter
Browse files Browse the repository at this point in the history
Add OR filter
  • Loading branch information
vasilakisfil authored Feb 3, 2017
2 parents acb9afe + 77ec409 commit 9a333df
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 32 deletions.
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ apply_filters(resource, {updated_at: { geq: "2014-11-2 14:25:04"}, unit: {id: 9}
```
or even filter a resource based on it's associations' associations:
```ruby
apply_filters(resource, {updated_at: { geq: "2014-11-2 14:25:04"}, unit: {id: 9, areas: {id: 22} }})
apply_filters(resource, {updated_at: { geq: "2014-11-2 14:25:04"}, unit: {id: 9, areas: {or: [{id: 22}, {id: 21}]} }})
```
and the list could go on.. Basically your whole db is exposed\* there. It's perfect for filtering a collection of resources on APIs.

Expand All @@ -30,7 +30,7 @@ which might or might not be a security issue. If you don't like that check

Add this line to your application's Gemfile:

gem 'active_hash_relation'
gem 'active_hash_relation', '~> 1.2.0

And then execute:

Expand All @@ -39,8 +39,10 @@ And then execute:
Or install it yourself as:

$ gem install active_hash_relation

## How to use
The gem exposes only one method: `apply_filters(resource, hash_params, include_associations: true, model: nil)`. `resource` is expected to be an ActiveRecord::Relation.
The gem exposes only one method: `apply_filters(resource, hash_params, include_associations: true, model: nil)`.
`resource` is expected to be an ActiveRecord::Relation.
That way, you can add your custom filters before passing the `Relation` to `ActiveHashRelation`.

In order to use it you have to include ActiveHashRelation module in your class. For instance in a Rails API controller you would do:
Expand Down Expand Up @@ -78,17 +80,21 @@ end
ActiveHashRelation.initialize!
```


## The API
### Columns
For each param, `apply_filters` method will search in the model's (derived from the first param, or explicitly defined as the last param) all the record's column names and associations. (filtering based on scopes are not working at the moment but will be supported soon). For each column, if there is such a param, it will apply the filter based on the column type. The following column types are supported:
For each param, `apply_filters` method will search in the model's (derived from the
first param, or explicitly defined as the last param) all the record's column names
and associations. (filtering based on scopes are not working at the moment but
will be supported soon). For each column, if there is such a param, it will
apply the filter based on the column type. The following column types are supported:


#### Integer, Float, Decimal, Date, Time or Datetime/Timestamp
You can apply an equality filter:

* `{example_column: 500}`

or using an array
or using an array (`ActiveRecord` translates that internally to an `IN` query)
* `{example_column: [500, 40]}`

or using a hash as a value you get more options:
Expand Down Expand Up @@ -157,6 +163,17 @@ You can apply null filter for generate query like this `"users.name IS NULL"` or

this can be used also for relations tables, so you can write like this `{ books: {title: {null: false }} }`


### OR Filter
You can apply an SQL `OR` (for ActiveRecord 5+) using the following syntax:
`{or: [{name: 'Filippos'}, {name: 'Vasilis'}]}`

It will generate: `WHERE ((users.name = 'Filippos') OR (users.name = 'Vasilis'))`

You can apply an `OR` on associations as well or even nested ones, there isn't much limitation on that.
I suggest you though to take a look on the [tests](spec/tests/or_filter_spec.rb), cause the syntax gets a bit complex after a while ;)


### Scopes
**Filtering on scopes is not enabled by default**.

Expand Down
4 changes: 0 additions & 4 deletions lib/active_hash_relation/column_filters.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
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!
Expand Down
42 changes: 22 additions & 20 deletions lib/active_hash_relation/filter_applier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,17 @@ def initialize(resource, params, include_associations: false, model: nil)
@params = HashWithIndifferentAccess.new(params)
end
@include_associations = include_associations
@model = model
@model = find_model(model)
end


def apply_filters
unless @model
@model = model_class_name(@resource)
if @model.nil? || engine_name == @model.to_s
@model = model_class_name(@resource, true)
end
end
run_or_filters

table_name = @model.table_name
@model.columns.each do |c|
next if @params[c.name.to_s].nil?
next if @params[c.name.to_s].is_a?(String) && @params[c.name.to_s].blank?

if c.respond_to?(:primary)
if c.primary
@resource = filter_primary(@resource, c.name, @params[c.name])
next
end
else #rails 4.2
if @model.primary_key == c.name
@resource = filter_primary(@resource, c.name, @params[c.name])
next
end
end

case c.type
when :integer
@resource = filter_integer(@resource, c.name, table_name, @params[c.name])
Expand Down Expand Up @@ -81,5 +64,24 @@ def apply_filters
def filter_class(resource_name)
"#{configuration.filter_class_prefix}#{resource_name.pluralize}#{configuration.filter_class_suffix}".constantize
end

def run_or_filters
if @params[:or].is_a?(Array)
if ActiveRecord::VERSION::MAJOR < 5
return Rails.logger.warn("OR query is supported on ActiveRecord 5+")
end

if @params[:or].length >= 2
array = @params[:or].map do |or_param|
self.class.new(@resource, or_param, include_associations: @include_associations).apply_filters
end

@resource = @resource.merge(array[0])
array[1..-1].each{|query| @resource = @resource.or(query)}
else
Rails.logger.warn("Can't run an OR with 1 element!")
end
end
end
end
end
11 changes: 11 additions & 0 deletions lib/active_hash_relation/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,16 @@ def model_class_name(resource, _engine = false)
def engine_name
Rails::Engine.subclasses[0].to_s.split('::').first
end

def find_model(model)
return model if model

model = model_class_name(@resource)
if model.nil? || engine_name == model.to_s
model = model_class_name(@resource, true)
end

return model
end
end
end
2 changes: 1 addition & 1 deletion lib/active_hash_relation/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ActiveHashRelation
VERSION = "1.1.0"
VERSION = "1.2.0"
end
10 changes: 9 additions & 1 deletion spec/support/support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ def query(str)
end

def q(*args)
args.join(' ')
args.each.with_index.inject(''){|memo, (str, index)|
if args[index-1] == '('
"#{memo}#{str}"
elsif str == ')'
"#{memo}#{str}"
else
"#{memo} #{str}"
end
}.strip
end
end

Expand Down
98 changes: 98 additions & 0 deletions spec/tests/or_filter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
describe ActiveHashRelation do
include Helpers

if ActiveRecord::VERSION::MAJOR < 5
context 'OR filter' do
it "one OR clause" do
logger = double('logger')
allow(logger).to receive(:warn)
allow(Rails).to receive(:logger).and_return(logger)

hash = {or: [{name: 'Filippos'}, {name: 'Vasilis'}]}

HelperClass.new.apply_filters(User.all, hash).to_sql
expect(logger).to have_received(:warn)
end
end

else
context 'OR filter' do
it "one OR clause" do
hash = {or: [{name: 'Filippos'}, {name: 'Vasilis'}]}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users",
"WHERE ((users.name = 'Filippos') OR (users.name = 'Vasilis'))"
)

expect(strip(query)).to eq expected_query.to_s
end

it "multiple OR clauses" do
hash = {or: [{or: [{name: 'Filippos'}, {name: 'Vasilis'}]}, {or: [{id: 1}, {id: 2}]}]}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users",
"WHERE",
"(",
"((users.name = 'Filippos') OR (users.name = 'Vasilis'))",
"OR",
"((users.id = 1) OR (users.id = 2))",
")"
)

expect(strip(query)).to eq expected_query.to_s
end

it "one complex OR clause" do
hash = {or: [{name: 'Filippos', token: '123'}, {name: 'Vasilis'}]}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users",
"WHERE",
"(",
"(users.name = 'Filippos') AND (users.token = '123')",
"OR",
"(users.name = 'Vasilis')",
")"
)

expect(strip(query)).to eq expected_query.to_s
end

it "nested OR clause" do
hash = {or: [{or: [{name: 'Filippos'}, {token: '123'}]}, {name: 'Vasilis'}]}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users",
"WHERE",
"(",
"((users.name = 'Filippos') OR (users.token = '123'))",
"OR",
"(users.name = 'Vasilis')",
")"
)

expect(strip(query)).to eq expected_query.to_s
end

it "OR clause on associations" do
hash = {microposts: {or: [{content: 'Sveavägen 4'}, {id: 1}]}}

query = HelperClass.new.apply_filters(User.all, hash, include_associations: true).to_sql
expected_query = q(
"SELECT users.* FROM users",
"INNER JOIN microposts ON microposts.user_id = users.id",
"WHERE ((microposts.content = 'Sveavägen 4') OR (microposts.id = 1))"
)

expect(strip(query)).to eq expected_query.to_s
end
end
end
end

29 changes: 29 additions & 0 deletions spec/tests/primary_key_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
describe ActiveHashRelation do
include Helpers

context 'primary_key' do
it "one key" do
hash = {id: 1}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users WHERE (users.id = 1)"
)

expect(strip(query)).to eq expected_query.to_s
end

it "multiple keys" do
hash = {id: [1,2,3,4]}

query = HelperClass.new.apply_filters(User.all, hash).to_sql
expected_query = q(
"SELECT users.* FROM users WHERE (users.id IN (1, 2, 3, 4))"
)

expect(strip(query)).to eq expected_query.to_s
end
end
end


0 comments on commit 9a333df

Please sign in to comment.