filterable
Declarative, whitelisted query filtering and ordering for ActiveRecord.
A model declares which columns are datable and sortable. filterable(params)
then folds a chain of small, composable filters over the relation, reading from
nested request params and touching only the columns the model explicitly
exposed. An unknown attribute, a malformed date, or an undeclared sort term
narrows nothing — it never raises and never leaks an arbitrary column into the
query.
Requires Ruby ≥ 3.2 and ActiveRecord ≥ 7.1. No dependency on ActionPack:
ActionController::Parameters are supported by duck-typing (to_unsafe_h).
Installation
The gem is published to the Fluence GitHub Packages registry:
# Gemfile
source 'https://rubygems.pkg.github.com/fluence-eu' do
gem 'filterable'
end
bundle install
Usage
In a Rails application a Railtie auto-includes the engine into every model, so
a model just declares its whitelisted columns:
class MovementDetail < ApplicationRecord
datable :value_date, :booking_date
sortable :value_date, :gross_amount_cents
end
filterable is then available on every model; one that declares nothing simply
returns all. See Rails integration for what the Railtie
wires, and Outside Rails to opt in manually.
From a controller, hand the request params straight in:
def index
@details = MovementDetail.filterable(params)
end
filterable reads from params[:filters] and returns an ordinary relation, so
it chains with everything else (pagination, scopes, includes, …):
MovementDetail.where(account: account).filterable(params).page(2)
Date filters (Datable)
Each declared datable attribute accepts these nested keys under filters[<attribute>]:
| Key | Meaning | SQL |
|---|---|---|
after |
strictly after the bound | > value |
before |
strictly before the bound | < value |
since |
on or before the bound (inclusive) | <= value |
from + to |
inclusive range (both required) | BETWEEN |
MovementDetail.filterable(
filters: { value_date: { after: '2026-01-15', before: '2026-03-01' } }
)
Unparseable values are dropped silently (the filter is a no-op), so a bad query param can never raise.
Ordering (Sortable)
filters[sort] is a comma-separated list of declared attributes, each optionally
prefixed with + (ascending, the default) or - (descending):
MovementDetail.filterable(filters: { sort: '-value_date,+gross_amount_cents' })
Undeclared attributes are ignored.
Public names vs. DB columns
Both DSLs accept a hash to expose a public name that differs from the column:
datable date: :value_date
sortable amount: :gross_amount_cents
# filters[date][after] / filters[sort]=-amount
Custom filters
Filterable::Concern is the whole engine. Register any object responding to
call(filters_params, scope), or a block, and it joins the fold:
class MovementDetail < ApplicationRecord
include Filterable::Concern
add_filter do |filters, scope|
scope.where(reference: filters[:reference]) if filters[:reference]
end
end
A filter returning nil leaves the scope untouched, so guard clauses are safe.
Rails integration
Filterable::Railtie registers a single initializer that hooks
ActiveSupport.on_load(:active_record) and mixes the engine into
ActiveRecord::Base:
include Filterable::Concern
include Filterable::Concerns::Datable
include Filterable::Concerns::Sortable
So every model gains filterable and the datable / sortable DSL with no
boilerplate. A model that declares no columns keeps an empty whitelist, so
filterable is a harmless pass-through that returns all.
Outside Rails
The railtie is loaded only when Rails::Railtie is defined. With plain
ActiveRecord, include the pieces yourself:
class MovementDetail < ActiveRecord::Base
include Filterable::Concern
include Filterable::Concerns::Datable
include Filterable::Concerns::Sortable
datable :value_date
sortable :value_date
end
Include order matters.
Filterable::Concernprovidesadd_filter, which theDatable/Sortableconcerns call at include time — so it must come first.
How it works
Filterable::Concernkeeps a per-class list of filters (cloned down the inheritance chain) andfilterablereduces the relation through them.Filterable::Concerns::Datable/Sortableare thinActiveSupport::Concernmixins: they register the concrete filters and add thedatable/sortabledeclaration DSL on top of the engine.- The concrete date filters live under
Filterable::Datable::{After,Before,Range,Since}and the ordering filter isFilterable::Sortable.
Development
bin/setup # install dependencies
bundle exec rake # specs + rubocop (the default task)
bundle exec rspec # specs only
bundle exec rubocop # lint only
bin/console # interactive prompt
The suite runs against an in-memory SQLite database (see spec/spec_helper.rb),
so there is nothing to set up.
Contributing
See CONTRIBUTING.md. Bug reports and pull requests are welcome on the GitHub repository.
License
Released under the MIT License.