Features

Filtering

Filter Lucid queries using partial, exact, operator, scope, trashed, and callback filters.

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:

MethodSQL 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 MemberOperator
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'),
  ]);
If you want to filter a joined table column directly (e.g., 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;
}
When a parameterless scope receives 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),
  ]);
ModifierDescription
.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.