[data-reveal]{opacity:1!important;transform:none!important}
Filters
Data API

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

OperatorDescription
eqEqual to (default)
neNot equal to
gtGreater than
geGreater than or equal to
ltLess than
leLess 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>;
ParameterDescription
contextThe request context, extended with this referencing the controller instance
proxyA query expression produced by the field modifier's unlockrequest() method; see the limitations below
keyThe field name being filtered
valueThe filter value from the query parameter
modeThe comparison operator from the query parameter
rowThe 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.