Filtering
The filter query parameter lets clients filter records by model attributes, relationships, Lucid scopes, or custom callbacks.
All filters must be explicitly allowed using .allowedFilters() on the query builder or via the filters property on a Filterable Class.
Basic Usage
By default, passing a string column name to .allowedFilters() registers a case-insensitive partial filter (LOWER(column) LIKE %value%):
// GET /users?filter[name]=john
const users = await User.query()
.withRequest(request)
.allowedFilters('name', 'email');
// Or pass an array:
// .allowedFilters(['name', 'email'])
If a client requests a filter that is not allowed, an InvalidFilterQuery exception (400 Bad Request) is thrown unless disableInvalidFilterQueryException is enabled in Configuration.
Filter Types
Use the AllowedFilter factory methods to configure how each filter behaves:
| Method | SQL Behavior |
|---|---|
AllowedFilter.partial('name') | Case-insensitive partial match (LOWER(name) LIKE '%value%'). Default when passing a string. |
AllowedFilter.exact('id') | Exact match (= value, WHERE IN (...) for comma-separated arrays, or IS NULL). |
AllowedFilter.beginsWithStrict('name') | Prefix match (name LIKE 'value%'). |
AllowedFilter.endsWithStrict('email') | Suffix match (email LIKE '%value'). |
AllowedFilter.operator('age', FilterOperator.GreaterThan) | Comparison operator (=, <, >, <=, >=, <>, or FilterOperator.Dynamic). |
AllowedFilter.scope('popular') | Invokes a Lucid model query scope (static popular = scope(...)). |
AllowedFilter.trashed() | Filters soft-deleted models (with or only) when using @eienjs/adonisjs-soft-deletes. |
AllowedFilter.callback('search', fn) | Runs a custom callback (query, value, property) => void. |
AllowedFilter.custom('name', new MyFilter()) | Uses a custom class implementing the Filter<Model> interface. |
Exact Filters
Exact filters check for equality (=). When multiple comma-separated values are passed in the URL, FiltersExact automatically uses whereIn:
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
// GET /users?filter[status]=active
// -> WHERE users.status = 'active'
// GET /users?filter[status]=active,pending
// -> WHERE users.status IN ('active', 'pending')
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.exact('status'),
AllowedFilter.exact('isAdmin'), // 'true' and 'false' are automatically converted to booleans
]);
Partial, BeginsWithStrict & EndsWithStrict Filters
Partial filters perform a LIKE query and automatically escape %, _, and \ characters in user input. Passing multiple comma-separated values groups them with OR:
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
// GET /users?filter[name]=john,jane
// -> WHERE (LOWER(name) LIKE '%john%' OR LOWER(name) LIKE '%jane%')
// GET /users?filter[username]=admin
// -> WHERE username LIKE 'admin%'
// GET /users?filter[email]=@example.com
// -> WHERE email LIKE '%@example.com'
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.partial('name'),
AllowedFilter.beginsWithStrict('username'),
AllowedFilter.endsWithStrict('email'),
]);
Operator Filters
Use AllowedFilter.operator() with FilterOperator to filter by numeric or date comparisons:
import { AllowedFilter, FilterOperator } from '@eienjs/adonisjs-api-query';
const users = await User.query()
.withRequest(request)
.allowedFilters([
// Fixed operator: GET /users?filter[age]=18 -> WHERE users.age >= 18
AllowedFilter.operator('age', FilterOperator.GreaterThanOrEqual),
// Dynamic operator: parses the operator prefix from the query value!
// GET /users?filter[salary]=>50000 -> WHERE users.salary > 50000
// GET /users?filter[salary]=<=100000 -> WHERE users.salary <= 100000
// GET /users?filter[salary]=<>0 -> WHERE users.salary <> 0
AllowedFilter.operator('salary', FilterOperator.Dynamic),
]);
Available FilterOperator values:
| Enum Member | Operator |
|---|---|
FilterOperator.Dynamic | '' (parses =, <, >, <=, >=, <> from the value prefix; defaults to =) |
FilterOperator.Equal | '=' |
FilterOperator.LessThan | '<' |
FilterOperator.GreaterThan | '>' |
FilterOperator.LessThanOrEqual | '<=' |
FilterOperator.GreaterThanOrEqual | '>=' |
FilterOperator.NotEqual | '<>' |
You can also pass 'and' (default) or 'or' as the third argument to AllowedFilter.operator(name, operator, boolean, internalName).
Filtering by Internal Column Name (Aliases)
Often you want to expose a clean filter name in the URL that maps to a different database column or property name on the model. Pass the internalName as the second argument:
// GET /users?filter[hasVerifiedEmail]=true -> filters on `emailVerifiedAt`
// GET /users?filter[user_id]=5 -> filters on `id`
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.exact('user_id', 'id'),
AllowedFilter.partial('full_name', 'name'),
]);
Filtering Nested Relationships
Exact, partial, beginsWithStrict, endsWithStrict, operator, and scope filters support dot-notation to filter records by related model attributes using whereHas automatically:
// GET /users?filter[posts.title]=AdonisJS&filter[posts.isPublished]=true
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.partial('posts.title'),
AllowedFilter.exact('posts.isPublished'),
// Or alias a nested relationship filter:
// GET /users?filter[commentAuthor]=John
AllowedFilter.exact('commentAuthor', 'posts.comments.authorName'),
]);
posts.title) without wrapping it in a whereHas subquery, pass false as the third argument (shouldAddRelationConstraint):
AllowedFilter.exact('posts.title', undefined, false)Scope Filters
Use AllowedFilter.scope() to trigger Lucid query scopes defined on your model:
import { BaseModel, column, scope } from '@adonisjs/lucid/orm';
export default class User extends BaseModel {
// Scope without extra parameters
public static verified = scope((query) => {
void query.whereNotNull('email_verified_at');
});
// Scope with a parameter
public static ofRole = scope((query, role: string) => {
void query.where('role', role);
});
// Scope with multiple parameters
public static ageBetween = scope((query, min: string, max: string) => {
void query.whereBetween('age', [Number(min), Number(max)]);
});
@column({ isPrimary: true })
declare public id: number;
@column()
declare public role: string;
@column()
declare public age: number;
}
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
import User from '#models/user';
// GET /users?filter[verified]=true&filter[ofRole]=admin&filter[ageBetween]=18,30
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.scope('verified'),
AllowedFilter.scope('ofRole'),
AllowedFilter.scope('ageBetween'),
]);
false (e.g., ?filter[verified]=false), the scope is not applied. When a scope accepts multiple arguments, comma-separated values (?filter[ageBetween]=18,30) are spread as individual parameters into the scope function.Trashed Filters (Soft Deletes)
If your model uses @eienjs/adonisjs-soft-deletes (or provides withTrashed() and onlyTrashed() query methods), use AllowedFilter.trashed():
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
// GET /users?filter[trashed]=with -> includes soft-deleted records (query.withTrashed())
// GET /users?filter[trashed]=only -> returns only soft-deleted records (query.onlyTrashed())
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.trashed(),
]);
Callback & Custom Filters
Callback Filter
For one-off custom filter logic, use AllowedFilter.callback():
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
// GET /users?filter[search]=john
const users = await User.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.callback('search', (query, value) => {
void query.where((subQuery) => {
void subQuery
.whereILike('name', `%${String(value)}%`)
.orWhereILike('email', `%${String(value)}%`);
});
}),
]);
Custom Filter Class
For reusable filters across multiple models, implement the Filter<Model> interface and pass an instance to AllowedFilter.custom(). See Custom Filters, Sorts & Includes for details.
Default, Nullable & Ignored Filter Values
Every AllowedFilter instance supports fluent modifier methods:
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
const users = await User.query()
.withRequest(request)
.allowedFilters([
// Apply a default filter value when `filter[status]` is omitted from the request:
AllowedFilter.exact('status').setDefault('active'),
// Ignore specific values (e.g. 'all' or '' from a frontend dropdown):
AllowedFilter.exact('role').ignore('all', ''),
// Allow filtering by `null` (WHERE deleted_at IS NULL):
AllowedFilter.exact('deletedAt').setNullable(true),
]);
| Modifier | Description |
|---|---|
.setDefault(value) | Applies value when the filter is not present in the request URL. Passing null also enables .setNullable(true). |
.unsetDefault() | Removes a previously configured default value. |
.ignore(...values) | Ignores any matching values (or strips them from array values). If all values are ignored, the filter is skipped. |
.setNullable(boolean) | When true, allows null filter values to execute query.whereNull(...) instead of skipping the filter. |
.rule(vineSchema) | Attaches a VineJS validation schema to this specific filter. |
