No release in over a year
ActiveRecord extension to filter records by a given set of parameters
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

ActiveRecord::Filterable

This is a gem created with the intent of being a go-to sollution when it comes to filtering your records with active record. The way we try to do it is creatin a filter method, where you can execute complex queries over your models, or simple ones as well, always maintaining the logic that you pass down. The gem also allows you to configure your own custom filters, with all the complex logic that you want, following a pattern developed by the rails guys, used on the scope method.

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add active_record-filterable

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install active_record-filterable

Or just add it to your Gemfile

gem 'active_record-filterable

Usage

The models

For the models that you want to respond to the filter method, you should add a act_as_filterable to it, like this:

class User < ActiveRecord::Base
  act_as_filterable
end

When you do it, the gem will redefine the ActiveRecord::DynamicMatchers for your columns, because the rails sollution, unfortunally doesn't retur a Class::ActiveRecord_Relation, so it can't be queried further.

# This model
User(id: integer, name: string, email: string, password_digest: string, created_at: datetime, updated_at: datetime)

# will have:

User#filter_by_id
User#filter_by_name
User#filter_by_email
User#filter_by_password_digest
User#filter_by_created_at
User#filter_by_updated_at

Note: The filter_by_ prepend it's very important for this gem, as we are going to see further

Using the filter method

This gem introduces the filter method for all models that have the act_as_filterable in it, and to use it, you could just:

User.filter(name: 'Jhon')
# will produce the following query
# => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"name\" = 'Jhon'"

And you can query like normal, continuing to give named params to it, and it will be continuing the query with the AND operator, like so:

User.filter(name: 'Jhon', email: 'jhon.smith@email.com')
# will produce the following query
# => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"name\" = 'Jhon'" AND \"users\".\"email\" = 'jhon.smith@email.com'"

So far, nothing spicy, right? Pretty much like the where method, default on the models. But now, you can query with OR conditions, just making sure that you are passing down an Array instead of a Hash.

User.filter([{name: "Jhon"}, {name: "doe"}])
# will produce
#   => "SELECT \"users\".* FROM \"users\" WHERE ( \"users\".\"name\" = 'Jhon' AND \"users\".\"name\" = 'doe' )"

And going down even further, if you need to have a complex case with and OR condition nested within the AND condition, just use a :or key, passing an array to it, like so:

User.filter([{name: "Jhon", email: "joaquin", or: [{name: "smith"}, {email: "smit"}]}, {name: "doe"}])
# will produce the following query
#   => "SELECT \"users\".* FROM \"users\" WHERE ( \"users\".\"name\" = 'Jhon' AND \"users\".\"email\" = 'joaquin' AND ( \"users\".\"name\" = 'smith' OR \"users\".\"email\" = 'smit' ) AND \"users\".\"name\" = 'doe' )"

The rule of thumb is:

  • If it is an Array -> the elements are joined by OR conditions
  • If it is an Hash -> the elements are joined by AND conditions

You can go as nested as you want, until you reach the call stack limit.

Which keys are valid for the hashes?

TLDR: Every class methods started with filter_by_ will generate a valid key (i.e.: filter_by_child_name -> :child_name will be a valid key)

So, be careful because the keys you choose to pass down NEED to map out to a class method with its name prepended with filter_by_.

Define your own filters

Every class method prepended with filter_by will turn into a valid key for the filter method, but we put down some helper methods to help you define it, using the scope pattern, already present in ActiveRecord

For instance if you have a custom, joined query to perform and you want for it to be in your possible keys for the filter method, you define it as the following:

class User < ActiveRecord::Base
  act_as_filterable
  has_many :posts, class_name: 'Post', foreign_key: 'author_id'
  has_many :likes, class_name: 'Like', foreign_key: 'author_id'
  has_many :comments, class_name: 'Comment', foreign_key: 'author_id'

  define_filter :posts_that_have_more_than_n_comments, ->(number_of_comments) { joins(posts: :comments).group('posts.id').having('count(comments.id) > ?', number_of_comments) }

  # This is the same that
  # scope :filter_by_posts_that_have_more_than_n_comments, ->(number_of_comments) { joins(posts: :comments).group('posts.id').having('count(comments.id) > ?', number_of_comments) }

  # or
#   def self.filter_by_posts_that_have_more_than_n_comments(n)
#     joins(posts: :comments).group('posts.id').having('count(comments.id) > ?', n)
#   end
end

The define_filter method takes up to 3 arguments, only requiring the first 2:

  • The name of the filter
    • Will be the name of the class method generated, prepened by "filter_by_"
  • The proc to be performed
    • Will be the body of said method
  • A block that extend its functionality
    • Can also be the body, if you rather declare it this way, or other methods that extend the filter functionality, more on that later.

Note: You can define you own class methods, if you want, or use the already well tested scope API, rather than that from the gem, it's really up to you.

Extending functionality

If you want, and it is totally up to you, it will not have any impact on the filter methor whatsoever, you can pass down a block as a third argument and use it as an extension for your filters, like so:

class User < ActiveRecord::Base
  act_as_filterable
  has_many :posts, class_name: 'Post', foreign_key: 'author_id'
  has_many :likes, class_name: 'Like', foreign_key: 'author_id'
  has_many :comments, class_name: 'Comment', foreign_key: 'author_id'

  define_filter :posts_that_have_more_than_n_comments, ->(number_of_comments) { joins(posts: :comments).group('posts.id').having('count(comments.id) > ?', number_of_comments) } do
    def newer_than(date)
      where('posts.created_at > ?', date)
    end

    def older_than(date)
      where('posts.created_at < ?', date)
    end
  end
end

With this you will be able to call:

irb(main):001:0> User.filter_by_posts_that_have_more_than_n_comments(2).newer_than(2.days.ago)
# => "SELECT \"users\".* FROM \"users\" INNER JOIN \"posts\" ON \"posts\".\"author_id\" = \"users\".\"id\" INNER JOIN \"comments\" ON \"comments\".\"post_id\" = \"posts\".\"id\" WHERE (posts.created_at > '2022-12-15 11:54:47.858206') GROUP BY \"posts\".\"id\" HAVING (count(comments.id) > 30)"

This is only present, because the implementation is flexible enough for you to make your body as a proc as well as a block, and the block has this options for you, so use it as you please.

NOTE: methods that extend your filters functionality will not add new valid keys to your filter method

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 the created tag, and push the .gem file to rubygems.org.

Contributing

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

License

The gem is available as open source under the terms of the MIT License.