zanith

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
db.user.findMany({ where: { email: '[email protected]' } })
// → WHERE "email" = $1
 
db.user.findMany({ where: { name: null } })
// → WHERE "name" IS NULL

Comparison operators

TS
{ 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
{ email: { contains: 'example' } } // ILIKE '%example%'
{ name: { startsWith: 'Al' } } // ILIKE 'Al%'
{ name: { endsWith: 'son' } } // ILIKE '%son'

List operators

TS
{ id: { in: ['a', 'b', 'c'] } } // IN ($1, $2, $3)
{ id: { notIn: ['x', 'y'] } } // NOT IN ($1, $2)

Null checks

TS
{ name: null } // IS NULL
{ name: { equals: null } } // IS NULL (explicit)
{ name: { not: null } } // IS NOT NULL

Logical combinators

Combine multiple conditions with AND, OR, and NOT:

TS
// 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

OperatorSQL outputWorks on
equals= $1All types
not!= $1All types
gt> $1number, string, Date
gte>= $1number, string, Date
lt< $1number, string, Date
lte<= $1number, string, Date
inIN ($1, $2, ...)All types
notInNOT IN ($1, ...)All types
containsILIKE '%val%'string
startsWithILIKE 'val%'string
endsWithILIKE '%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
db.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

MethodSQLExample
.eq(value)= $1post.published.eq(true)
.neq(value)!= $1post.status.neq('ARCHIVED')
.gt(value)> $1post.viewCount.gt(100)
.gte(value)>= $1contract.value.gte(10000)
.lt(value)< $1user.age.lt(65)
.lte(value)<= $1user.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 NULLuser.name.isNull()
.isNotNull()IS NOT NULLuser.name.isNotNull()
.like(pattern)LIKE $1user.email.like('%@%.com')
.ilike(pattern)ILIKE $1user.email.ilike('%admin%')

JSONB methods

MethodSQLExample
.jsonText(key)->>key = $1post.metadata.jsonText('type').eq('article')
.jsonContains(obj)@> $1::jsonbpost.metadata.jsonContains({ featured: true })
.jsonHasKey(key)? $1post.metadata.jsonHasKey('tags')

Array methods

MethodSQLExample
.arrayContains(vals)@> ARRAY[$1, ...]post.tags.arrayContains(['typescript'])

Combining with and(), or(), not()

TS
import { and, or, not } from 'zanith';
 
// AND — all must be true
and(post.published.eq(true), author.role.eq('ADMIN'))
 
// OR — any can be true
or(post.title.contains('urgent'), post.viewCount.gt(1000))
 
// NOT — negate
not(post.status.eq('ARCHIVED'))
 
// Nest freely
or(
post.title.contains('urgent'),
and(
post.published.eq(true),
not(author.role.eq('USER'))
)
)