Query DSL
The Query DSL is a composable, type-safe way to express WHERE, ORDER BY,
JOIN, and SELECT clauses. Every expression is a plain object the
Repository resolves into parameterized SQL at query time — no string
interpolation, no SQL injection.
The five building blocks are:
| Building block | Purpose | SQL clause |
|---|---|---|
field(Entity, "prop") | Type-safe column reference | table.column |
Filter.fieldValue / fieldField / raw / group | WHERE predicates | WHERE ... |
Sort.by | Sort direction | ORDER BY ... |
Join.on | Join another entity | JOIN ... ON ... |
Project.field / expr | Select specific columns or expressions | SELECT ... |
field() — type-safe column reference
field(Entity, "property_name") returns a FieldRef — a typed reference to a
column on an entity. The property name is checked at compile time against the
entity's keys, so a typo is a TypeScript error, not a runtime SQL error.
Code
Every Filter, Sort, Join, and Project.field accepts a FieldRef. The
Repository resolves it to the qualified <schema>.<table>.<column> name
using entity metadata at query time.
Filter — WHERE predicates
Filter is a namespace with four factories. Each returns a FilterExpr — a
plain object the query builder turns into a parameterized WHERE clause.
Filter.fieldValue(left, op, right, operand?)
The most common form: compare a column to a literal value. right is bound as
a $n parameter — never interpolated into the SQL string.
Code
Filter.fieldField(left, op, right, operand?)
Compare two columns (useful in joins and self-comparisons).
Code
Filter.raw(left, op, right, operand?)
Escape hatch for raw SQL fragments. Use sparingly — left and right are
emitted as-is into the SQL string, so you are responsible for safety. Prefer
fieldValue whenever the operand is a literal.
Code
Filter.group(filters, operand?)
Group filters with parentheses. The operand controls whether the group is
AND-ed or OR-ed with its siblings.
Code
SqlOperator
The full set of operators accepted by Filter.fieldValue / fieldField /
raw:
| Operator | SQL | Notes |
|---|---|---|
= | = | equality |
!= / <> | != / <> | inequality (both accepted) |
<, <=, >, >= | <, <=, >, >= | comparison |
ILIKE | ILIKE | case-insensitive LIKE (PostgreSQL) |
LIKE | LIKE | case-sensitive LIKE |
IN | IN | right is an array → = ANY($n) |
NOT IN | NOT IN | right is an array → <> ALL($n) |
BETWEEN | BETWEEN | right is [low, high] |
IS / IS NOT | IS / IS NOT | for NULL / TRUE / FALSE |
SqlExpressionOperand
Every filter accepts an optional operand: "AND" | "OR" (default "AND")
that controls how it combines with the previous filter in the array. This is
how you build WHERE a AND (b OR c) without nested groups — though Filter.group
is clearer for that case.
Sort — ORDER BY
Sort.by(field, dir?) returns a SortingExpr. dir defaults to "ASC".
Code
SqlSortDirection is "ASC" | "DESC".
Join — JOIN another entity
Join.on(left, right, type?, options?) returns a JoinExpr. The left is the
joined table's field; right is the base table's field. The argument
order matches standard SQL JOIN left ON left.x = right.y reading order.
Breaking change in 0.1.9: the argument order was swapped from
Join.on(right, left, ...)toJoin.on(left, right, ...). See Changelog.
Code
castRightTo / castLeftTo — type casts in the ON clause
When the two sides of an ON have different PG types (e.g. text vs uuid),
use castRightTo / castLeftTo to add an explicit cast. This is how
buildAuditTrailJoins() safely joins changed_by (text) to user_profile.uuid
(uuid): the cast triggers a regex guardrail that excludes non-UUID values like
"system" rather than throwing a cast error.
Code
SqlJoinType is "INNER" | "LEFT" | "RIGHT".
Project — SELECT specific columns or expressions
By default, finders select every column of the entity. Pass a projections
array to select specific columns or SQL expressions. Each projection becomes a
column in the result row.
Project.field(fieldRef, alias?)
Select a column from the entity (or a joined entity).
Code
Project.expr(expr, alias)
Select a raw SQL expression with an alias. Use for aggregates, function calls, or computed columns.
Code
Putting it all together
A complete query with filters, sorting, a join, and projections:
Code
The generated SQL (simplified):
Code
Next steps
- Repository — pass these expressions to
findAll,findByPage,find,count. - Entities & decorators — the entities that
field()references. - Audit trail —
buildAuditTrailJoins()usesJoin.onwithcastRightTo. - Architecture — how the query builder turns these expressions into SQL.