Filters
Overview
The @Filter decorator adds filtering capabilities to list endpoints. Filters allow clients to narrow down results using query parameters. The module provides a default filter with standard comparison operators and supports custom filter functions for advanced logic.
Basic Usage
Default Filter
Apply @Filter() without arguments to enable standard comparison filtering on a string field. Query filter values remain strings: TypeScript annotations, @Field, and write validators do not parse them into numbers or booleans.
import { Filter } from "@antelopejs/interface-data-api/metadata";
@RegisterDataController()
class UserAPI extends DataController(
User,
DefaultRoutes.All,
Controller("/users"),
) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Listable()
@Access(AccessMode.ReadOnly)
declare _id: string;
@Listable()
@Access(AccessMode.ReadWrite)
@Filter()
declare status: string;
@Listable()
@Access(AccessMode.ReadWrite)
@Filter()
declare role: string;
}
Query Parameter Format
Filters are applied through query parameters using the following format:
filter_<fieldName>=<value>
filter_<fieldName>=<operator>:<value>
When no operator is specified, eq (equal) is used by default.
Comparison Operators
| Operator | Description |
|---|---|
eq | Equal to (default) |
ne | Not equal to |
gt | Greater than |
ge | Greater than or equal to |
lt | Less than |
le | Less than or equal to |
Examples
Retrieve active users:
GET /users/list?filter_status=active
Retrieve users whose role is not admin:
GET /users/list?filter_role=ne:admin
Combine multiple filters:
GET /users/list?filter_status=active&filter_role=ne:admin
Numeric and Boolean Filters
Parse and validate values explicitly in a custom filter. These filters support the six numeric comparisons and boolean equality/inequality; unsupported input throws an HTTP 400 response.
import { assert } from "@antelopejs/interface-api-util";
import type { FilterFunction } from "@antelopejs/interface-data-api/metadata";
export const numberFilter: FilterFunction<Record<string, unknown>> = (
_context,
proxy,
_key,
raw,
mode,
) => {
const value = Number(raw);
assert(raw.trim() !== "" && Number.isFinite(value), 400, "Invalid number");
assert(
["eq", "ne", "gt", "ge", "lt", "le"].includes(mode),
400,
"Invalid comparison",
);
return proxy[mode](value);
};
export const booleanFilter: FilterFunction<Record<string, unknown>> = (
_context,
proxy,
_key,
raw,
mode,
) => {
assert(raw === "true" || raw === "false", 400, "Invalid boolean");
assert(mode === "eq" || mode === "ne", 400, "Invalid comparison");
return proxy[mode](raw === "true");
};
Apply the functions to the corresponding fields in your controller:
@Filter(numberFilter, false)
declare age: number;
@Filter(booleanFilter, false)
declare isActive: boolean;
With these custom filters, ?filter_age=gt:30&filter_isActive=true compares against 30 and true, not "30" and "true". Keep useIndex false for callbacks that parse values: indexed equality uses the raw string directly and bypasses the filter callback.
Custom Filters
For filtering logic beyond simple comparisons, pass a custom filter function to @Filter.
Filter Function Signature
type FilterFunction<T, U> = (
context: RequestContext & { this: T },
proxy: ValueProxy<any>,
key: string,
value: string,
mode: "eq" | "ne" | "gt" | "ge" | "lt" | "le",
row: ValueProxy<U>,
) => ValueProxyOrValue<boolean>;
| Parameter | Description |
|---|---|
context | The request context, extended with this referencing the controller instance |
proxy | A query expression produced by the field modifier's unlockrequest() method; see the limitations below |
key | The field name being filtered |
value | The filter value from the query parameter |
mode | The comparison operator from the query parameter |
row | The full database row as a ValueProxy |
Example: Pattern Matching Filter
function filterByEmailDomain(
context: RequestContext,
proxy: ValueProxy<any>,
key: string,
value: string,
mode: string,
row: ValueProxy<Record<string, any>>,
) {
assert(mode === "eq", 400, "Only equality is supported for email domains");
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return proxy.match(`@${escaped}$`);
}
@RegisterDataController()
class UserAPI extends DataController(
User,
DefaultRoutes.All,
Controller("/users"),
) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Listable()
@Access(AccessMode.ReadWrite)
@Filter(filterByEmailDomain)
declare email: string;
@Listable()
@Access(AccessMode.ReadWrite)
@Filter(numberFilter, false)
declare age: number;
}
Query for users with Gmail addresses:
GET /users/list?filter_email=gmail.com
Import assert from @antelopejs/interface-api-util. Escaping the domain makes the dot literal rather than a regular-expression wildcard. This example deliberately rejects operators other than eq instead of silently treating every request as equality.
The useIndex Parameter
The @Filter decorator accepts an optional second parameter, useIndex, that controls whether the filter can use a database index for optimized lookups.
@Filter(undefined, true) // Default filter with index optimization
declare status: string;
@Filter(customFilter, false) // Custom filter without index optimization
declare name: string;
When useIndex is not specified:
- Default filters (no custom function) automatically use the index if the field has one.
- Custom filters default to no index usage.
Index-optimized filters with an eq operator perform an indexed lookup instead of a full table scan, which significantly improves performance on large datasets.
Mandatory Route Filters
Use DefaultRoutes.WithOptions to constrain a list route. The filters map contains [stringValue, comparison] tuples for fields declared with @Filter. Request filters for other fields are retained, but a route filter wins when both specify the same field.
import { DefaultRoutes } from "@antelopejs/interface-data-api";
const activeUsers = DefaultRoutes.WithOptions(DefaultRoutes.List, {
filters: { status: ["active", "eq"] },
});
On a controller with a status filter, ?filter_status=inactive&filter_role=admin still restricts status to "active" while retaining the requested role. Route filters are list-query constraints, not authorization for separate get/edit/delete routes.
Modifier Integration
The proxy parameter is a database expression, not an unlocked model instance. The Data API calls the modifier's unlockrequest() method to build that expression. Localization can select a locale at query time, but the built-in EncryptionModifier.unlockrequest() returns the stored expression unchanged: it does not decrypt ciphertext in the database. A custom modifier must explicitly support query-time access before a filter can operate on its logical value.
For raw database values, use the row parameter and access the field directly.