Advanced Usage
Custom Filters, Sorts & Includes
Build reusable filter, sort, and include classes implementing Filter, Sort, and Include interfaces.
When callback functions (AllowedFilter.callback, AllowedSort.callback, AllowedInclude.callback) need to be reused across multiple models or endpoints, you can create dedicated classes implementing the Filter, Sort, or Include interfaces from @eienjs/adonisjs-api-query/types.
Custom Filter Class
Implement the Filter<Model> interface with a handle(query, value, property) method and register it via AllowedFilter.custom():
import type { LucidModel, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import type { StrictValuesWithoutRaw } from '@adonisjs/lucid/types/querybuilder';
import type { Filter } from '@eienjs/adonisjs-api-query/types';
export class DateRangeFilter<Model extends LucidModel> implements Filter<Model> {
public handle(
query: ModelQueryBuilderContract<Model, InstanceType<Model>>,
value: StrictValuesWithoutRaw | null,
property: string,
): void {
if (!Array.isArray(value) || value.length !== 2) {
return;
}
const [startDate, endDate] = value as [string, string];
void query.whereBetween(property, [startDate, endDate]);
}
}
import { AllowedFilter } from '@eienjs/adonisjs-api-query';
import { DateRangeFilter } from '#filters/custom/date_range_filter';
import Order from '#models/order';
// GET /orders?filter[created_between]=2026-01-01,2026-12-31
const orders = await Order.query()
.withRequest(request)
.allowedFilters([
AllowedFilter.custom('created_between', new DateRangeFilter(), 'createdAt'),
]);
Custom Sort Class
Implement the Sort<Model> interface with a handle(query, isDescending, property) method and register it via AllowedSort.custom():
import type { LucidModel, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import type { Sort } from '@eienjs/adonisjs-api-query/types';
export class StringLengthSort<Model extends LucidModel> implements Sort<Model> {
public handle(
query: ModelQueryBuilderContract<Model, InstanceType<Model>>,
isDescending: boolean,
property: string,
): void {
const direction = isDescending ? 'DESC' : 'ASC';
void query.orderByRaw(`LENGTH(??) ${direction}`, [property]);
}
}
import { AllowedSort } from '@eienjs/adonisjs-api-query';
import User from '#models/user';
import { StringLengthSort } from '#sorts/custom/string_length_sort';
// GET /users?sort=-nameLength
const users = await User.query()
.withRequest(request)
.allowedSorts([
AllowedSort.custom('nameLength', new StringLengthSort(), 'name'),
]);
Custom Include Class
Implement the Include<ParentModel> interface with a handle(query, include) method and register it via AllowedInclude.custom():
import type { LucidModel, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import type { ExtractModelRelations } from '@adonisjs/lucid/types/relations';
import type { Include } from '@eienjs/adonisjs-api-query/types';
export class PublishedRelationInclude<ParentModel extends LucidModel> implements Include<ParentModel> {
public handle(
query: ModelQueryBuilderContract<ParentModel, InstanceType<ParentModel>>,
include: string,
): void {
void query.preload(include as ExtractModelRelations<InstanceType<ParentModel>>, (subQuery) => {
void subQuery.where('isPublished', true);
});
}
}
import { AllowedInclude } from '@eienjs/adonisjs-api-query';
import { PublishedRelationInclude } from '#includes/custom/published_relation_include';
import User from '#models/user';
// GET /users?include=publishedPosts
const users = await User.query()
.withRequest(request)
.allowedIncludes([
AllowedInclude.custom('publishedPosts', new PublishedRelationInclude(), 'posts'),
]);
