Repository
The Repository is the low-level engine that turns entity metadata + the
Query DSL into parameterized SQL. The Dal gateway delegates to
an internal Repository(pool), so dal.findById(...) and repo.findById(...)
are the same call. Use new Repository(client) directly only when you need to
participate in a transaction via dal.withClient() — see
Connections & transactions.
Code
Finders
findById(entity, id, options?)
Find a single row by its @Key() column. id is bigint | string (matches
bigint PKs). Throws NotFoundError by default if no row matches; pass
throwIfNotFound: false to return null instead.
Code
findByUUID(entity, uuid, options?)
Find a single row by its UUID. Assumes the entity has a uuid column (the
convention enforced by IExposableEntity). Same throw/return semantics as
findById.
Code
find(entity, projections, options?)
Find a single row matching the filters. Throws NotFoundError by default if
zero rows match; throws MultipleRowsError if more than one row matches. Pass
throwIfNotFound: false to return null instead of throwing on zero rows.
Code
findAll(entity, projections, options?)
Find all rows matching the filters. Returns TResult[] (empty array if no
matches). Pass stream: true to get an AsyncIterable instead — see
Streaming below.
Code
findByPage(entity, projections, options?)
Paginated find. Returns PaginatedEntity<TEntity> — { entities, total_records }.
total_records is bigint (PostgreSQL COUNT(*) returns bigint). Pass
limit and offset on the options.
Code
count(entity, options?)
Count rows matching the filters. Returns bigint.
Code
FindOptions
| Field | Type | Default | Purpose |
|---|---|---|---|
throwIfNotFound | boolean | true (for find) | Throw NotFoundError on zero rows |
deletedRecords | "EXCLUDED" | "ONLY" | "INCLUDED" | "EXCLUDED" | How to handle soft-deleted rows |
filters | FilterExpr[] | [] | WHERE predicates |
sorting | SortingExpr[] | [] | ORDER BY clauses |
joins | JoinExpr[] | [] | JOIN clauses |
stream | boolean | false | Stream results via pg-query-stream |
tableName | string | entity's table | Override the table name (e.g. for audit tables) |
deletedRecords modes
| Mode | Behavior |
|---|---|
"EXCLUDED" (default) | WHERE deleted_at IS NULL — soft-deleted rows invisible |
"INCLUDED" | No deleted_at filter — all rows returned |
"ONLY" | WHERE deleted_at IS NOT NULL — only soft-deleted rows returned |
Code
Writes
Every write returns the full hydrated row (RETURNING *). For auditable
entities, pass AuditableWriteOptions (which requires actor); for
non-auditable entities, pass plain WriteOptions.
add(entity, values, options?)
Insert a single row. The DAL auto-fills created_at, created_by,
updated_at, updated_by, version for auditable entities — you only pass
business fields.
Code
upsert(entity, values, options?)
Insert or update on conflict. conflictTarget defaults to the @Key() column;
override with UpsertOptions.conflictTarget (e.g. "email" for a natural key).
The version guard applies on the ON CONFLICT (update) branch only — the
pure-INSERT branch starts at version = 1.
Code
update(entity, matchValue, updates, options?)
Update a single row. matchValue is the value of the matchBy column
(defaults to @Key()). For auditable entities, updates must include the
version you read — the version guard runs atomically in the same UPDATE
statement. See Optimistic locking.
Code
delete(entity, matchValue, options?) — soft delete
Soft-delete a row: sets deleted_at and deleted_by, increments version.
The row stays in the table and is excluded from default finders. Returns the
deleted row.
Code
restore(entity, matchValue, options?)
Restore a soft-deleted row: clears deleted_at and deleted_by, increments
version. Returns the restored row.
Code
hardDelete(entity, matchValue, options?)
Permanently delete a row (DELETE FROM ...). The row is gone — no soft-delete
flag, no restore. The version guard still applies (so a concurrent writer can't
hard-delete a row you're about to update). Returns void.
Code
WriteOptions and AuditableWriteOptions
| Field | Type | Required for | Purpose |
|---|---|---|---|
actor | string | auditable entities | Stamped into created_by/updated_by/deleted_by |
audit | AuditPort | optional | If injected, write ops emit field-level audit deltas |
logger | LoggerPort | optional | If injected, audit errors are logged instead of swallowed |
tableName | string | optional | Override the table name (e.g. for audit tables) |
MatchByOptions
| Field | Type | Default | Purpose |
|---|---|---|---|
matchBy | keyof TEntity & string | the @Key() column | Which property to use as the WHERE left operand |
Bulk operations
Bulk ops handle large row counts without hitting PostgreSQL's 65535-parameter
limit. See Architecture: bulk strategies
for the TEMP TABLE strategy used by updateMany.
addMany(entity, rows, options?)
Batched multi-row INSERT. Auto-calculated batch size stays under the parameter limit. Returns all inserted rows.
Code
upsertMany(entity, rows, options?)
Batched INSERT ... ON CONFLICT DO UPDATE. Uses the TEMP TABLE strategy for large batches. Returns all upserted rows.
Code
updateMany(entity, rows, options?)
Batched UPDATE using the TEMP TABLE strategy: CREATE TEMP TABLE, batched
INSERT into it, then UPDATE target SET ... FROM temp WHERE target.id = temp.id.
Atomic and SQL-injection safe. Returns all updated rows.
Code
deleteMany(entity, matchValues, options?)
Bulk soft-delete. matchValues is an array of match values (PK by default).
Returns void.
Code
BulkOptions
| Field | Type | Default | Purpose |
|---|---|---|---|
batchSize | number | auto-calculated | Override the batch size (rarely needed) |
timeoutMs | number | session default | Per-statement timeout in ms (transaction-scoped) |
Clone
clone(entity, sourceUuid, options)
Copy a record by UUID. Fetches the source (including soft-deleted rows),
excludes the PK and @Unique columns, stamps the @CloneField column with the
source UUID, resets audit/deletable fields, and inserts the new row. See
Clone for the full 7-step flow.
Code
Streaming
findAll with stream: true returns an AsyncIterable backed by
pg-query-stream. Each FETCH batch is bounded by the session
statement_timeout — safe for very large result sets that would otherwise
exhaust memory.
Code
The stream is a Node.js async iterator; you can use for await ... of, pipe it
through a transform, or collect it with Array.fromAsync (Node 22+).
Error handling
Every DAL error extends the abstract DalError class, which adds a stable
code: string property. Branch on instanceof or on err.code — both work.
Code
See Architecture: error handling philosophy
for the full code table, and Optimistic locking for the
ERR01/ERR02/ERR03 flow.
Next steps
- Connections & transactions — pool config,
withClient, per-call timeouts, graceful shutdown. - Query DSL — the expressions passed to finders.
- Audit trail — the
AuditPortsystem that hooks into write ops. - Optimistic locking — the version guard on auditable writes.
- Clone — the 7-step clone flow.
- API reference — the full mechanical listing of every exported symbol.