Fltr
Fltr lets you define and compose reusable filters for Ecto queries, with optional parsing for external input.
Usage
Define the supported filters and their argument counts, then implement one
to_expr clause for each filter:
defmodule TeamFilter do
use Fltr, filters: [active: 0, id: 1, name: 1]
def to_expr(:active), do: dynamic([team], team.active)
def to_expr(:id, id), do: dynamic([team], team.id == ^id)
def to_expr(:name, name), do: dynamic([team], team.name == ^name)
end
The argument count does not include the filter name. Here, :active maps to
to_expr/1, while :id and :name map to to_expr/2. Fltr verifies the
required callback arities when the module compiles.
A single filter
Pass a canonical filter directly to to_expr/1, then interpolate the resulting
dynamic expression into an Ecto query:
import Ecto.Query
filter = TeamFilter.to_expr({:active})
Team
|> where(^filter)
|> Repo.all()
Boolean groups
Use :all to require every child filter and :any to require at least one.
Groups can contain other groups:
filter =
{:all,
[
{:active},
{:any, [{:id, 7}, {:name, "Rovers"}]}
]}
filter = TeamFilter.to_expr(filter)
Team
|> where(^filter)
|> Repo.all()
External filters
When a filter comes from outside the application, pass it through the
parse/1 function generated by use Fltr before compiling it:
import Ecto.Query
def list_teams(filter) do
with {:ok, filter} <- TeamFilter.parse(filter) do
filter = TeamFilter.to_expr(filter)
Team
|> where(^filter)
|> Repo.all()
end
end
External filters use strings and lists:
list_teams([
"all",
[
["active"],
["any", [["id", 7], ["name", "Rovers"]]]
]
])
Arguments pass through unchanged by default. Define parse/2 in the filter
module when an argument needs validation or normalization:
def parse(:id, id) when is_binary(id) do
case Integer.parse(id) do
{id, ""} -> {:ok, id}
_other -> :error
end
end
def parse(:id, _id), do: :error
Lists and string-named tuples are treated as external input and invoke
parse/2. Atom-named tuples are trusted canonical expressions: Fltr checks
their argument count but does not parse their values again. Always call
parse/1 before converting external input into an atom-named tuple.
parse/1 returns an error describing invalid input:
{:error, {:invalid_filter, input}}
{:error, {:unknown_filter, name}}
{:error, {:invalid_arguments, name, arguments}}