Filtering
The filtering extensions, along with the field selection and the automatic DB querying is a powerful tool that makes it trivial for API clients to slice and dice what data to retrieve.
To take advantage of this extension you just need to do two things. First add which attributes you want to allow to filter by in your action design, and second, provide the mapping from these filter names, to the names of the underlying associations and/or database columns. Let's take a look at what this means before we get into the details.
Let's assume you want to allow to filter your Posts
by their title
, or by their author's email
or by the date is was posted_at
. To do so, you'd define a filters
param (typically in the index
action) much like this:
params do
attribute :filters, Praxis::Types::FilteringParams.for(MediaTypes::Post) do
filter 'title', using: ['=', '!='], fuzzy: true
filter 'author.email', using: ['=', '!=']
filter 'posted_at', using: ['=', '!=','>','<','>=','=<']
end
end
Note that we've made some decisions here, about restricting which operators we allow for each filter, including deciding that we can allow the client for filter by prefix/postfix on the title (i.e., a LIKE
or REGEXP
-type condition).
In your Resource business logic, you also have to define how these API filter names might map differently to the lower level DB columns of your data. In this case, we can see that the title
and posted_at
filters map to the same column names. We can also see that we need to map the author's email filter, to an underlying user
association, and a email_address
column. In other words, the mapping can traverse associations and/or map to names of columns that might or might not coincide with the filter names used. For simplicity, Praxis allows you to omit mapping simple names that concide (so we could have perfectly omitted title
and posted_at
in this example.) Note that omission is for 'simple' fields, you still need to define mappings for any nested field even if they both map to the same(i.e., 'user.id' => 'user.id' still needs to defined ).
filters_mapping(
'title': 'title',
'author.email': 'user.email_address',
'posted_at': 'posted_at',
)
FilteringParams type
Every filter defined in the Filtering params type can declare:
- the exposed name of the filter
- the operators that are allowed to use
- and if they can be used to do prefix/suffix matching.
When you use the .for
method of the FilteringParams
type, the defined filter names must correspond to fields of the mediatype. These names can recurse down into other related fields of the mediatypes, all the way down to the final condition. In other words, filtering is not restricted to a direct attribute, or a single nested level. The only way to provide filter names that do not map to the mediatype would be to use the FilteringParams
type without the .for
method, and properly build your parsing and coercion of attributes.
There are 8 available operators to choose from: =
, <
, >
,!=
, >=
, <=
, !
,!!
. The first 6 are seemingly self explanatory and always take a value (i.e., filter_name>value
). The last 2 operators are for checking NULL (or NOT NULL) values and do not accept a value. In particular:
filter_name!
would filter results whosefilter_name is NOT NULL
filter_name!!
would filter results whosefilter_name is NULL
Something important to point out, is that the filtering framework also allows to place conditions on associations directly (instead of leaf attributes). This powerful feature (only available for the !
and !!
operators) allows filtering results where full collections of related objects are empty, or are not empty. For example:
author!
would return only Posts which have associated authors.comments!!
would return only Posts which have no associated comments
Filters for string attributes, can also accept a fuzzy: true
option, which would allow to prefix or postfix patch the value. For example, using our title
filter above would allow us to do things like this:
title=*ending
, which will translate to a SQL query similar toLIKE "%ending"
title=starting*
, which will translate to a SQL query similar toLIKE "starting%"
title=*Iam%20a%2A%21
(whose encoded value after the*
isIam a*!
) which will translate to a SQL query similar toLIKE "%Iam a*!"
A filter condition generally has a single value, but Praxis allows you to pass a comma-separated list of values as a compact way to do a multimatch without requiring several clauses. These will generally be translated to IN clause
SQL queries. Here is an example some examples:
author.email=one%40example.com,two%40example.com
, which will typically translate into... email_address IN ('one@example.com', 'one@example.com)
Remember that we need to always escape the filter values. This multi-map case is no different, so we will do so for each of them but not the "commas" as that's part of the filter syntax. Also note that all of the resulting string of the filters
value will also be url-encoded when forming the query string of the API request.
The syntax allows you to to combine multiple filter conditions in the same API call. In fact, it is possible to combine them with AND
, OR
operands, as well as group them with parenthesis. The precedence of operands is exactly what you'd expect, where AND
has higher-precedence than OR
unless there are parenthesized groups defining an element.
&
is used toAND
two filters and/or expressions. For example:title=one&posted_at>2000-01-01
|
is used toOR
two filters and/or expressions. For example:title=one|author.email=email1
(
and)
are used to group one or more filter conditions. For example:(a=1|b=2)&c=3&d=4
Not to repeat ourselves too much, but obviously do not escape these symbols when used in this mode. Only escape them when they are part of a value.