psyipm/query_filter

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# QueryFilter

[![Build Status](https://semaphoreci.com/api/v1/igormalinovskiy/query_filter/branches/master/shields_badge.svg)](https://semaphoreci.com/igormalinovskiy/query_filter)
[![Code Climate](https://codeclimate.com/github/psyipm/query_filter/badges/gpa.svg)](https://codeclimate.com/github/psyipm/query_filter)
[![Gem Version](https://badge.fury.io/rb/query_filter.svg)](https://badge.fury.io/rb/query_filter)

This gem provides DSL to write custom complex filters for ActiveRecord models. It automatically parses whitelisted params and adds filters to query.

Instead of writing this:
```ruby
class Order < ActiveRecord::Base
  def self.filter(params)
    query = all
    query = query.with_state(params[:state]) if params[:state].present?
    query
  end
end
```

you can write this:
```ruby
class Order < ActiveRecord::Base
  def self.filter(params)
    OrderFilter.new(all, params).to_query
  end
end
```

Where OrderFilter class looks like:

```ruby
# app/filters/order_filter.rb
#
class OrderFilter < QueryFilter::Base
  scope :customer_id
  scope :service

  range :total

  date_range :completed_at

  def scope_customer_id(value)
    query.where(customer_id: value)
  end

  def scope_service(value)
    query.where(service_id: value.to_i)
  end

  def range_total(range)
    query.where(range.query('orders.total'))
  end

  def date_range_completed_at(period)
    query.where(completed_at: period.range_original)
  end
end
```

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'query_filter'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install query_filter

## Usage

This gem support next types of filter params:
* scope - simple filter by column
    ```ruby
    scope :state

    def scope_state(value)
      # your code
    end
    ```
    Keyword `scope` defines type of filter, and argument `:state` is a key from params to use for filter. Can be restricted using array of allowed values

* range - between, greater than or equal, less than or equal
    For range inputs, can be used with 2 arguments or with 1, for example if both params supplied as
    ```ruby
    { price_from: 100, price_to: 200 }
    ```
    this will produce a query like
    ```sql
    orders.price BETWEEN 100 AND 200
    ```
    With one parameter `price_from` a query would look like:
    ```sql
    orders.price >= 100
    ```
    or `price_to`:
    ```sql
    orders.price <= 200
    ```

* splitter range - the same as range, but with one param
  Some JS slider libraries join ranges with splitter, this filter could be used for that.
  Sample params:
  ```ruby
  { price: '100;200' }
  ```
  query:
  ```sql
  orders.price BETWEEN 100 AND 200
  ```

* date range - the same as range, but for dates
    Sample params:
    ```ruby
    { created_at: '06/24/2017 to 06/30/2017' }
    ```
    query:
    ```sql
    orders.created_at BETWEEN '2017-06-24 04:00:00.000000' AND '2017-07-01 03:59:59.999999'
    ```

    When we have date with custom time:
    ```ruby
    date_range :range, format: '%m/%d/%Y %H:%M'

    def date_range_range(period)
      query.where(created_at: period.range)
    end
    ```

    `period.range` will ignore time and always return time start_of_day and end_of_day, but the method `period.range_original` will return dates without time modification

    ```ruby
    def date_range_range(period)
      query.where(created_at: period.range_original)
    end
    ```

* order by
    ```ruby
      order_by :sort_column, via: :sort_mode

      def order_by_sort_column(column, direction)
        # your code
      end
    ```
    Sample params:
    ```ruby
    { sort_column: 'created_at', sort_mode: 'desc' }
    ```
    query:
    ```sql
    ORDER BY "orders"."created_at" DESC
    ```

### Sample class with usage examples

To use scope filter with Order model define your filter class as `app/filters/order_filter.rb`
```ruby
class OrderFilter < QueryFilter::Base
  # can be restricted using array of values
  scope :state, only: [:pending, :completed]
  scope :deleted, only: TRUE_ARRAY
  scope :archived, if: :allow_archived?

  range :price

  splitter_range :price

  date_range :created_at

  order_by :sort_column, via: :sort_mode

  # Filter will be applied when following params present
  # { state: :pending }
  def scope_state(value)
    query.with_state(value)
  end

  def scope_deleted(_value)
    query.where(deleted: true)
  end

  def scope_archived(_)
    query.where(archived: true)
  end

  # Filter will be applied when following params present
  # { price_from: 100, price_to: 200 }
  def range_price(range)
    # You should pass SQL column name to `query` method
    # this will result with following query:
    # 'orders.price BETWEEN 100 AND 200'
    query.where(range.query('orders.price'))
  end

  def splitter_range_price(values)
    # 'orders.price BETWEEN 100 AND 200'
    query.where(price: value.range)
  end

  def date_range_created_at(period)
    query.where(created_at: period.range)
  end

  def order_by_sort_column(column, direction)
    query.reorder("orders.#{column} #{direction} NULLS LAST")
  end

  protected

  def allow_archived?
    @params[:old] == '1' || params[:state] == 'archived'
  end
end
```

### Configuration

You can set up some options in general

```ruby
# config/initializers/query_filter.rb
QueryFilter.setup do |config|
  # Date period format, by default: %m/%d/%Y
  config.date_period_format = '%d-%m-%Y'

  # Splitter to parse date period values, by default 'to'
  config.date_period_splitter = 'until'
end
```

## Testing

query_filter gem defines some custom RSpec matchers. Include them to your project:

```ruby
# spec/support/query_filter.rb

require "query_filter/rspec_matchers"

RSpec.configure do |config|
  config.include QueryFilter::RSpecMatchers, type: :query_filter
end
```

Custom matchers will be available to use in specs:

```ruby
# spec/filters/order_filter_spec.rb

describe OrderFilter, type: :query_filter do
  context "scope deleted" do
    it "performs query" do
      expect { filter(deleted: true) }.to perform_query(deleted: true)
    end

    it "doesn't perform query with wrong params" do
      expect { filter(deleted: "invalid_param") }.to_not perform_query
    end
  end

  it "performs query by state" do
    expect(relation).to receive(:with_state).with(:pending)
    filter(state: :pending)
  end

  it "reorders by sort_column" do
    expect { filter(sort_column: :id, sort_mode: :desc) }
      .to reorder.by("orders.id DESC NULLS LAST")
  end

  context "query against the database" do
    let(:order) { create(:order, state: :pending) }

    # relation is a double by default, but can be redefined to an actual ActiveRecord::Relation:
    before do
      relation Order.all
    end

    it "finds the order" do
      expect(filter(state: :pending)).to contain_exactly(order)
      expect(filter(state: :invalid_state)).to be_empty
    end
  end
end
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/query_filter.

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).