Skip to content
NewFrom Mongoose to SQL6 min read

Query Filters

A filter is a named $where fragment attached to an entity and applied to every query on it unless bypassed. Soft-delete is a built-in one; define your own for visibility flags, tenants and the like.

Use the @Filter decorator, the @Entity({ filters }) option, or defineFilter from the imperative API:

import { Entity, Id, Field, Filter } from 'uql-orm';
@Filter('active', { where: { status: 'active' }, default: false })
@Entity()
export class Task {
@Id({ type: Number }) id?: number;
@Field({ type: String }) status?: string | null;
}
defineFilter(Task, 'active', { where: { status: 'active' }, default: false });

where is a $where fragment, or a function of the context that returns one. default (true unless set) says whether the filter applies without being asked for; set it false for an opt-in filter like active. When a where function returns undefined, onMissing decides: 'skip' (the default) leaves the filter out, and 'throw' fails the query.

A filter that is not a security filter yields to your query: if your $where names the same field, your condition wins.

Every read, update and delete takes filters in its options:

pool.findMany(Task, {}, { filters: false }); // disable all filters
pool.findMany(Task, {}, { filters: { softDelete: false } }); // disable one
pool.findMany(Task, {}, { filters: { active: true } }); // enable an opt-in filter

A where function receives the current context, such as the tenant. Set it for a block of code with withContext, and read it anywhere with getContext():

@Filter('tenant', {
// undefined makes the query throw UqlSecurityError instead of running unscoped
where: (ctx) =>
ctx?.tenantId != null ? { companyId: ctx.tenantId } : undefined,
security: true,
})
@Entity()
export class Invoice {
@Id({ type: Number }) id?: number;
@Field({ type: Number }) companyId?: number | null;
}
await withContext({ tenantId: 42 }, () => pool.findMany(Invoice, {}));
// ... WHERE (companyId = 42)

The context follows await, Promise.all and transactions, but not event callbacks such as emitters, timers and queues; see event-driven pipelines. To type its keys, declare interface UqlContext { tenantId: number } inside declare module 'uql-orm'.

A filter with security: true enforces isolation:

  • It always applies, ignoring filters: false and a bypass by name.
  • It is AND-merged, so a $where on the same field cannot widen it.
  • It fails closed: when its where returns undefined, as with no context, the query throws UqlSecurityError.

Over HTTP the server reads only query keys, so a client cannot send filters or context.

Multi-tenancy covers setting the context per request, the {} escape hatch for trusted cross-tenant jobs, and how security filters guard writes.