codegen-db.ts reads your schema files and generates a DbService for every pgTable, exported from @/db. One obvious way to read and write, the same on every table.
Standardised patterns. Zero guesswork.
Where it comes from
DbService lives in the @ship/db package; the generated wiring lives in apps/api/src/db.ts. Add or change a *.schema.ts file, run codegen, and @/db gains a fully typed service for that table:
@/db looks like this — one DbService per table, plus transaction:
Methods
Everydb.<table> exposes the same surface. Reads take a uniform options object; writes return the affected rows.
Inputs and outputs are inferred from the Drizzle table, so
data, where columns and returned rows are all typed against the real schema.
Reads
Pagination
findPage runs the page query and a matching count in parallel and returns { results, count, pagesCount } — the exact shape of listResultSchema(itemSchema) from @/resources/base.schema, so it drops straight into an endpoint’s .output():
Writes
RETURNING, so you rarely need a follow-up read.
The filter API
where is a plain object. A bare value means equality; an object of operators expresses everything else. OR and AND take arrays of nested filters.
A bare
null is shorthand for isNull, which is why soft-delete reads spell it deletedAt: null. OR and AND nest arbitrarily.
Ordering
orderBy maps columns to a direction:
Soft delete
Every table built onbaseColumns (from @/resources/base.schema) carries a deletedAt timestamp:
updateOne that sets deletedAt, and live reads filter deletedAt: null explicitly.
deleteOne / deleteMany are real DELETE statements. Prefer the soft-delete pattern for user-facing data and keep deletedAt: null in your read filters.Relation loading
Passwith or columns to findFirst / find / findPage and the call transparently delegates to Drizzle’s relational query builder, keeping precise inference of the joined result.
apps/api/src/relations.ts. When it exists, codegen-db.ts emits the relations-aware generic — DbService<typeof table, typeof rawDb.query.table> — so with/columns results are fully typed. Without it, the generated service is DbService<typeof table> and plain reads still work.
Mutations emit events
Any resource with ahandlers/ directory is auto-wired so its DbService publishes a typed MutationEvent after each write commits:
<resource>/handlers/*.ts to run side effects without coupling them to the endpoint that caused the change:
<table>.{insert,update,delete}. See the event handlers model for how this keeps writes decoupled from their side effects.
In an endpoint
The data service is the body of most handlers. Compose gates with.use(), declare the contract with .input()/.output(), then call db:
db.transaction.