Skip to content

Filter System

Every repository read, update, and delete verb takes the same filter object. It picks rows (where), columns (fields), order (order), and how many (limit/skip).

The vocabulary ships as its own package, @venizia/ignis-filter. Applications on @venizia/ignis already get every name here re-exported from the core barrel, so nothing changes for them. Install it directly only when you want the filter language without the server framework - a browser or a Web Worker - since it resolves no node builtin and no server-only dependency:

typescript
import { QueryOperators, Sorts, type TFilter } from '@venizia/ignis-filter';
import { FilterSchema, WhereSchema } from '@venizia/ignis-filter/schemas';

On a server take the schemas from @venizia/ignis instead: the ones on that subpath carry no OpenAPI metadata, so a route built on them documents nothing.

In one example

where picks rows, fields picks columns, order sorts, limit bounds the result:

typescript
import { postRepository } from '@/repositories';

const posts = await postRepository.find({
  filter: {
    where: {
      status: 'published',
      or: [{ featured: true }, { rating: { gte: 4.5 } }],
    },
    fields: ['id', 'title', 'rating', 'publishedAt'],
    order: ['rating DESC', 'publishedAt DESC'],
    limit: 20,
  },
});

postRepository is a @repository({ model: Post, dataSource })-bound repository. Post's schema comes from @/schemas - see Models and Repositories.

sql
-- Equivalent SQL
SELECT "id", "title", "rating", "published_at"
FROM "post"
WHERE "status" = 'published' AND ("featured" = true OR "rating" >= 4.5)
ORDER BY "rating" DESC, "published_at" DESC
LIMIT 20

How it works

  • TFilter maps straight to SQL. Every property corresponds to one clause of the generated query - see the table below.
  • where takes a bare value or an operator object. A bare value is implicit equality (null becomes IS NULL, an array becomes IN). An operator object keys into one of the operator families.
  • Multiple where keys are an implicit AND. A dot-notation key ('metadata.path') targets a JSON/JSONB column instead of a top-level column, and accepts the same operators. IGNIS casts the operand automatically when it's a number.
  • A model's settings.defaultFilter merges into every query for that model - see Default filter below.
Filter propertySQL equivalentPurpose
whereWHEREFilter rows by conditions
fieldsSELECT col1, col2Select specific columns
orderORDER BYSort results
limitLIMITRestrict the number of results
skip / offsetOFFSETSkip rows for pagination (aliases - skip wins if both are given)
includeSeparate relational queryEager-load related rows (Relations & Includes)

The where operator families

FamilyOperatorsExample
Comparisoneq, ne/neq, gt, gte, lt, lte{ age: { gte: 18, lte: 65 } }
Null / presenceis, isn, exists, notExists{ deletedAt: null } or { verifiedAt: { exists: true } }
Listin/inq, nin{ status: { inq: ['active', 'pending'] } }
Rangebetween, notBetween{ score: { between: [40, 60] } }
Patternlike, nlike, ilike, nilike, regexp, iregexp{ email: { ilike: '%@company.com' } }
Logicaland, or, not{ or: [{ role: 'admin' }, { role: 'moderator' }] }
Array (PostgreSQL)contains, containedBy, overlaps{ tags: { contains: ['typescript'] } }
JSON pathcomparison, null, list, range, and pattern operators, on a 'column.path' key{ 'metadata.score': { gt: 80 } }

Full operator-by-operator tables, one line each, live on the Quick Reference page.

Fields, order, and pagination

  • fields selects columns - an array, or a { field: true } object (inclusion-only; false is ignored).
  • order takes 'field ASC' / 'field DESC' strings, including JSON paths.
  • limit, when omitted, resolves through query.limit ?? model settings.defaultLimit ?? 10.
  • skip / offset both map to SQL OFFSET.

Default filter

  • Applies automatically. A model's settings.defaultFilter merges into every read, update, and delete for that model.
  • AND-composes on collision. When the default and the caller's filter constrain the same field, IGNIS AND-composes the two conditions instead of one replacing the other.
  • One override escape. Setting that same field to a plain scalar (not an operator object) replaces the default outright - the one intentional opt-out, and it needs no shouldSkipDefaultFilter. The full collision table lives on the Default Filter page.
typescript
import { model, BaseEntity } from '@venizia/ignis';
import { postTable } from '@/schemas';

@model({
  type: 'entity',
  settings: { defaultFilter: { where: { isDeleted: false } } },
})
export class Post extends BaseEntity<typeof Post.schema> {
  static override schema = postTable;
}

await postRepository.find({ filter: { where: { status: 'published' } } });
// WHERE "isDeleted" = false AND "status" = 'published' - different keys, both apply

await postRepository.find({
  filter: { where: { status: 'published' } },
  options: { shouldSkipDefaultFilter: true },
});
// WHERE "status" = 'published' - default filter skipped entirely

Operators

Each operator family and every long-form topic has its own page:

PageCovers
Quick ReferenceEvery operator, one line each - the fast lookup
Comparison Operatorseq, ne/neq, gt, gte, lt, lte
Null Operatorsis, isn, direct null, exists/notExists
List Operatorsin/inq, nin
Range Operatorsbetween, notBetween
Pattern Matchinglike, nlike, ilike, nilike, regexp, iregexp
Logical OperatorsImplicit/explicit and, or, not, empty-group semantics
Array Operatorscontains, containedBy, overlaps (PostgreSQL array columns)
JSON FilteringDot-path queries into JSON/JSONB columns
Fields, Order & Paginationfields, order, limit/skip/offset, defaultLimit
Default Filtersettings.defaultFilter, the collision/narrowing law, shouldSkipDefaultFilter
Application UsageHow a filter flows controller -> service -> repository
Use Case GalleryReal-world filters with the SQL they produce
Tips & Edge CasesPerformance notes and common gotchas

See also

Files: