zanith

Advanced Features

Transactions, raw SQL, JSONB operations, window functions, incremental patching, and other advanced capabilities.

Transactions

Zanith provides a unified transaction context. Inside a transaction, you get the same API as the main client — model queries, raw SQL, everything in one atomic operation.

TS
await db.transaction(async (tx) => {
// Create user
const user = await tx.user.create({
data: { email: '[email protected]', name: 'Alice' },
});
 
// Create related records
await tx.post.create({
data: { title: 'First Post', authorId: user.id },
});
 
// Raw SQL inside same transaction
await tx.raw`UPDATE counters SET n = n + 1 WHERE name = 'users'`;
 
// If any operation throws, everything rolls back
});

Raw SQL

When you need full SQL control, use tagged template literals. Values are automatically parameterized — never interpolated into the SQL string.

TS
// Values become $1, $2, etc.
const users = await db.raw<{ id: string; email: string }>`
SELECT id, email FROM users
WHERE created_at > ${startDate}
AND role = ${role}
LIMIT ${limit}
`;
 
// users: Array<{ id: string, email: string }>
 
// Complex queries that don't fit the builder
const report = await db.raw`
WITH monthly AS (
SELECT
DATE_TRUNC('month', created_at) as month,
COUNT(*) as count
FROM posts
WHERE published = true
GROUP BY 1
)
SELECT * FROM monthly ORDER BY month DESC
`;

JSONB operations

PostgreSQL's JSONB is first-class in Zanith. Access nested values, check containment, and test key existence — all through typed field references.

TS
// Text extraction: metadata->>'type' = 'article'
.where(({ post }) => post.metadata.jsonText('type').eq('article'))
 
// JSON path access: metadata->'config' (returns JSON, not text)
.where(({ post }) => post.metadata.jsonPath('config'))
 
// Containment: metadata @> '{"featured": true}'::jsonb
.where(({ post }) => post.metadata.jsonContains({ featured: true }))
 
// Key existence: metadata ? 'tags'
.where(({ post }) => post.metadata.jsonHasKey('tags'))

Incremental schema patching

Instead of rebuilding the entire schema graph when a model changes, Zanith patches only the affected parts. This takes 0.7ms for a single model change.

TS
import { applyPatch } from 'zanith';
 
// Original graph
const graph = compileSchema([User, Post], [Role]);
 
// Patch: add a field to User without rebuilding Post
const patched = applyPatch(graph, {
upsertModels: [updatedUserModel],
});
 
// patched is a NEW graph — original is not mutated
// Post model is untouched

Available patch operations:

OperationEffect
upsertModelsAdd or replace models
removeModelsRemove models (and their relations)
upsertEnumsAdd or replace enums
removeEnumsRemove enums
addRelationsAdd new relation edges
removeRelationsRemove specific relations

Error handling

Zanith normalizes PostgreSQL errors into structured, actionable types.

TS
import { UniqueConstraintError, ForeignKeyError, NotNullError } from 'zanith';
 
try {
await db.user.create({ data: { email: '[email protected]' } });
} catch (err) {
if (err instanceof UniqueConstraintError) {
// err.constraint — constraint name
// err.fields — ['email']
console.log(`Duplicate: ${err.fields.join(', ')}`);
}
if (err instanceof ForeignKeyError) {
// err.constraint, err.detail
}
if (err instanceof NotNullError) {
// err.column
}
}

Query logging

Observe every query Zanith executes for debugging and monitoring.

TS
// The logger interface
interface QueryEvent {
sql: string;
params: unknown[];
durationMs: number;
rowCount: number;
error?: Error;
timestamp: Date;
}
 
// Parameter masking for safe logging
import { maskParams, formatQueryLog } from 'zanith';
 
const event = { sql: 'SELECT ...', params: ['secret'], durationMs: 5, rowCount: 10 };
console.log(formatQueryLog(event));
// [Zanith] OK 5ms | SELECT ... | params: [sec***ret] | rows: 10

Database adapters

Zanith supports two PostgreSQL drivers. Both are optional peer dependencies.

node-postgres (pg)

TS
import { PgAdapter } from 'zanith/adapters/pg';
 
const adapter = new PgAdapter({
connectionString: 'postgresql://user:pass@localhost:5432/mydb',
// OR individual fields:
host: 'localhost',
port: 5432,
database: 'mydb',
user: 'myuser',
password: 'mypass',
pool: { max: 20 },
});

postgres.js

TS
import { PostgresJsAdapter } from 'zanith/adapters/postgres';
 
const adapter = new PostgresJsAdapter({
connectionString: 'postgresql://user:pass@localhost:5432/mydb',
});