Phoenix API Toolkit v0.15.0 PhoenixApiToolkit.Ecto.DynamicFilters View Source
Dynamic filtering of Ecto queries is useful for creating list/index functions, and ultimately list/index endpoints, that accept a map of filters to apply to the query. Such a map can be based on HTTP query parameters, naturally.
Several filtering types are so common that they have been implemented using standard filter macro's. This way, you only have to define which fields are filterable in what way.
Documentation for such filters can be autogenerated using generate_filter_docs/2
.
Example without standard filters
import Ecto.Query
require Ecto.Query
def list_without_standard_filters(filters \\ %{}) do
from(user in "users", as: :user)
|> apply_filters(filters, fn
{:order_by, {field, direction}}, query ->
order_by(query, [user: user], [{^direction, field(user, ^field)}])
{literal, value}, query when literal in [:id, :name, :residence, :address] ->
where(query, [user: user], field(user, ^literal) == ^value)
_, query ->
query
end)
end
# filtering is optional
iex> list_without_standard_filters()
#Ecto.Query<from u0 in "users", as: :user>
# multiple literal matches can be combined
iex> list_without_standard_filters(%{residence: "New York", address: "Main Street"})
#Ecto.Query<from u0 in "users", as: :user, where: u0.address == ^"Main Street", where: u0.residence == ^"New York">
# literal matches and sorting can be combined
iex> list_without_standard_filters(%{residence: "New York", order_by: {:name, :desc}})
#Ecto.Query<from u0 in "users", as: :user, where: u0.residence == ^"New York", order_by: [desc: u0.name]>
# other fields are ignored / passed through
iex> list_without_standard_filters(%{number_of_arms: 3})
#Ecto.Query<from u0 in "users", as: :user>
Example with standard filters and autogenerated docs
Standard filters can be applied using the standard_filters/4
macro. It supports various filtering styles:
literal matches, set membership, smaller/greater than comparisons, ordering and pagination. These filters must
be configured at compile time. Standard filters can be combined with non-standard custom filters.
Documentation can be autogenerated.
@filter_definitions [
literals: [:id, :username, :address, :balance],
sets: [:roles],
smaller_than: [
inserted_before: :inserted_at,
balance_lt: :balance
],
greater_than_or_equals: [
inserted_at_or_after: :inserted_at,
balance_gte: :balance
]
]
# a custom filter function
def by_group_name(query, group_name) do
from(
[user: user] in query,
join: group in assoc(user, :group),
as: :group,
where: group.name == ^group_name
)
end
@doc """
My awesome list function. You can filter it, you know! And we guarantee the docs are up-to-date!
#{generate_filter_docs(@filter_definitions, literals: [:group_name])}
"""
def list_with_standard_filters(filters \\ %{}) do
from(user in "users", as: :user)
|> apply_filters(filters, fn
# Add custom filters first and fallback to standard filters
{:group_name, value}, query -> by_group_name(query, value)
filter, query -> standard_filters(query, filter, :user, @filter_definitions)
end)
end
# filtering is optional
iex> list_with_standard_filters()
#Ecto.Query<from u0 in "users", as: :user>
# let's do some filtering
iex> list_with_standard_filters(%{username: "Peter", balance_lt: 50.00, address: ["sesame street"]})
#Ecto.Query<from u0 in "users", as: :user, where: u0.address in ^["sesame street"], where: u0.balance < ^50.0, where: u0.username == ^"Peter">
# limit, offset, and order_by are supported
iex> list_with_standard_filters(%{limit: 10, offset: 1, order_by: {:address, :desc}})
#Ecto.Query<from u0 in "users", as: :user, order_by: [desc: u0.address], limit: ^10, offset: ^1>
# complex custom filters can be combined with the standard filters
iex> list_with_standard_filters(%{group_name: "admins", balance_gte: 50.00})
#Ecto.Query<from u0 in "users", as: :user, join: g1 in assoc(u0, :group), as: :group, where: u0.balance >= ^50.0, where: g1.name == ^"admins">
# other fields are ignored / passed through
iex> list_with_standard_filters(%{number_of_arms: 3, order_by: {:boom, :asc}})
#Ecto.Query<from u0 in "users", as: :user>
This will generate the following docs:
iex> generate_filter_docs(@filter_definitions, literals: [:group_name])
"## Literal filters\n\nLiteral filters are compared for equality (is the filter value equal to the row's value?).\nThe following filters are supported:\n* `address`\n* `balance`\n* `group_name`\n* `id`\n* `username`\n\n## Set filters\n\nSet filters are compared for set membership (is the filter value a member of the row's set?).\nThe following filters are supported:\n* `roles`\n\n## Smaller-than filters\n\nSmaller-than filters are compared relatively (is the filter value smaller than the row value?).\nThe following filters are supported:\n\nFilter | Must be smaller than\n--- | ---\n`balance_lt` | `balance`\n`inserted_before` | `inserted_at`\n\n## Greater-than-or-equal-to filters\n\nGreater-than-or-equals filters are compared relatively (is the filter value greater than or equal to the row value?).\nThe following filters are supported:\n\nFilter | Must be greater than or equal to\n--- | ---\n`balance_gte` | `balance`\n`inserted_at_or_after` | `inserted_at`\n\n## Order-by filters\n\nOrder-by filters take an argument of format `{:field, :direction}`, so for example\n`{:username, :desc}`, and sort the result set.\nThe following fields are supported:\n* `address`\n* `balance`\n* `id`\n* `username`\n\nThe supported directions can be found in the docs of `Ecto.Query.order_by/3`.\n\n## Pagination filters\n\nThe pagination filters are `limit` and `offset`.\nThese filters invoke `Ecto.Query.limit/2` and `Ecto.Query.offset/2` respectively.\n"
Which will be rendered as:
Literal filters
Literal filters are compared for equality (is the filter value equal to the row's value?). The following filters are supported:
address
balance
group_name
id
username
Set filters
Set filters are compared for set membership (is the filter value a member of the row's set?). The following filters are supported:
roles
Smaller-than filters
Smaller-than filters are compared relatively (is the filter value smaller than the row value?). The following filters are supported:
Filter Must be smaller than balance_lt
balance
inserted_before
inserted_at
Greater-than-or-equal-to filters
Greater-than-or-equals filters are compared relatively (is the filter value greater than or equal to the row value?). The following filters are supported:
Filter Must be greater than or equal to balance_gte
balance
inserted_at_or_after
inserted_at
Order-by filters
Order-by filters take an argument of format
{:field, :direction}
, so for example{:username, :desc}
, and sort the result set. The following fields are supported:
address
balance
id
username
The supported directions can be found in the docs of
Ecto.Query.order_by/3
.Pagination filters
The pagination filters are
limit
andoffset
. These filters invokeEcto.Query.limit/2
andEcto.Query.offset/2
respectively.
Link to this section Summary
Types
Extra filters supported by a function, for which documentation should be generated by generate_filter_docs/2
.
A keyword list of filter types and the fields for which documentation should be generated.
Format of a filter that can be applied to a query to narrow it down
Filter definitions supported by standard_filters/4
.
A keyword list of filter types and the fields for which they should be generated.
Functions
Applies filters
to query
by reducing filters
using filter_reductor
.
Combine with the custom queries from Ecto.Query
to write complex
filterables. Several standard filters have been implemented in
standard_filters/4
.
Generate a markdown docstring from filter definitions, as passed to standard_filters/4
,
as defined by filter_definitions/0
. By specifying extras
, documentation can be generated
for any custom filters supported by your function as well.
Applies standard filters to the query. Standard filters include filters for literal matches, set membership, smaller/greater than comparisons, ordering and pagination.
Link to this section Types
Specs
extra_filter_definitions() :: [ literals: [atom()], sets: [atom()], smaller_than: keyword(atom()), greater_than_or_equals: keyword(atom()), order_by: [atom()] ]
Extra filters supported by a function, for which documentation should be generated by generate_filter_docs/2
.
A keyword list of filter types and the fields for which documentation should be generated.
Specs
Format of a filter that can be applied to a query to narrow it down
Specs
filter_definitions() :: [ literals: [atom()], sets: [atom()], smaller_than: keyword(atom()), greater_than_or_equals: keyword(atom()) ]
Filter definitions supported by standard_filters/4
.
A keyword list of filter types and the fields for which they should be generated.
Link to this section Functions
Specs
apply_filters( Ecto.Query.t(), map(), (Ecto.Query.t(), filter() -> Ecto.Query.t()) ) :: Ecto.Query.t()
Applies filters
to query
by reducing filters
using filter_reductor
.
Combine with the custom queries from Ecto.Query
to write complex
filterables. Several standard filters have been implemented in
standard_filters/4
.
See the module docs Elixir.PhoenixApiToolkit.Ecto.DynamicFilters
for details and examples.
Specs
generate_filter_docs(filter_definitions(), extra_filter_definitions()) :: binary()
Generate a markdown docstring from filter definitions, as passed to standard_filters/4
,
as defined by filter_definitions/0
. By specifying extras
, documentation can be generated
for any custom filters supported by your function as well.
See the module docs Elixir.PhoenixApiToolkit.Ecto.DynamicFilters
for details and examples.
standard_filters(query, filter, main_binding, filter_definitions)
View Source (macro)Specs
standard_filters(Ecto.Query.t(), filter(), atom(), filter_definitions()) :: any()
Applies standard filters to the query. Standard filters include filters for literal matches, set membership, smaller/greater than comparisons, ordering and pagination.
See the module docs Elixir.PhoenixApiToolkit.Ecto.DynamicFilters
for details and examples.
Mandatory parameters:
query
: the Ecto query that is narrowed downfilter
: the current filter that is being applied toquery
main_binding
: the named binding of the Ecto model that generic queries are applied tofilter_definitions
: keyword list of filter types and the fields for which they should be generated
The options supported by the filter_definitions
parameter are:
literals
: fields comparable by equality, also the fields by which the query can be ordered.sets
: fields comparable by set membershipsmaller_than
: keyword list of virtual "smaller_than" fields and the actual field with which a smaller-than comparison is madegreater_than_or_equals
: keyword list of virtual "greater_than_or_equals" fields and the actual field with which a greater-than-or-equal-to comparison is made
For ordering on multiple columns, this is a start:
{:order_by, fields}, q ->
Enum.reduce(fields, q, fn {binding, dir, fld}, q ->
order_by(q, [{^binding, bd}], [{^dir, field(bd, ^fld)}])
end)