Query Filters
Query Filters
Section titled “Query Filters”A filter is a named $where fragment attached to an entity and applied to every query unless bypassed. It’s UQL’s equivalent of EF Core global query filters or Eloquent global scopes - a JSON-native default $where. Soft-delete is the built-in example; you can define your own for visibility flags, multi-tenancy, and more.
Defining a filter
Section titled “Defining a filter”Use the @Filter decorator, the @Entity({ filters }) option, or defineFilter:
import { Entity, Id, Field, Filter } from 'uql-orm';
@Filter('active', { condition: { status: 'active' }, default: false })@Entity()export class Task { @Id() id?: number; @Field() status?: string;}condition- a$wherefragment (or a function returning one, see context).default- whether it applies unless bypassed. Defaults totrue; setfalsefor opt-in filters likeactiveabove.
A default-on filter’s keys are only added when your $where doesn’t already mention them, so an explicit $where on that field opts out.
The decorator-free equivalent, via defineFilter (see Imperative Definition):
import { defineEntity, defineField, defineFilter, defineId } from 'uql-orm';
class Task { id?: number; status?: string;}
defineId(Task, 'id', { type: Number });defineField(Task, 'status', { type: String });defineFilter(Task, 'active', { condition: { status: 'active' }, default: false });defineEntity(Task, {});Bypassing filters
Section titled “Bypassing filters”Every read/update/delete accepts QueryOptions.filters:
querier.findMany(Task, {}, { filters: false }); // disable all filtersquerier.findMany(Task, {}, { filters: { softDelete: false } }); // disable onequerier.findMany(Task, {}, { filters: { active: true } }); // force-enable a default:false filterParameterized filters & context
Section titled “Parameterized filters & context”A filter condition can be a function of an ambient context (e.g. the current tenant). Set the context for a span with withContext (and read it anywhere with getContext()); it propagates across awaits, Promise.all, and transactions - but not into event-callback ticks (emitters, timers, queues): bridge those with captureContext(), or scope a single pool call with pool.withQuerier(cb, { context }) (see event-driven pipelines):
import { withContext } from 'uql-orm';
@Filter('tenant', { // with no tenant, return `undefined` (instead of { companyId: undefined }) so the query throws instead of running unscoped condition: (ctx) => (ctx?.tenantId != null ? { companyId: ctx.tenantId } : undefined), security: true,})@Entity()export class Invoice { /* ... */ }
await withContext({ tenantId: 42 }, () => querier.findMany(Invoice, {}));// generates: ... WHERE (companyId = 42)Security filters (row-level security)
Section titled “Security filters (row-level security)”Mark a filter security: true to enforce tenant isolation / RLS. Security filters:
- are always applied -
filters: falseand per-name bypass are ignored; - are AND-merged as an independent predicate, so a client
$whereon the same field can’t widen them (companyId = 'other' AND companyId = 42matches no rows); - fail closed - if the condition can’t resolve (missing context), the query throws
UqlSecurityErrorinstead of running unscoped. (SetonMissing: 'skip'only on non-security filters.)
A condition may return {} to mean “resolved: no restriction” - the escape hatch for trusted cross-tenant work (startup recovery, maintenance jobs) that runs under an explicit system context, while a missing context still fails closed:
condition: (ctx) => (ctx?.system ? {} : ctx?.tenantId != null ? { companyId: ctx.tenantId } : undefined),// withContext({ system: true }, () => ...) -> unscoped; no context -> throwsOver HTTP
Section titled “Over HTTP”createRequestHandler({ getContext }) derives the context from the (verified) request and runs the whole request inside withContext, so filters scope automatically:
createRequestHandler({ getContext: (req) => ({ tenantId: req.user.tenantId }),});The wire parser allowlists query keys, so a remote client cannot send filters or context to bypass a security filter - those are server-only. Derive tenant/auth from a trusted source (session, verified JWT), never from client input.