Filters & Where
Every way to filter your data — from simple equality to complex nested conditions. All filters are type-safe: wrong field names or wrong value types are compile errors.
Two filter APIs
Zanith has two ways to filter data. The object syntax works in CRUD methods (findMany, findFirst, etc.). The field reference syntaxworks in the relational query builder (.query().where()).
Object syntax (CRUD methods)
Direct values
The simplest filter — field equals a value:
TS
5 linesdb.user.findMany({ where: { email: '[email protected]' } })// → WHERE "email" = $1 db.user.findMany({ where: { name: null } })// → WHERE "name" IS NULLComparison operators
TS
7 lines{ age: { equals: 25 } } // = 25{ age: { not: 25 } } // != 25{ age: { gt: 18 } } // > 18{ age: { gte: 18 } } // >= 18{ age: { lt: 65 } } // < 65{ age: { lte: 65 } } // <= 65{ age: { gte: 18, lt: 65 } } // >= 18 AND < 65 (combined)String operators
All string operators use case-insensitive matching (ILIKE):
TS
3 lines{ email: { contains: 'example' } } // ILIKE '%example%'{ name: { startsWith: 'Al' } } // ILIKE 'Al%'{ name: { endsWith: 'son' } } // ILIKE '%son'List operators
TS
2 lines{ id: { in: ['a', 'b', 'c'] } } // IN ($1, $2, $3){ id: { notIn: ['x', 'y'] } } // NOT IN ($1, $2)Null checks
TS
3 lines{ name: null } // IS NULL{ name: { equals: null } } // IS NULL (explicit){ name: { not: null } } // IS NOT NULLLogical combinators
Combine multiple conditions with AND, OR, and NOT:
TS
16 lines// AND — all conditions must match{ AND: [{ age: { gte: 18 } }, { age: { lt: 65 } }] } // OR — any condition can match{ OR: [{ role: 'ADMIN' }, { role: 'MODERATOR' }] } // NOT — negate a condition{ NOT: { role: 'USER' } } // Nested — combine freely{ OR: [ { email: { contains: 'admin' } }, { AND: [{ role: 'ADMIN' }, { active: true }] }, ],}Operator reference
| Operator | SQL output | Works on |
|---|---|---|
equals | = $1 | All types |
not | != $1 | All types |
gt | > $1 | number, string, Date |
gte | >= $1 | number, string, Date |
lt | < $1 | number, string, Date |
lte | <= $1 | number, string, Date |
in | IN ($1, $2, ...) | All types |
notIn | NOT IN ($1, ...) | All types |
contains | ILIKE '%val%' | string |
startsWith | ILIKE 'val%' | string |
endsWith | ILIKE '%val' | string |
Field reference syntax (query builder)
In the relational query builder, you use typed field references instead of objects. These give you autocomplete and let you filter across joined models.
TS
12 linesdb.post.query() .with({ author: true }) .where(({ post, author }) => or( post.title.contains('urgent'), and( post.published.eq(true), author.role.in(['ADMIN', 'MODERATOR']) ) ) ) .execute();Available methods on field references
| Method | SQL | Example |
|---|---|---|
.eq(value) | = $1 | post.published.eq(true) |
.neq(value) | != $1 | post.status.neq('ARCHIVED') |
.gt(value) | > $1 | post.viewCount.gt(100) |
.gte(value) | >= $1 | contract.value.gte(10000) |
.lt(value) | < $1 | user.age.lt(65) |
.lte(value) | <= $1 | user.age.lte(65) |
.contains(str) | ILIKE '%str%' | post.title.contains('guide') |
.startsWith(str) | ILIKE 'str%' | user.name.startsWith('Al') |
.endsWith(str) | ILIKE '%str' | user.email.endsWith('@corp.com') |
.in(values) | IN ($1, $2, ...) | author.role.in(['ADMIN', 'USER']) |
.isNull() | IS NULL | user.name.isNull() |
.isNotNull() | IS NOT NULL | user.name.isNotNull() |
.like(pattern) | LIKE $1 | user.email.like('%@%.com') |
.ilike(pattern) | ILIKE $1 | user.email.ilike('%admin%') |
JSONB methods
| Method | SQL | Example |
|---|---|---|
.jsonText(key) | ->>key = $1 | post.metadata.jsonText('type').eq('article') |
.jsonContains(obj) | @> $1::jsonb | post.metadata.jsonContains({ featured: true }) |
.jsonHasKey(key) | ? $1 | post.metadata.jsonHasKey('tags') |
Array methods
| Method | SQL | Example |
|---|---|---|
.arrayContains(vals) | @> ARRAY[$1, ...] | post.tags.arrayContains(['typescript']) |
Combining with and(), or(), not()
TS
19 linesimport { and, or, not } from 'zanith'; // AND — all must be trueand(post.published.eq(true), author.role.eq('ADMIN')) // OR — any can be trueor(post.title.contains('urgent'), post.viewCount.gt(1000)) // NOT — negatenot(post.status.eq('ARCHIVED')) // Nest freelyor( post.title.contains('urgent'), and( post.published.eq(true), not(author.role.eq('USER')) ))