Filter Classes
Writing filter classes — validated mode, lean mode, setup(), $query, $input, $context, and advanced features.
A filter class is a regular NestJS injectable that extends an ORM-specific base class (MikroOrmFilter<E> or TypeOrmFilter<E>). Methods decorated with @FilterFor() each handle one input key, mutating the query builder to add constraints.
Validated mode (filter as DTO)
When you add class-validator decorators to your filter class, it acts as both the filter and the validation DTO. Input is validated before any filter method runs.
import { Injectable } from '@nestjs/common';
import { Filterable, FilterFor, escapeLike } from '@dudousxd/nestjs-filter';
import { MikroOrmFilter } from '@dudousxd/nestjs-filter-mikro-orm';
import { IsOptional, IsString, IsNumber, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { User } from './user.entity.js';
@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
@IsOptional() @IsString()
name?: string;
@IsOptional() @IsNumber() @Min(0)
@Type(() => Number)
minAge?: number;
@FilterFor('name')
applyName(value: string) {
this.whereLike('name', value);
}
@FilterFor('minAge')
applyMinAge(value: number) {
this.$query.andWhere({ age: { $gte: value } });
}
}Use @Type(() => Number) from class-transformer on numeric fields. Query strings always arrive as strings, and class-transformer handles the coercion before validation runs.
Lean mode (no validation)
If you skip the class-validator decorators, the filter works in lean mode. Input keys are dispatched to matching @FilterFor() methods without validation.
@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
@FilterFor('name')
applyName(value: string) {
this.whereLike('name', value);
}
@FilterFor('minAge')
applyMinAge(value: number) {
this.$query.andWhere({ age: { $gte: value } });
}
}You can also explicitly disable validation by setting validation: 'off' in FilterModule.forRoot().
Auto-fields
By default, @Filterable({ entity: User }) enables auto-fields — any input key matching an entity column is automatically applied as a WHERE condition. Dot-notation for relations (posts.title) also works automatically. This means you can write zero-method filter classes for simple equality, array, and operator-based queries.
To opt out: @Filterable({ entity: User, autoFields: false })
Default behavior (auto-fields on)
@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
// No @FilterFor methods needed for simple equality checks.
// GET /users?name=Al&status=active → WHERE name = 'Al' AND status = 'active'
// GET /users?status[]=A&status[]=B → WHERE status IN ('A', 'B')
}The adapter introspects entity metadata (via getEntityFields()) to discover valid columns and rejects unknown fields automatically. This prevents unknown-column probing and SQL errors without requiring an explicit allowed list.
You can restrict which fields are auto-applied by combining with allowed:
@Filterable({ entity: User, allowed: ['name', 'email', 'status', 'age'] })The adapter introspects the entity's ORM metadata to build the set of valid column names. Only keys matching real entity columns are accepted; all others are silently skipped. If the adapter does not support metadata introspection, it falls back to accepting all keys (legacy behavior) with a logged warning.
If your adapter does not implement getEntityFields(), using auto-fields without an allowed list means any column name the client sends will be accepted. Always combine with allowed to restrict which fields are queryable, or ensure your adapter supports metadata introspection.
Explicit field list
You can also pass an explicit array of field names to auto-apply:
@Filterable({ entity: User, autoFields: ['name', 'email'] })Only name and email will be auto-applied. Other keys still need @FilterFor methods.
Opting out
To disable auto-fields entirely, set autoFields: false:
@Filterable({ entity: User, autoFields: false })With this setting, only keys with @FilterFor methods are dispatched. Unknown keys are handled by the onUnknownKey policy.
Mixing auto-fields with @FilterFor
You can combine auto-fields with explicit @FilterFor methods. Any key that has a @FilterFor mapping is dispatched to the method; only unmatched keys fall through to the auto-field handler.
@Injectable()
@Filterable({ entity: User, allowed: ['name', 'status', 'role'] })
export class UserFilter extends MikroOrmFilter<User> {
@FilterFor('name')
applyName(value: string) {
// Custom LIKE behavior for name
this.whereLike('name', value);
}
// 'status' and 'role' are auto-applied with simple equality
}Bracket notation with auto-fields
Auto-fields support operator objects via query string bracket notation:
GET /users?name[contains]=fleet&age[gte]=18&age[lte]=65The adapter detects operator objects and applies them accordingly (e.g. LIKE '%fleet%' for contains, >= 18 for gte).
The @FilterFor() decorator
@FilterFor(inputKey?) maps an input key to the decorated method. If inputKey is omitted, the method name is used as the key.
// Explicit key mapping
@FilterFor('name')
applyName(value: string) { /* ... */ }
// Method name = key (equivalent to @FilterFor('status'))
@FilterFor()
status(value: string) { /* ... */ }Each filter method receives two arguments:
value-- the input value for this keykey-- the input key name (useful when one method handles multiple keys)
The setup() hook
The optional setup() method runs once before any filter methods are dispatched. Use it for pre-processing that applies regardless of which input keys are present.
@Injectable()
@Filterable({ entity: Order })
export class OrderFilter extends MikroOrmFilter<Order> {
setup() {
// Always filter to the current user's orders
const userId = this.$context.user?.id;
if (userId) {
this.$query.andWhere({ userId });
}
}
@FilterFor('status')
applyStatus(value: string) {
this.$query.andWhere({ status: value });
}
}setup() can be synchronous or async. If it throws, the error is wrapped in a FilterMethodException with key === 'setup'.
Accessing $query, $input, and $context
Inside any filter method (including setup()), three properties are available via AsyncLocalStorage:
| Property | Type | Description |
|---|---|---|
this.$query | ORM QueryBuilder | The query builder being built up. MikroORM: QueryBuilder<E>. TypeORM: SelectQueryBuilder<E>. |
this.$input | Readonly<Record<string, unknown>> | The full normalized input object (frozen). |
this.$context | FilterContext | Optional context: { req?, user?, raw? }. Set by the interceptor or FilterRunner.apply(). |
this.$adapter | FilterAdapter | null | The active ORM adapter, if registered. |
The input() helper method
The input() method provides convenient access to input values:
// Get full input object
const all = this.input();
// Get a single key
const name = this.input('name');
// Get with default value
const page = this.input('page', 1);whitelistMethod() and blacklistMethod()
These protected methods let you dynamically allow or block filter keys at runtime, typically called in setup():
setup() {
const isAdmin = this.$context.user?.role === 'admin';
if (!isAdmin) {
// Non-admins cannot filter by internal status
this.blacklistMethod('internalStatus');
}
}setup() {
// Dynamically allow a key that is not in the static @FilterFor map
if (this.input('enableAdvanced')) {
this.whitelistMethod('advancedScore');
}
}Whitelisted keys bypass the static allowed/blocked checks in the dispatcher. Blacklisted keys are skipped regardless of configuration.
push() for dynamic input injection
The push() method injects additional key/value pairs into the dispatch queue. Pushed entries are processed after the current dispatch loop completes (BFS order).
@FilterFor('dateRange')
applyDateRange(value: string) {
const [start, end] = value.split(',');
this.push('startDate', start);
this.push('endDate', end);
}
@FilterFor('startDate')
applyStartDate(value: string) {
this.$query.andWhere({ createdAt: { $gte: new Date(value) } });
}
@FilterFor('endDate')
applyEndDate(value: string) {
this.$query.andWhere({ createdAt: { $lte: new Date(value) } });
}You can also push multiple keys at once:
this.push({ startDate: '2024-01-01', endDate: '2024-12-31' });related() for imperative relation filtering
The related() method lets you filter by a related entity's columns imperatively, without needing the @Relations decorator:
@FilterFor('publishedOnly')
async applyPublishedOnly(value: boolean) {
if (value) {
await this.related('posts', { status: 'published' });
}
}This creates a join on the posts relation and adds WHERE posts.status = 'published'. It delegates to the adapter's applyRelationConstraint under the hood.
// Full signature
await this.related(relationName: string, conditions: Record<string, unknown>): Promise<void>Use related() when you need conditional or dynamic relation filtering inside a @FilterFor method. For static key-to-relation mapping, prefer the @Relations decorator.
Inheritance
@FilterFor metadata is inherited from parent classes. A child class can override or extend parent filter methods:
@Injectable()
@Filterable({ entity: User })
export class BaseUserFilter extends MikroOrmFilter<User> {
@FilterFor('name')
applyName(value: string) {
this.whereLike('name', value);
}
}
@Injectable()
@Filterable({ entity: User })
export class AdminUserFilter extends BaseUserFilter {
// Inherits applyName from BaseUserFilter
@FilterFor('role')
applyRole(value: string) {
this.$query.andWhere({ role: value });
}
}The getFilterForMap() function walks the prototype chain, so parent @FilterFor mappings are available in child classes. If a child defines the same input key, the child's method takes precedence.
@Filterable() options
The @Filterable() class decorator associates the filter with an entity and optionally restricts which input keys are dispatched:
@Filterable({
entity: User,
allowed: ['name', 'email', 'status'], // only these keys
})@Filterable({
entity: User,
blocked: ['internalId', 'secret'], // everything except these
})| Option | Type | Description |
|---|---|---|
entity | Type<unknown> | Required. The entity class this filter targets. |
allowed | readonly string[] | Whitelist of input keys. Only these keys are dispatched. |
blocked | readonly string[] | Blacklist of input keys. These keys are never dispatched. |
You cannot specify both allowed and blocked on the same filter. The decorator throws an error if both are set.
ORM helper methods
Both MikroOrmFilter and TypeOrmFilter provide convenience methods for common LIKE queries:
| Method | SQL equivalent |
|---|---|
this.whereLike(field, value) | field LIKE '%value%' |
this.whereBeginsWith(field, value) | field LIKE 'value%' |
this.whereEndsWith(field, value) | field LIKE '%value' |
All values are escaped via escapeLike() to prevent SQL injection through LIKE wildcards.
@FilterFor('name')
applyName(value: string) {
this.whereLike('name', value); // safe: escapes %, _, and \
}The escapeLike() function is also exported from @dudousxd/nestjs-filter for manual use:
import { escapeLike } from '@dudousxd/nestjs-filter';
const safe = escapeLike(userInput); // escapes %, _, \