@murumets-ee/db 0.0.0-preview-20261014110621
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +94 -0
- package/README.md +15 -0
- package/dist/extensions-CEyIHQA7.mjs +2 -0
- package/dist/extensions-CEyIHQA7.mjs.map +1 -0
- package/dist/index.d.mts +1649 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +20 -0
- package/dist/index.mjs.map +1 -0
- package/dist/test-utils.d.mts +105 -0
- package/dist/test-utils.d.mts.map +1 -0
- package/dist/test-utils.mjs +2 -0
- package/dist/test-utils.mjs.map +1 -0
- package/package.json +39 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,1649 @@
|
|
|
1
|
+
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import { Sql } from "postgres";
|
|
3
|
+
import { InferInsertModel, SQL, Table } from "drizzle-orm";
|
|
4
|
+
import { AnyPgColumn, AnyPgColumn as AnyPgColumn$1, PgColumnBuilderBase, PgTable, PgTableWithColumns, TableConfig } from "drizzle-orm/pg-core";
|
|
5
|
+
import { Logger } from "drizzle-orm/logger";
|
|
6
|
+
|
|
7
|
+
//#region src/client.d.ts
|
|
8
|
+
interface DbConfig {
|
|
9
|
+
url: string;
|
|
10
|
+
readOnlyUrl?: string;
|
|
11
|
+
poolMin?: number;
|
|
12
|
+
poolMax?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Per-statement ceiling in milliseconds for the READ-ONLY pool only
|
|
15
|
+
* (default {@link DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS}). Must be a
|
|
16
|
+
* positive integer; `0` and negatives are rejected rather than treated as
|
|
17
|
+
* "disabled" — see {@link createReadOnlyClient}.
|
|
18
|
+
*
|
|
19
|
+
* The read-write pool is deliberately not covered: it serves migrations,
|
|
20
|
+
* bulk imports and admin writes, where a statement running for minutes is
|
|
21
|
+
* legitimate.
|
|
22
|
+
*/
|
|
23
|
+
readOnlyStatementTimeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Default read-only `statement_timeout`, in milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* Calibrated against the one perf-harness scenario that actually exercises
|
|
29
|
+
* this pool: `locale-fallback` drives real content-api reads (which read
|
|
30
|
+
* through `app.db.readOnly`) at ~540 rps with p95 ≈ 55 ms, flat from 1k to
|
|
31
|
+
* 1M rows. 5 s sits ~100× above that.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately NOT cited as justification: the harness's pathological
|
|
34
|
+
* `tickets-per-agent` scenario (11.6 s p95 at 1M rows). It reads through
|
|
35
|
+
* `AdminClient.aggregate` on the READ-WRITE pool, so it cannot reach the pool
|
|
36
|
+
* being capped here — and its p50 at 1M is 5016 ms, i.e. this ceiling would
|
|
37
|
+
* sit at its median rather than safely above it. The harness contains no
|
|
38
|
+
* measurement of a >5 s read-ONLY-pool query, so this default rests on a
|
|
39
|
+
* single scenario plus the ~100× margin, not on a measured bad case.
|
|
40
|
+
*/
|
|
41
|
+
declare const DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS = 5000;
|
|
42
|
+
/**
|
|
43
|
+
* Postgres' own upper bound for `statement_timeout` (a signed 32-bit
|
|
44
|
+
* millisecond count). Anything larger is rejected with SQLSTATE 22023 — but
|
|
45
|
+
* postgres-js connects LAZILY, so an out-of-range value would let the process
|
|
46
|
+
* boot green and then fail EVERY read at connection time, with an error
|
|
47
|
+
* pointing at the connection rather than at config. Rejecting it up front
|
|
48
|
+
* keeps the failure attached to its cause.
|
|
49
|
+
*/
|
|
50
|
+
declare const MAX_STATEMENT_TIMEOUT_MS = 2147483647;
|
|
51
|
+
/**
|
|
52
|
+
* Parse a `readOnlyStatementTimeoutMs` override out of an environment string,
|
|
53
|
+
* returning `undefined` (i.e. "use the default") for anything unusable.
|
|
54
|
+
*
|
|
55
|
+
* This exists so an operator can raise the ceiling on a deployment whose read
|
|
56
|
+
* path is legitimately slower WITHOUT a rebuild and redeploy. That makes it an
|
|
57
|
+
* incident-time tool, which forces two properties:
|
|
58
|
+
*
|
|
59
|
+
* - **It must never throw.** `lumi.config.ts` is evaluated at BUILD time, and a
|
|
60
|
+
* throw there fails the build rather than the request. Passing an unvalidated
|
|
61
|
+
* value straight through would be worse still: `createReadOnlyClient` rejects
|
|
62
|
+
* out-of-range values, so a typo'd env var would abort app construction — an
|
|
63
|
+
* escape hatch that can itself take the process down is not an escape hatch.
|
|
64
|
+
* - **It must not fail silently either.** Someone raising the ceiling mid-incident
|
|
65
|
+
* and getting the 5 s default back with no signal would conclude the knob is
|
|
66
|
+
* broken. Hence the warning: `console` rather than `@murumets-ee/logging`
|
|
67
|
+
* because config is evaluated before any logger exists.
|
|
68
|
+
*/
|
|
69
|
+
declare function parseStatementTimeoutEnv(raw: string | undefined): number | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Create a full read-write database client.
|
|
72
|
+
*
|
|
73
|
+
* Its transactions are tracked (`trackTransactions`), so `onCommit` works on
|
|
74
|
+
* every `tx` it hands out. This is where `app.db.readWrite` and the CLI's
|
|
75
|
+
* client come from, which is what lets `AdminClient` hold its post-commit
|
|
76
|
+
* seams until a caller's transaction actually commits — with no change at any
|
|
77
|
+
* call site that opens one.
|
|
78
|
+
*/
|
|
79
|
+
declare function createDbClient(config: DbConfig): PostgresJsDatabase;
|
|
80
|
+
/**
|
|
81
|
+
* Create a read-only database client.
|
|
82
|
+
*
|
|
83
|
+
* Sets `default_transaction_read_only = on` as a startup parameter on every
|
|
84
|
+
* connection in the pool, so any implicit transaction issued through this
|
|
85
|
+
* client is read-only at the PostgreSQL level. This is the defense-in-depth
|
|
86
|
+
* layer beneath the application's own routing of writes through `AdminClient`
|
|
87
|
+
* — even if a write somehow reaches a read-only client, Postgres rejects it.
|
|
88
|
+
*
|
|
89
|
+
* Also sets `statement_timeout` so a single slow read cannot hold a pool
|
|
90
|
+
* connection indefinitely. The pool defaults to 10 connections, so ten
|
|
91
|
+
* concurrently-slow queries exhaust it and every later read blocks. Rate
|
|
92
|
+
* limiting cannot close this gap: a limiter bounds how many requests ARRIVE,
|
|
93
|
+
* this bounds how long each one HOLDS a connection.
|
|
94
|
+
*
|
|
95
|
+
* Implementation note: postgres-js's `connection` config object is sent in
|
|
96
|
+
* the PostgreSQL startup packet for every new pool connection, so both GUCs
|
|
97
|
+
* are applied uniformly across the whole pool — not just whichever connection
|
|
98
|
+
* happened to run a one-off `SET` statement.
|
|
99
|
+
*
|
|
100
|
+
* @throws if `readOnlyStatementTimeoutMs` is not a positive integer. `0` is
|
|
101
|
+
* rejected rather than accepted as Postgres' "disabled" sentinel, for two
|
|
102
|
+
* reasons: this is an availability control, so a typo'd `0` must not silently
|
|
103
|
+
* remove it; and postgres-js's startup-packet builder filters falsy values
|
|
104
|
+
* out, so a `0` would never reach the server anyway — the connection would
|
|
105
|
+
* quietly inherit whatever server- or role-level default happened to be set.
|
|
106
|
+
* Deployments that need a longer ceiling raise the number.
|
|
107
|
+
*/
|
|
108
|
+
declare function createReadOnlyClient(config: DbConfig): PostgresJsDatabase;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/deploy/advisory-lock.d.ts
|
|
111
|
+
/**
|
|
112
|
+
* An advisory lock's two-part key. Both halves are signed 32-bit integers —
|
|
113
|
+
* Postgres' `(int4, int4)` form, so toolkit locks sit in their own namespace
|
|
114
|
+
* (`space`) and cannot collide with an application's single-`bigint` keys.
|
|
115
|
+
*/
|
|
116
|
+
interface AdvisoryLockKey {
|
|
117
|
+
readonly space: number;
|
|
118
|
+
readonly id: number;
|
|
119
|
+
}
|
|
120
|
+
/** The first half of every toolkit-owned advisory lock key: ASCII `LUMI`. */
|
|
121
|
+
declare const TOOLKIT_ADVISORY_LOCK_SPACE = 1280658761;
|
|
122
|
+
/** One deploy-step runner per database. */
|
|
123
|
+
declare const DEPLOY_STEPS_LOCK: AdvisoryLockKey;
|
|
124
|
+
type AdvisoryLockResult<T> = {
|
|
125
|
+
readonly acquired: true;
|
|
126
|
+
readonly value: T;
|
|
127
|
+
} | {
|
|
128
|
+
readonly acquired: false;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Run `fn` while holding the advisory lock `key`, or return
|
|
132
|
+
* `{ acquired: false }` without running it when someone else holds it.
|
|
133
|
+
*/
|
|
134
|
+
declare function withAdvisoryLock<T>(db: PostgresJsDatabase, key: AdvisoryLockKey, fn: () => Promise<T>): Promise<AdvisoryLockResult<T>>;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/deploy/deploy-steps-table.d.ts
|
|
137
|
+
/**
|
|
138
|
+
* The deploy-step ledger (plan `deploy-drift` D002): one row per ATTEMPT of a
|
|
139
|
+
* deploy step on a site. Nothing here is ever updated or deleted — the current
|
|
140
|
+
* state of a step on a site is its NEWEST row, and the older ones are its
|
|
141
|
+
* history. Resetting a step is a new attempt, never a removed row.
|
|
142
|
+
*
|
|
143
|
+
* A framework table, not a plugin's: every lumi-cms project has deploy steps
|
|
144
|
+
* available and therefore has this table, whichever plugins it loads. It is
|
|
145
|
+
* wired where `entity_refs` is — `createApp`, `buildCliApp` and `lumi migrate`'s
|
|
146
|
+
* schema map — so it reaches the schema snapshot and gets a generated
|
|
147
|
+
* migration like any other table (unlike `_toolkit_migrations`, which is raw
|
|
148
|
+
* SQL outside the snapshots and is not the shape to copy).
|
|
149
|
+
*
|
|
150
|
+
* It lives in `@murumets-ee/db` because the runner that writes it lives in
|
|
151
|
+
* `@murumets-ee/core`, and core owns no tables of its own.
|
|
152
|
+
*/
|
|
153
|
+
/** The table's name — also its key in the migration schema map. */
|
|
154
|
+
declare const DEPLOY_STEPS_TABLE_NAME = "toolkit_deploy_steps";
|
|
155
|
+
/**
|
|
156
|
+
* What an attempt ended as. STORED states only: "waiting" and "waiting for
|
|
157
|
+
* another step" are computed on every read from what is declared and what is
|
|
158
|
+
* recorded, and are never written (a cached pending list is the bug Drupal had).
|
|
159
|
+
*/
|
|
160
|
+
declare const DEPLOY_STEP_STATES: readonly ["applied", "needs-a-person", "marked-done"];
|
|
161
|
+
type DeployStepState = (typeof DEPLOY_STEP_STATES)[number];
|
|
162
|
+
declare const deployStepsTable: {
|
|
163
|
+
table: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
164
|
+
name: string;
|
|
165
|
+
schema: undefined;
|
|
166
|
+
columns: {
|
|
167
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
168
|
+
name: string;
|
|
169
|
+
tableName: string;
|
|
170
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
171
|
+
columnType: string;
|
|
172
|
+
data: unknown;
|
|
173
|
+
driverParam: unknown;
|
|
174
|
+
notNull: false;
|
|
175
|
+
hasDefault: false;
|
|
176
|
+
isPrimaryKey: false;
|
|
177
|
+
isAutoincrement: false;
|
|
178
|
+
hasRuntimeDefault: false;
|
|
179
|
+
enumValues: string[] | undefined;
|
|
180
|
+
baseColumn: never;
|
|
181
|
+
identity: undefined;
|
|
182
|
+
generated: undefined;
|
|
183
|
+
}, {}, {}>;
|
|
184
|
+
};
|
|
185
|
+
dialect: "pg";
|
|
186
|
+
}>;
|
|
187
|
+
schema: TableDefinition<{
|
|
188
|
+
readonly id: ColumnFactory<string, "uuid", true, true>; /** The seeder's `name`. */
|
|
189
|
+
readonly step: ColumnFactory<string, "varchar", true, false>; /** The site handle the attempt was for. */
|
|
190
|
+
readonly site: ColumnFactory<string, "varchar", true, false>; /** One of {@link DEPLOY_STEP_STATES}. */
|
|
191
|
+
readonly state: ColumnFactory<string, "varchar", true, false>; /** Why it needs a person — the step's own words, or the error's message. */
|
|
192
|
+
readonly reason: ColumnFactory<string, "text", false, false>; /** What a person should do about it — the step's own words. */
|
|
193
|
+
readonly whatToDo: ColumnFactory<string, "text", false, false>; /** Rows the step said it created. Null for a mark and for a throw. */
|
|
194
|
+
readonly created: ColumnFactory<number, "integer", false, false>;
|
|
195
|
+
readonly at: ColumnFactory<Date, "timestamp", true, true>;
|
|
196
|
+
/**
|
|
197
|
+
* Who ran it: `deploy`, `cli`, or a user id. A user id is varchar, never
|
|
198
|
+
* uuid — better-auth ids are random strings.
|
|
199
|
+
*/
|
|
200
|
+
readonly by: ColumnFactory<string, "varchar", true, false>; /** That person's name or e-mail as it was then; null for `deploy` / `cli`. */
|
|
201
|
+
readonly byName: ColumnFactory<string, "varchar", false, false>; /** The commit of the build that ran it; null when the build was not told. */
|
|
202
|
+
readonly buildCommit: ColumnFactory<string, "varchar", false, false>; /** The toolkit version that ran it. */
|
|
203
|
+
readonly toolkitVersion: ColumnFactory<string, "varchar", false, false>;
|
|
204
|
+
}>;
|
|
205
|
+
columnKinds: Readonly<Record<string, ColumnKind>>;
|
|
206
|
+
primaryKeyColumns: readonly string[];
|
|
207
|
+
makeClient: (database: import("drizzle-orm/postgres-js").PostgresJsDatabase, partitionValue?: string) => TableClient<{
|
|
208
|
+
readonly id: ColumnFactory<string, "uuid", true, true>; /** The seeder's `name`. */
|
|
209
|
+
readonly step: ColumnFactory<string, "varchar", true, false>; /** The site handle the attempt was for. */
|
|
210
|
+
readonly site: ColumnFactory<string, "varchar", true, false>; /** One of {@link DEPLOY_STEP_STATES}. */
|
|
211
|
+
readonly state: ColumnFactory<string, "varchar", true, false>; /** Why it needs a person — the step's own words, or the error's message. */
|
|
212
|
+
readonly reason: ColumnFactory<string, "text", false, false>; /** What a person should do about it — the step's own words. */
|
|
213
|
+
readonly whatToDo: ColumnFactory<string, "text", false, false>; /** Rows the step said it created. Null for a mark and for a throw. */
|
|
214
|
+
readonly created: ColumnFactory<number, "integer", false, false>;
|
|
215
|
+
readonly at: ColumnFactory<Date, "timestamp", true, true>;
|
|
216
|
+
/**
|
|
217
|
+
* Who ran it: `deploy`, `cli`, or a user id. A user id is varchar, never
|
|
218
|
+
* uuid — better-auth ids are random strings.
|
|
219
|
+
*/
|
|
220
|
+
readonly by: ColumnFactory<string, "varchar", true, false>; /** That person's name or e-mail as it was then; null for `deploy` / `cli`. */
|
|
221
|
+
readonly byName: ColumnFactory<string, "varchar", false, false>; /** The commit of the build that ran it; null when the build was not told. */
|
|
222
|
+
readonly buildCommit: ColumnFactory<string, "varchar", false, false>; /** The toolkit version that ran it. */
|
|
223
|
+
readonly toolkitVersion: ColumnFactory<string, "varchar", false, false>;
|
|
224
|
+
}, import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
225
|
+
name: string;
|
|
226
|
+
schema: undefined;
|
|
227
|
+
columns: {
|
|
228
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
229
|
+
name: string;
|
|
230
|
+
tableName: string;
|
|
231
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
232
|
+
columnType: string;
|
|
233
|
+
data: unknown;
|
|
234
|
+
driverParam: unknown;
|
|
235
|
+
notNull: false;
|
|
236
|
+
hasDefault: false;
|
|
237
|
+
isPrimaryKey: false;
|
|
238
|
+
isAutoincrement: false;
|
|
239
|
+
hasRuntimeDefault: false;
|
|
240
|
+
enumValues: string[] | undefined;
|
|
241
|
+
baseColumn: never;
|
|
242
|
+
identity: undefined;
|
|
243
|
+
generated: undefined;
|
|
244
|
+
}, {}, {}>;
|
|
245
|
+
};
|
|
246
|
+
dialect: "pg";
|
|
247
|
+
}>>;
|
|
248
|
+
partitionColumn: "step" | "site" | "at" | "id" | "state" | "reason" | "whatToDo" | "created" | "by" | "byName" | "buildCommit" | "toolkitVersion" | undefined;
|
|
249
|
+
};
|
|
250
|
+
declare const toolkitDeploySteps: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
251
|
+
name: string;
|
|
252
|
+
schema: undefined;
|
|
253
|
+
columns: {
|
|
254
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
255
|
+
name: string;
|
|
256
|
+
tableName: string;
|
|
257
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
258
|
+
columnType: string;
|
|
259
|
+
data: unknown;
|
|
260
|
+
driverParam: unknown;
|
|
261
|
+
notNull: false;
|
|
262
|
+
hasDefault: false;
|
|
263
|
+
isPrimaryKey: false;
|
|
264
|
+
isAutoincrement: false;
|
|
265
|
+
hasRuntimeDefault: false;
|
|
266
|
+
enumValues: string[] | undefined;
|
|
267
|
+
baseColumn: never;
|
|
268
|
+
identity: undefined;
|
|
269
|
+
generated: undefined;
|
|
270
|
+
}, {}, {}>;
|
|
271
|
+
};
|
|
272
|
+
dialect: "pg";
|
|
273
|
+
}>;
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/extensions.d.ts
|
|
276
|
+
/**
|
|
277
|
+
* Postgres extension helpers — the single source of truth for how the toolkit
|
|
278
|
+
* enables a Postgres extension.
|
|
279
|
+
*
|
|
280
|
+
* Declarative source: plugins declare `server.extensions: ['btree_gist']`
|
|
281
|
+
* (see `@murumets-ee/core`'s `PluginServer`). Consumers — `lumi migrate`
|
|
282
|
+
* (emits into the generated migration) and `@murumets-ee/db`'s `createTestDb`
|
|
283
|
+
* (enables before `pushSchema`) — both route through the helpers here so the
|
|
284
|
+
* validation rule and the emitted SQL are identical everywhere (the migrate
|
|
285
|
+
* dedup-scan relies on the canonical statement being byte-for-byte stable).
|
|
286
|
+
*
|
|
287
|
+
* Why validate the name: `CREATE EXTENSION` cannot parameterize an identifier,
|
|
288
|
+
* so the extension name is interpolated as a literal. Names come from plugin
|
|
289
|
+
* code (not request input), but per the "validate at the boundary" rule we
|
|
290
|
+
* still whitelist the identifier shape so a typo or a hostile plugin can't
|
|
291
|
+
* inject SQL through the one place a raw identifier reaches a DDL string.
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* Valid Postgres extension identifier: a lowercase unquoted identifier —
|
|
295
|
+
* leading letter/underscore, then letters/digits/underscores. Covers every
|
|
296
|
+
* real extension name (`btree_gist`, `pg_trgm`, `uuid-ossp` is the lone
|
|
297
|
+
* hyphenated outlier and is intentionally rejected here; declare it quoted via
|
|
298
|
+
* a custom migration if ever needed). Anchored, so no embedded whitespace,
|
|
299
|
+
* quotes, semicolons, or comment markers can slip through.
|
|
300
|
+
*/
|
|
301
|
+
declare const EXTENSION_NAME_PATTERN: RegExp;
|
|
302
|
+
/**
|
|
303
|
+
* Throw if `name` is not a safe, unquoted Postgres extension identifier.
|
|
304
|
+
* Kept separate from {@link createExtensionStatement} so callers that only
|
|
305
|
+
* want to validate (e.g. a config linter) don't have to build SQL.
|
|
306
|
+
*/
|
|
307
|
+
declare function assertValidExtensionName(name: string): void;
|
|
308
|
+
/**
|
|
309
|
+
* The canonical, idempotent statement that enables one extension.
|
|
310
|
+
* Validates first, then double-quotes the (already-safe) identifier.
|
|
311
|
+
*
|
|
312
|
+
* This exact string is what `lumi migrate` writes into migration files AND
|
|
313
|
+
* what it scans prior migrations for to avoid re-emitting — do not change the
|
|
314
|
+
* shape without updating the dedup scan in `packages/cli/src/commands/migrate.ts`.
|
|
315
|
+
*/
|
|
316
|
+
declare function createExtensionStatement(name: string): string;
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/listen.d.ts
|
|
319
|
+
/**
|
|
320
|
+
* Create a dedicated postgres-js connection sized for `LISTEN/NOTIFY`.
|
|
321
|
+
*
|
|
322
|
+
* The underlying postgres-js Sql instance is intentionally NOT a Drizzle
|
|
323
|
+
* client — `LISTEN`/`NOTIFY` is a low-level Postgres feature that the
|
|
324
|
+
* Drizzle query builder doesn't model. The realtime cross-process bridge
|
|
325
|
+
* (in `@murumets-ee/core/realtime`) holds one of these per process:
|
|
326
|
+
*
|
|
327
|
+
* - `sql.listen(channel, onnotify, onlisten)` opens a dedicated long-lived
|
|
328
|
+
* connection and delivers every `NOTIFY` on the channel to `onnotify`.
|
|
329
|
+
* - `sql.notify(channel, payload)` (or `pg_notify`) issues a notification.
|
|
330
|
+
*
|
|
331
|
+
* Pool size is intentionally small (max 2): one connection is parked on
|
|
332
|
+
* `LISTEN`, leaving headroom for occasional `NOTIFY` issues from the same
|
|
333
|
+
* Sql instance without contending with the rest of the app's pool.
|
|
334
|
+
*/
|
|
335
|
+
declare function createListenClient(config: Pick<DbConfig, 'url'>): Sql;
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/migrate.d.ts
|
|
338
|
+
/**
|
|
339
|
+
* Arguments passed to migration `up` / `down` functions.
|
|
340
|
+
*
|
|
341
|
+
* Intentionally tiny: we hand over a read-write Drizzle handle bound to
|
|
342
|
+
* the current transaction. If a migration needs anything beyond raw SQL
|
|
343
|
+
* (e.g. calling an AdminClient for a data backfill), it can import the
|
|
344
|
+
* relevant toolkit helpers inside the migration file — the CLI loader
|
|
345
|
+
* runs the migration in the same process as the config, so all package
|
|
346
|
+
* imports resolve normally.
|
|
347
|
+
*/
|
|
348
|
+
interface MigrateArgs {
|
|
349
|
+
db: PostgresJsDatabase;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Shape of a migration module loaded from a `.ts` file.
|
|
353
|
+
*
|
|
354
|
+
* At minimum a migration must export `up`. `down` is optional — when
|
|
355
|
+
* absent, `lumi migrate:rollback` refuses to roll that file back.
|
|
356
|
+
*/
|
|
357
|
+
interface MigrationModule {
|
|
358
|
+
up: (args: MigrateArgs) => Promise<void>;
|
|
359
|
+
down?: (args: MigrateArgs) => Promise<void>;
|
|
360
|
+
}
|
|
361
|
+
interface MigrationSource {
|
|
362
|
+
/** Absolute path to the migration file on disk. */
|
|
363
|
+
path: string;
|
|
364
|
+
/** `project` for user migrations, `toolkit` for shipped-with-toolkit ones. */
|
|
365
|
+
namespace: 'toolkit' | 'project';
|
|
366
|
+
/** Filename only, used for ordering + tracking. */
|
|
367
|
+
name: string;
|
|
368
|
+
}
|
|
369
|
+
interface MigrationStatus {
|
|
370
|
+
applied: MigrationSource[];
|
|
371
|
+
pending: MigrationSource[];
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Loader callback — imports a migration `.ts` file and returns its exports.
|
|
375
|
+
*
|
|
376
|
+
* The CLI owns `jiti` (already used to load `lumi.config.ts`), so
|
|
377
|
+
* `@murumets-ee/db` stays free of TS-runtime loader dependencies.
|
|
378
|
+
* Callers who never apply migrations (read-only servers, tests) never
|
|
379
|
+
* pay the loader cost.
|
|
380
|
+
*/
|
|
381
|
+
type MigrationLoader = (absolutePath: string) => Promise<MigrationModule>;
|
|
382
|
+
/** Split discovered migrations into applied vs pending by consulting the tracking table. */
|
|
383
|
+
declare function getMigrationStatus(db: PostgresJsDatabase, projectRoot: string): Promise<MigrationStatus>;
|
|
384
|
+
/**
|
|
385
|
+
* Apply every pending migration in order.
|
|
386
|
+
*
|
|
387
|
+
* Each file runs inside its own transaction: the migration's `up()`
|
|
388
|
+
* executes first, then the `_toolkit_migrations` insert commits. If
|
|
389
|
+
* `up()` throws, the transaction rolls back and the whole command
|
|
390
|
+
* aborts — nothing gets half-applied.
|
|
391
|
+
*
|
|
392
|
+
* The `loader` callback is injected so `@murumets-ee/db` itself doesn't
|
|
393
|
+
* depend on a TS-runtime loader. Callers pass a jiti-backed loader from
|
|
394
|
+
* the CLI or their own equivalent.
|
|
395
|
+
*/
|
|
396
|
+
declare function runMigrations(db: PostgresJsDatabase, projectRoot: string, loader: MigrationLoader): Promise<void>;
|
|
397
|
+
/**
|
|
398
|
+
* Roll back the most recent applied migration (or N most recent).
|
|
399
|
+
*
|
|
400
|
+
* Each file's `down()` runs in its own transaction. Files without a
|
|
401
|
+
* `down` export cause the rollback to abort before running anything —
|
|
402
|
+
* we never roll back some but not all of a requested batch.
|
|
403
|
+
*/
|
|
404
|
+
declare function rollbackMigrations(db: PostgresJsDatabase, projectRoot: string, loader: MigrationLoader, count?: number): Promise<void>;
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/query-counter.d.ts
|
|
407
|
+
/**
|
|
408
|
+
* Per-request DB query counter — opt-in, off by default.
|
|
409
|
+
*
|
|
410
|
+
* Wired into the Drizzle client when `LUMI_DB_QUERY_COUNTER=1`. The harness
|
|
411
|
+
* (apps/perf-harness) wraps each HTTP request in `runWithQueryStats` and emits
|
|
412
|
+
* the count as an `x-db-query-count` response header, so k6 scenarios can
|
|
413
|
+
* assert per-endpoint query budgets like:
|
|
414
|
+
*
|
|
415
|
+
* checks: { 'findById ≤ 2 queries': (r) => r.headers['X-Db-Query-Count'] <= '2' }
|
|
416
|
+
*
|
|
417
|
+
* No-op for any code path that does not enter `runWithQueryStats` — the ALS
|
|
418
|
+
* store is undefined and `logQuery` returns immediately.
|
|
419
|
+
*/
|
|
420
|
+
interface QueryStats {
|
|
421
|
+
count: number;
|
|
422
|
+
}
|
|
423
|
+
declare function getQueryStats(): QueryStats | undefined;
|
|
424
|
+
declare function runWithQueryStats<T>(fn: (stats: QueryStats) => Promise<T>): Promise<T>;
|
|
425
|
+
declare const queryCountingLogger: Logger;
|
|
426
|
+
declare function isQueryCounterEnabled(): boolean;
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/schema-registry.d.ts
|
|
429
|
+
/**
|
|
430
|
+
* Schema registry for storing Drizzle schemas
|
|
431
|
+
* This will be populated by @org/entity when schemas are generated from entity definitions
|
|
432
|
+
*
|
|
433
|
+
* We use PgTableWithColumns<any> because entity table schemas are dynamic (columns
|
|
434
|
+
* not known at compile time) and we need runtime property access (.id, .status, etc.).
|
|
435
|
+
* AnyPgTable won't work here — it doesn't expose column accessors.
|
|
436
|
+
*/
|
|
437
|
+
type AnyTable = PgTableWithColumns<any>;
|
|
438
|
+
interface SchemaRegistry {
|
|
439
|
+
register(name: string, schema: AnyTable): void;
|
|
440
|
+
get(name: string): AnyTable | undefined;
|
|
441
|
+
all(): Record<string, AnyTable>;
|
|
442
|
+
has(name: string): boolean;
|
|
443
|
+
}
|
|
444
|
+
declare class SchemaRegistryImpl implements SchemaRegistry {
|
|
445
|
+
private schemas;
|
|
446
|
+
register(name: string, schema: AnyTable): void;
|
|
447
|
+
get(name: string): AnyTable | undefined;
|
|
448
|
+
all(): Record<string, AnyTable>;
|
|
449
|
+
has(name: string): boolean;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Global schema registry instance
|
|
453
|
+
* Entities will register their schemas here when they are defined
|
|
454
|
+
*/
|
|
455
|
+
declare const schemaRegistry: SchemaRegistryImpl;
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/table/types.d.ts
|
|
458
|
+
/**
|
|
459
|
+
* The discriminating "kind" tag for a column factory.
|
|
460
|
+
*
|
|
461
|
+
* Used at the type level to gate features that only make sense for certain
|
|
462
|
+
* kinds — e.g. only `'jsonb'` columns accept dotted-path keys in the
|
|
463
|
+
* where-builder.
|
|
464
|
+
*/
|
|
465
|
+
type ColumnKind = 'uuid' | 'varchar' | 'text' | 'integer' | 'bigint' | 'double' | 'boolean' | 'timestamp' | 'jsonb' | 'uuidArray';
|
|
466
|
+
/**
|
|
467
|
+
* Internal column factory.
|
|
468
|
+
*
|
|
469
|
+
* A `ColumnFactory` is a callable that, given a column name, produces the
|
|
470
|
+
* corresponding Drizzle column builder. The phantom type parameters
|
|
471
|
+
* (`__type`, `__kind`, `__notNull`) are used purely for compile-time
|
|
472
|
+
* inference of {@link Row} and {@link InsertRow} types — they have no runtime
|
|
473
|
+
* representation beyond a small metadata object.
|
|
474
|
+
*
|
|
475
|
+
* Users do not construct `ColumnFactory` values directly; they use the
|
|
476
|
+
* `column.*` builders from `./columns.ts`.
|
|
477
|
+
*/
|
|
478
|
+
interface ColumnFactory<TType = unknown, TKind extends ColumnKind = ColumnKind, TNotNull extends boolean = boolean, THasDefault extends boolean = boolean> {
|
|
479
|
+
/** Build the underlying Drizzle column for the given column name. */
|
|
480
|
+
(name: string): PgColumnBuilderBase;
|
|
481
|
+
readonly __type: TType;
|
|
482
|
+
readonly __kind: TKind;
|
|
483
|
+
readonly __notNull: TNotNull;
|
|
484
|
+
readonly __hasDefault: THasDefault;
|
|
485
|
+
/**
|
|
486
|
+
* Optional explicit Postgres column name. When set, the column uses this
|
|
487
|
+
* name in the database instead of the JS property name. Needed when porting
|
|
488
|
+
* existing tables that use snake_case column names to `defineTable`.
|
|
489
|
+
*/
|
|
490
|
+
readonly __pgName?: string;
|
|
491
|
+
/** True when the column is declared as a primary key at the column level. */
|
|
492
|
+
readonly __primaryKey?: boolean;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Convenience helper: extract the JS value type for a single column.
|
|
496
|
+
*
|
|
497
|
+
* - `notNull: true` → `T`
|
|
498
|
+
* - `notNull: false` → `T | null`
|
|
499
|
+
*/
|
|
500
|
+
type ColumnValue<C> = C extends ColumnFactory<infer T, ColumnKind, infer N> ? (N extends true ? T : T | null) : never;
|
|
501
|
+
/**
|
|
502
|
+
* Map a columns record to its row shape (the result of a SELECT).
|
|
503
|
+
*/
|
|
504
|
+
type Row<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols]: ColumnValue<TCols[K]> };
|
|
505
|
+
/**
|
|
506
|
+
* Map a columns record to its insert shape.
|
|
507
|
+
*
|
|
508
|
+
* - Required columns: must be supplied unless they have a default
|
|
509
|
+
* - Nullable columns: optional, accept `null`
|
|
510
|
+
* - Columns with defaults (`defaultRandom`, `defaultNow`, explicit `default`): optional
|
|
511
|
+
*/
|
|
512
|
+
type InsertRow<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols as TCols[K] extends ColumnFactory<unknown, ColumnKind, true, false> ? K : never]: ColumnValue<TCols[K]> } & { [K in keyof TCols as TCols[K] extends ColumnFactory<unknown, ColumnKind, true, false> ? never : K]?: ColumnValue<TCols[K]> };
|
|
513
|
+
/**
|
|
514
|
+
* Operator object for a single column.
|
|
515
|
+
*
|
|
516
|
+
* Each property is a comparison operator. Combine with `$and` / `$or` /
|
|
517
|
+
* `$not` at the parent level to build composite predicates.
|
|
518
|
+
*
|
|
519
|
+
* `ilike` and `startsWith` accept user-supplied strings — the table client
|
|
520
|
+
* automatically escapes `%` and `_` in the value before wrapping with
|
|
521
|
+
* pattern wildcards, so callers cannot inject pattern characters by accident.
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* ```ts
|
|
525
|
+
* { status: 'open' } // shorthand for { eq: 'open' }
|
|
526
|
+
* { status: { in: ['open', 'pending'] } }
|
|
527
|
+
* { createdAt: { gte: new Date('2026-01-01') } }
|
|
528
|
+
* { name: { ilike: 'foo%' } } // % escaped from value, only outer wildcards work
|
|
529
|
+
* { description: { isNull: true } }
|
|
530
|
+
* ```
|
|
531
|
+
*/
|
|
532
|
+
type ColumnOperators<T> = T | null | {
|
|
533
|
+
eq: T;
|
|
534
|
+
} | {
|
|
535
|
+
ne: T;
|
|
536
|
+
} | {
|
|
537
|
+
in: T[];
|
|
538
|
+
} | {
|
|
539
|
+
notIn: T[];
|
|
540
|
+
} | {
|
|
541
|
+
gt: T;
|
|
542
|
+
} | {
|
|
543
|
+
gte: T;
|
|
544
|
+
} | {
|
|
545
|
+
lt: T;
|
|
546
|
+
} | {
|
|
547
|
+
lte: T;
|
|
548
|
+
} | {
|
|
549
|
+
isNull: true;
|
|
550
|
+
} | {
|
|
551
|
+
isNotNull: true;
|
|
552
|
+
} | {
|
|
553
|
+
ilike: string;
|
|
554
|
+
} | {
|
|
555
|
+
startsWith: string;
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* The full WhereClause type.
|
|
559
|
+
*
|
|
560
|
+
* Keys are either column names or dotted-path keys for jsonb columns
|
|
561
|
+
* (e.g. `'metadata.author.name'`). Values are operator objects per
|
|
562
|
+
* {@link ColumnOperators}.
|
|
563
|
+
*
|
|
564
|
+
* Combine with `$and` / `$or` / `$not` for composite predicates. All
|
|
565
|
+
* top-level keys are implicitly AND-ed together.
|
|
566
|
+
*
|
|
567
|
+
* The type intentionally accepts `string` for jsonb-path keys rather than
|
|
568
|
+
* a precisely-narrowed template literal, because the path's nested type is
|
|
569
|
+
* unknown to the column definition. Path segments are validated at runtime
|
|
570
|
+
* against `/^[a-zA-Z_][a-zA-Z0-9_-]*$|^\d+$/` and the parent column must
|
|
571
|
+
* be a `jsonb` kind, otherwise the where-builder throws.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```ts
|
|
575
|
+
* // Simple equality
|
|
576
|
+
* { userId: 'u_123', status: 'active' }
|
|
577
|
+
*
|
|
578
|
+
* // Composite with $or
|
|
579
|
+
* { $or: [
|
|
580
|
+
* { status: 'open' },
|
|
581
|
+
* { status: 'pending', assignee: { isNull: true } },
|
|
582
|
+
* ] }
|
|
583
|
+
*
|
|
584
|
+
* // jsonb path access (column 'metadata' must be jsonb)
|
|
585
|
+
* { 'metadata.author.name': { ilike: 'alice%' } }
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
type WhereClause<TCols extends Record<string, ColumnFactory>> = { [K in keyof TCols]?: ColumnOperators<ColumnValue<TCols[K]>> } & {
|
|
589
|
+
[key: `${string}.${string}`]: ColumnOperators<unknown>;
|
|
590
|
+
} & {
|
|
591
|
+
$and?: WhereClause<TCols>[];
|
|
592
|
+
$or?: WhereClause<TCols>[];
|
|
593
|
+
$not?: WhereClause<TCols>;
|
|
594
|
+
};
|
|
595
|
+
/**
|
|
596
|
+
* A partial-index predicate for a table-level index / unique definition.
|
|
597
|
+
*
|
|
598
|
+
* Two forms are accepted:
|
|
599
|
+
*
|
|
600
|
+
* - A plain {@link SQL} fragment (e.g. `sql`deleted_at IS NULL``). Use only
|
|
601
|
+
* when the predicate references columns by their raw Postgres identifier —
|
|
602
|
+
* fragile if the column is stored case-sensitively (Drizzle quotes column
|
|
603
|
+
* identifiers), so prefer the callback form below.
|
|
604
|
+
* - A callback `(columns) => SQL` that receives the table's **resolved**
|
|
605
|
+
* Drizzle column objects (keyed by their JS property name). Interpolating a
|
|
606
|
+
* resolved column into the fragment (`` (c) => sql`${c.isCanonical} = true` ``)
|
|
607
|
+
* always emits the correct quoted identifier regardless of column casing.
|
|
608
|
+
* This is the only place the resolved columns exist — they are produced
|
|
609
|
+
* inside Drizzle's `pgTable(...)` constructor callback, AFTER `defineTable`
|
|
610
|
+
* has been called — so a def-time `SQL` cannot reference them directly.
|
|
611
|
+
*
|
|
612
|
+
* Mirrors the `SchemaFragment.uniqueIndex.where` channel on the
|
|
613
|
+
* `@murumets-ee/entity` behavior `constraints()` API — same drizzle
|
|
614
|
+
* `uniqueIndex().on(...).where(sql)` mechanism, different (infra-table) surface.
|
|
615
|
+
*
|
|
616
|
+
* @remarks Use an INLINE boolean literal (`` sql`${col} = true` ``), NOT
|
|
617
|
+
* `eq(col, true)` — the latter binds a `$1` parameter, which is invalid in
|
|
618
|
+
* DDL ("there is no parameter $1").
|
|
619
|
+
*
|
|
620
|
+
* @remarks The callback param is typed as a loose `Record<string, AnyPgColumn>`
|
|
621
|
+
* rather than the exact `{ [K in keyof TCols]: AnyPgColumn }`. That mapped
|
|
622
|
+
* type would sit in a contravariant (function-argument) position of the
|
|
623
|
+
* object passed to `defineTable<const TCols>`, which collapses `TCols` to the
|
|
624
|
+
* base `Record<string, ColumnFactory>` and silently degrades `Row<TCols>` /
|
|
625
|
+
* `InsertRow<TCols>` / `WhereClause<TCols>` to `unknown`-typed for EVERY table
|
|
626
|
+
* that declares a partial `where` (neither `NoInfer<...>` nor `NoInfer` inside
|
|
627
|
+
* `keyof` prevents the `const`-inference fallback here). The loose param keeps
|
|
628
|
+
* `c.<col>` usable (each is `AnyPgColumn`, exactly what `sql`${col}`` needs)
|
|
629
|
+
* while letting `TCols` be inferred solely from `columns`.
|
|
630
|
+
*/
|
|
631
|
+
type TablePredicate = SQL | ((columns: Record<string, AnyPgColumn$1>) => SQL);
|
|
632
|
+
/**
|
|
633
|
+
* Index definition for a table.
|
|
634
|
+
*
|
|
635
|
+
* Indexes are declared at the table level (not per-column) so composite
|
|
636
|
+
* indexes are first-class. Each index gets an auto-generated name unless
|
|
637
|
+
* `name` is supplied: `idx_<table>_<col1>_<col2>`.
|
|
638
|
+
*/
|
|
639
|
+
interface IndexDefinition<TCols extends Record<string, ColumnFactory>> {
|
|
640
|
+
on: (keyof TCols)[];
|
|
641
|
+
name?: string;
|
|
642
|
+
/**
|
|
643
|
+
* Optional partial-index predicate — emits `CREATE INDEX ... WHERE <pred>`.
|
|
644
|
+
* See {@link TablePredicate}. Omit for a full index (unchanged behavior).
|
|
645
|
+
*/
|
|
646
|
+
where?: TablePredicate;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Unique constraint definition.
|
|
650
|
+
*
|
|
651
|
+
* Composite uniques (multi-column) are declared here. Single-column
|
|
652
|
+
* uniqueness is also supported here rather than as a column-level option,
|
|
653
|
+
* to keep the column builder API uniform.
|
|
654
|
+
*/
|
|
655
|
+
interface UniqueDefinition<TCols extends Record<string, ColumnFactory>> {
|
|
656
|
+
on: (keyof TCols)[];
|
|
657
|
+
/**
|
|
658
|
+
* The constraint (or, with `where`, unique index) name in Postgres.
|
|
659
|
+
*
|
|
660
|
+
* Without it, a plain unique is named by drizzle — `<table>_<db col
|
|
661
|
+
* names>_unique`, using each column's `pgName` when set — and a partial
|
|
662
|
+
* unique is named `uq_<table>_<props>`. Either way `defineTable` throws when
|
|
663
|
+
* the resulting identifier exceeds Postgres's 63-character limit (which
|
|
664
|
+
* would otherwise truncate it silently); supply a shorter `name` then.
|
|
665
|
+
*/
|
|
666
|
+
name?: string;
|
|
667
|
+
/**
|
|
668
|
+
* Optional partial-uniqueness predicate — emits a partial UNIQUE **INDEX**
|
|
669
|
+
* (`CREATE UNIQUE INDEX ... WHERE <pred>`) rather than a `UNIQUE` constraint,
|
|
670
|
+
* because Postgres only supports a predicate on a unique INDEX, never on a
|
|
671
|
+
* table-level `UNIQUE` constraint. When omitted, a plain `UNIQUE` constraint
|
|
672
|
+
* is emitted exactly as before (fully backward compatible). See
|
|
673
|
+
* {@link TablePredicate}.
|
|
674
|
+
*/
|
|
675
|
+
where?: TablePredicate;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Declares that rows in this infrastructure table belong to an entity and
|
|
679
|
+
* should be cleaned up when that entity is deleted — the declarative
|
|
680
|
+
* replacement for hand-coded `db.delete(infraTable).where(...)` in routes.
|
|
681
|
+
*
|
|
682
|
+
* - `cascade` → delete the dependent rows
|
|
683
|
+
* - `set-null` → null the FK column on the dependent rows (column must be nullable)
|
|
684
|
+
*
|
|
685
|
+
* The cleanup runs inside the entity-delete transaction via the table's own
|
|
686
|
+
* TableClient (no raw DB access escapes the typed-client layer).
|
|
687
|
+
*/
|
|
688
|
+
interface TableDependency<TCols extends Record<string, ColumnFactory>> {
|
|
689
|
+
/** Entity name whose deletion triggers this cleanup (e.g. 'ticket'). */
|
|
690
|
+
entity: string;
|
|
691
|
+
/** Column on THIS table holding the entity id. */
|
|
692
|
+
column: keyof TCols;
|
|
693
|
+
/** `restrict` is intentionally unsupported for infra dependencies. */
|
|
694
|
+
onDelete: 'cascade' | 'set-null';
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* The shape passed to `defineTable`.
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* ```ts
|
|
701
|
+
* defineTable({
|
|
702
|
+
* name: 'ticket_read_state',
|
|
703
|
+
* columns: {
|
|
704
|
+
* ticketId: column.uuid({ notNull: true }),
|
|
705
|
+
* userId: column.varchar({ length: 255, notNull: true }),
|
|
706
|
+
* lastReadAt: column.timestamp({ notNull: true, defaultNow: true, withTimezone: true }),
|
|
707
|
+
* },
|
|
708
|
+
* primaryKey: ['ticketId', 'userId'],
|
|
709
|
+
* indexes: [{ on: ['userId'] }],
|
|
710
|
+
* })
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
interface TableDefinition<TCols extends Record<string, ColumnFactory>> {
|
|
714
|
+
/**
|
|
715
|
+
* Postgres table name. Convention: snake_case, prefix with package name
|
|
716
|
+
* (e.g. `toolkit_jobs`, `ticketing_read_state`).
|
|
717
|
+
*
|
|
718
|
+
* Validated at definition time against `/^[a-z][a-z0-9_]*$/` to prevent
|
|
719
|
+
* accidental quoting issues and to enforce a consistent naming scheme.
|
|
720
|
+
*/
|
|
721
|
+
name: string;
|
|
722
|
+
columns: TCols;
|
|
723
|
+
/**
|
|
724
|
+
* Primary key column(s). May be a single column key or an array for
|
|
725
|
+
* composite keys.
|
|
726
|
+
*
|
|
727
|
+
* If omitted, the table has no primary key — only valid for tables that
|
|
728
|
+
* declare a column with `primaryKey: true` at the column level (e.g. via
|
|
729
|
+
* `column.uuid({ primaryKey: true, defaultRandom: true })`).
|
|
730
|
+
*/
|
|
731
|
+
primaryKey?: keyof TCols | (keyof TCols)[];
|
|
732
|
+
unique?: UniqueDefinition<TCols>[];
|
|
733
|
+
indexes?: IndexDefinition<TCols>[];
|
|
734
|
+
/**
|
|
735
|
+
* Declarative cleanup when a referenced entity is deleted. The framework
|
|
736
|
+
* registers each entry and executes it (via TableClient) inside the
|
|
737
|
+
* entity-delete transaction. See {@link TableDependency}.
|
|
738
|
+
*/
|
|
739
|
+
dependsOn?: TableDependency<TCols>[];
|
|
740
|
+
/**
|
|
741
|
+
* A column that CONFINES every client of this table to one value — the
|
|
742
|
+
* site of a site-bound entity's companion table (plan `multisite` D003).
|
|
743
|
+
*
|
|
744
|
+
* When set, `makeClient(db)` refuses to build a client without a partition
|
|
745
|
+
* value, and the client it builds with one:
|
|
746
|
+
* - ANDs `column = value` into every WHERE (reads, updates, deletes, claims,
|
|
747
|
+
* counts, aggregates — including the calls that pass no WHERE at all);
|
|
748
|
+
* - stamps `column = value` on every inserted / upserted row, overwriting
|
|
749
|
+
* whatever the caller passed;
|
|
750
|
+
* - prepends `column` to every upsert / tryClaim conflict target (the
|
|
751
|
+
* table's uniques lead with it);
|
|
752
|
+
* - refuses a patch that tries to change `column`.
|
|
753
|
+
*
|
|
754
|
+
* A partitioned table therefore cannot be read or written unconfined by
|
|
755
|
+
* accident: forgetting the value is an error, not a wider result.
|
|
756
|
+
*/
|
|
757
|
+
partitionColumn?: keyof TCols & string;
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/table/client.d.ts
|
|
761
|
+
/**
|
|
762
|
+
* Default page size for `findMany` when no `limit` is supplied.
|
|
763
|
+
*
|
|
764
|
+
* Chosen as a safe upper bound for "show me a page of stuff" — anything
|
|
765
|
+
* above this should be paginated explicitly.
|
|
766
|
+
*/
|
|
767
|
+
declare const DEFAULT_LIMIT = 100;
|
|
768
|
+
/**
|
|
769
|
+
* Hard cap on `findMany.limit`. Caller-supplied values above this are
|
|
770
|
+
* rejected with `TableClientError` (no clamping).
|
|
771
|
+
*/
|
|
772
|
+
declare const MAX_LIMIT = 1000;
|
|
773
|
+
/**
|
|
774
|
+
* Hard cap on `insertMany.values.length` per single call.
|
|
775
|
+
*/
|
|
776
|
+
declare const MAX_BATCH = 1000;
|
|
777
|
+
/**
|
|
778
|
+
* A custom error thrown by the table client for any caller-side mistake
|
|
779
|
+
* (empty where on update/delete, batch too large, malformed where, …).
|
|
780
|
+
*
|
|
781
|
+
* Database errors are NOT wrapped — they propagate as-is from postgres-js
|
|
782
|
+
* so callers can match on Postgres error codes.
|
|
783
|
+
*/
|
|
784
|
+
declare class TableClientError extends Error {
|
|
785
|
+
constructor(message: string);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Order-by spec accepted by `findMany`.
|
|
789
|
+
*/
|
|
790
|
+
interface OrderBySpec<TCols extends Record<string, ColumnFactory>> {
|
|
791
|
+
column: keyof TCols & string;
|
|
792
|
+
dir?: 'asc' | 'desc';
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* `findMany` options.
|
|
796
|
+
*/
|
|
797
|
+
interface FindManyOptions<TCols extends Record<string, ColumnFactory>> {
|
|
798
|
+
where?: WhereClause<TCols>;
|
|
799
|
+
orderBy?: OrderBySpec<TCols>[];
|
|
800
|
+
limit?: number;
|
|
801
|
+
offset?: number;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Options for the `claim()` atomic locking operation.
|
|
805
|
+
*
|
|
806
|
+
* @see {@link TableClient.claim}
|
|
807
|
+
*/
|
|
808
|
+
interface ClaimOptions<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
|
|
809
|
+
/** Filter for rows eligible to be claimed. Must be non-empty. */
|
|
810
|
+
where: WhereClause<TCols>;
|
|
811
|
+
/** Literal values to SET on claimed rows. */
|
|
812
|
+
set?: Partial<InferInsertModel<TTable>>;
|
|
813
|
+
/** Raw SQL expressions to SET (e.g. `{ attempts: sql\`attempts + 1\` }`). */
|
|
814
|
+
setSql?: Record<string, SQL>;
|
|
815
|
+
/** ORDER BY for the subselect — controls claim priority. */
|
|
816
|
+
orderBy?: OrderBySpec<TCols>[];
|
|
817
|
+
/** Maximum number of rows to claim. Must be 1..MAX_LIMIT. */
|
|
818
|
+
limit: number;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Options for the `tryClaim()` atomic insert-or-replace-if operation.
|
|
822
|
+
*
|
|
823
|
+
* @see {@link TableClient.tryClaim}
|
|
824
|
+
*/
|
|
825
|
+
interface TryClaimOptions<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
|
|
826
|
+
/**
|
|
827
|
+
* Unique-constraint column(s) that should trigger the conflict path.
|
|
828
|
+
*
|
|
829
|
+
* Single string for a single-column unique constraint; array for a
|
|
830
|
+
* composite unique. Must reference a real column on this table.
|
|
831
|
+
*/
|
|
832
|
+
target: (keyof TCols & string) | (keyof TCols & string)[];
|
|
833
|
+
/**
|
|
834
|
+
* Predicate that decides whether an existing conflicting row may be
|
|
835
|
+
* **replaced**. Required and must be non-empty.
|
|
836
|
+
*
|
|
837
|
+
* - On INSERT (no conflict): always wins; predicate is irrelevant.
|
|
838
|
+
* - On CONFLICT + predicate matches: row is replaced; result is
|
|
839
|
+
* `{ acquired: true, row }`.
|
|
840
|
+
* - On CONFLICT + predicate does NOT match: existing row is preserved;
|
|
841
|
+
* result is `{ acquired: false, row }` (where `row` is the surviving
|
|
842
|
+
* existing row).
|
|
843
|
+
*
|
|
844
|
+
* Use `$or` to express composite re-entrancy rules — e.g.
|
|
845
|
+
* `{ $or: [{ expiresAt: { lt: now } }, { lockedBy: callerId }] }` for
|
|
846
|
+
* "replace if expired, or if I already hold it".
|
|
847
|
+
*/
|
|
848
|
+
replaceWhen: WhereClause<TCols>;
|
|
849
|
+
/**
|
|
850
|
+
* Patch applied on the replace path. Defaults to the full `values`
|
|
851
|
+
* payload (last-write-wins on the columns being inserted).
|
|
852
|
+
*/
|
|
853
|
+
set?: Partial<InferInsertModel<TTable>>;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Specification for a single aggregate function.
|
|
857
|
+
*/
|
|
858
|
+
type AggregateSpec<TCols extends Record<string, ColumnFactory>> = {
|
|
859
|
+
fn: 'count';
|
|
860
|
+
} | {
|
|
861
|
+
fn: 'count';
|
|
862
|
+
column: keyof TCols & string;
|
|
863
|
+
} | {
|
|
864
|
+
fn: 'sum' | 'avg' | 'min' | 'max';
|
|
865
|
+
column: keyof TCols & string;
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* Options for the `aggregate()` method.
|
|
869
|
+
*/
|
|
870
|
+
interface AggregateOptions<TCols extends Record<string, ColumnFactory>> {
|
|
871
|
+
/** Named aggregate expressions. Each key becomes an output column. */
|
|
872
|
+
select: Record<string, AggregateSpec<TCols>>;
|
|
873
|
+
/** Columns to GROUP BY. Included automatically in the result. */
|
|
874
|
+
groupBy?: (keyof TCols & string)[];
|
|
875
|
+
/** Filter rows before aggregation (WHERE). */
|
|
876
|
+
where?: WhereClause<TCols>;
|
|
877
|
+
/** Order results. Can reference both grouped columns and aliases. */
|
|
878
|
+
orderBy?: OrderBySpec<TCols>[];
|
|
879
|
+
/** Limit the number of groups returned. */
|
|
880
|
+
limit?: number;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Generic CRUD client. Constructed by `defineTable` — never instantiated
|
|
884
|
+
* directly by callers.
|
|
885
|
+
*
|
|
886
|
+
* The same instance shape is exposed both at the top level (using the
|
|
887
|
+
* package's main `db` connection) and inside a `transaction()` callback
|
|
888
|
+
* (using the transactional `tx` handle), so user code is identical
|
|
889
|
+
* regardless of transactional context.
|
|
890
|
+
*
|
|
891
|
+
* @template TCols The columns record passed to `defineTable`. Drives the
|
|
892
|
+
* where-builder DSL and the `Row<TCols>` / `InsertRow<TCols>` shapes.
|
|
893
|
+
* @template TTable The underlying Drizzle pgTable type, inferred from the
|
|
894
|
+
* table value passed to `new TableClient(...)`. Carrying this through
|
|
895
|
+
* lets `.insert().values(...)`, `.update().set(...)`, and `.returning()`
|
|
896
|
+
* use Drizzle's own `InferInsertModel` / `InferSelectModel` instead of
|
|
897
|
+
* falling back to `as never` casts.
|
|
898
|
+
*/
|
|
899
|
+
declare class TableClient<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTable> {
|
|
900
|
+
/** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
|
|
901
|
+
readonly table: TTable;
|
|
902
|
+
/** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
|
|
903
|
+
private readonly columnKinds;
|
|
904
|
+
/**
|
|
905
|
+
* The active database handle. May be a top-level `PostgresJsDatabase`
|
|
906
|
+
* or a transaction handle of compatible shape — the client doesn't
|
|
907
|
+
* distinguish.
|
|
908
|
+
*/
|
|
909
|
+
private readonly db;
|
|
910
|
+
/** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
|
|
911
|
+
private readonly primaryKeyColumns;
|
|
912
|
+
/**
|
|
913
|
+
* The partition this client is confined to — see
|
|
914
|
+
* `TableDefinition.partitionColumn`. Absent for unpartitioned tables.
|
|
915
|
+
*/
|
|
916
|
+
private readonly partition?;
|
|
917
|
+
/**
|
|
918
|
+
* @internal — instances are created by `defineTable()`.
|
|
919
|
+
*/
|
|
920
|
+
constructor(/** The Drizzle table object. Exposed via `defineTable().table` for typed JOINs. */
|
|
921
|
+
|
|
922
|
+
table: TTable, /** Per-column kind metadata. Used by the where-builder for jsonb path validation. */
|
|
923
|
+
|
|
924
|
+
columnKinds: Readonly<Record<string, ColumnKind>>,
|
|
925
|
+
/**
|
|
926
|
+
* The active database handle. May be a top-level `PostgresJsDatabase`
|
|
927
|
+
* or a transaction handle of compatible shape — the client doesn't
|
|
928
|
+
* distinguish.
|
|
929
|
+
*/
|
|
930
|
+
|
|
931
|
+
db: PostgresJsDatabase, /** Primary key column names. Required by `claim()`. Empty for tables without a declared PK. */
|
|
932
|
+
|
|
933
|
+
primaryKeyColumns?: readonly string[],
|
|
934
|
+
/**
|
|
935
|
+
* The partition this client is confined to — see
|
|
936
|
+
* `TableDefinition.partitionColumn`. Absent for unpartitioned tables.
|
|
937
|
+
*/
|
|
938
|
+
|
|
939
|
+
partition?: {
|
|
940
|
+
readonly key: string;
|
|
941
|
+
readonly value: string;
|
|
942
|
+
} | undefined);
|
|
943
|
+
/** `partition.key = partition.value`, or `undefined` for an unpartitioned client. */
|
|
944
|
+
private partitionCondition;
|
|
945
|
+
/** AND the partition into `condition` (which may be absent). */
|
|
946
|
+
private confine;
|
|
947
|
+
/** Stamp the partition onto a row about to be inserted (overwriting the caller's value). */
|
|
948
|
+
private stamp;
|
|
949
|
+
/** Refuse a patch that would move a row out of its partition. */
|
|
950
|
+
private refusePartitionPatch;
|
|
951
|
+
/** Conflict-target keys with the partition column leading (the table's uniques lead with it). */
|
|
952
|
+
private partitionTarget;
|
|
953
|
+
/**
|
|
954
|
+
* The table typed as the non-generic `PgTable` base.
|
|
955
|
+
*
|
|
956
|
+
* Drizzle's `.from()` (select) uses a conditional type
|
|
957
|
+
* `TableLikeHasEmptySelection<TTable>` that TypeScript cannot evaluate
|
|
958
|
+
* against a bounded generic `TTable extends PgTable` — it can only
|
|
959
|
+
* resolve when the argument is the concrete base `PgTable`. `.insert()`,
|
|
960
|
+
* `.update()`, and `.delete()` have simpler signatures (`<T extends
|
|
961
|
+
* PgTable>(t: T)`) that DO accept the generic directly, so those call
|
|
962
|
+
* sites keep `this.table` and still get `InferInsertModel<TTable>` /
|
|
963
|
+
* `InferSelectModel<TTable>` inference for `.values()`, `.set()`, and
|
|
964
|
+
* `.returning()`.
|
|
965
|
+
*/
|
|
966
|
+
private get _selectTable();
|
|
967
|
+
/**
|
|
968
|
+
* Look up a Drizzle column by its JS property name, throwing
|
|
969
|
+
* `TableClientError` with the supplied `context` if no such column exists.
|
|
970
|
+
* Wraps `getTableColumns(this.table)` — uses Drizzle's typed column map
|
|
971
|
+
* instead of an indexed cast on the table object.
|
|
972
|
+
*/
|
|
973
|
+
private getColumnOrThrow;
|
|
974
|
+
/**
|
|
975
|
+
* Build an array of ORDER BY SQL expressions from an `OrderBySpec[]`.
|
|
976
|
+
* Used by `findMany`, `distinct`, `claim`, and `aggregate`.
|
|
977
|
+
*/
|
|
978
|
+
private buildOrderBy;
|
|
979
|
+
/**
|
|
980
|
+
* Throw `TableClientError` if the where clause has no top-level keys.
|
|
981
|
+
* Used by every method that mutates or reads-by-key (`update`, `delete`,
|
|
982
|
+
* `findOne`, `exists`, `claim`, ...) — none of them have a sane
|
|
983
|
+
* "everything" path, so empty wheres are always a caller mistake.
|
|
984
|
+
*/
|
|
985
|
+
private requireNonEmptyWhere;
|
|
986
|
+
/**
|
|
987
|
+
* Verify every key in `patch` names a real column on this table. Used by
|
|
988
|
+
* `update`, `updateMany`, `upsert`, and `claim` so a typo'd or hostile
|
|
989
|
+
* patch key surfaces a clear error instead of being silently dropped by
|
|
990
|
+
* Drizzle's set logic.
|
|
991
|
+
*/
|
|
992
|
+
private validateColumnKeys;
|
|
993
|
+
/**
|
|
994
|
+
* Find a single row matching the given where clause, or `null` if no row
|
|
995
|
+
* matches. Throws if the where is empty.
|
|
996
|
+
*
|
|
997
|
+
* @example
|
|
998
|
+
* ```ts
|
|
999
|
+
* await readState.client.findOne({ ticketId: 't_1', userId: 'u_1' })
|
|
1000
|
+
* ```
|
|
1001
|
+
*/
|
|
1002
|
+
findOne(where: WhereClause<TCols>): Promise<Row<TCols> | null>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Find multiple rows. Pagination, ordering, and filtering are all
|
|
1005
|
+
* optional but `limit` is hard-capped at {@link MAX_LIMIT}.
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* await jobs.client.findMany({
|
|
1010
|
+
* where: { status: 'pending', runAt: { lte: new Date() } },
|
|
1011
|
+
* orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'runAt', dir: 'asc' }],
|
|
1012
|
+
* limit: 25,
|
|
1013
|
+
* })
|
|
1014
|
+
* ```
|
|
1015
|
+
*/
|
|
1016
|
+
findMany(options?: FindManyOptions<TCols>): Promise<Row<TCols>[]>;
|
|
1017
|
+
/**
|
|
1018
|
+
* Count rows matching the where clause. Returns an exact count via
|
|
1019
|
+
* `SELECT COUNT(*)`. For estimated/cached counts on large tables, see
|
|
1020
|
+
* `countEstimate()` and `countCached()` (added in a follow-up PR).
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* ```ts
|
|
1024
|
+
* const open = await jobs.client.count({ status: 'pending' })
|
|
1025
|
+
* ```
|
|
1026
|
+
*/
|
|
1027
|
+
count(where?: WhereClause<TCols>): Promise<number>;
|
|
1028
|
+
/**
|
|
1029
|
+
* Returns `true` if at least one row matches the where clause. Always
|
|
1030
|
+
* uses `LIMIT 1` and short-circuits — cheaper than `count() > 0` on
|
|
1031
|
+
* large tables.
|
|
1032
|
+
*/
|
|
1033
|
+
exists(where: WhereClause<TCols>): Promise<boolean>;
|
|
1034
|
+
/**
|
|
1035
|
+
* Return unique non-null values for a single column.
|
|
1036
|
+
*
|
|
1037
|
+
* Null values are excluded by default — pass `includeNull: true` to
|
|
1038
|
+
* include them. Results can be filtered with `where` and ordered with
|
|
1039
|
+
* `orderBy` (ascending or descending on the distinct column).
|
|
1040
|
+
*
|
|
1041
|
+
* @example Get distinct entity types from the audit log
|
|
1042
|
+
* ```ts
|
|
1043
|
+
* const types = await audit.client.distinct('entityType', { orderBy: 'asc' })
|
|
1044
|
+
* ```
|
|
1045
|
+
*
|
|
1046
|
+
* @example Distinct with a filter
|
|
1047
|
+
* ```ts
|
|
1048
|
+
* const actions = await audit.client.distinct('action', {
|
|
1049
|
+
* where: { userId: 'u_1' },
|
|
1050
|
+
* orderBy: 'asc',
|
|
1051
|
+
* })
|
|
1052
|
+
* ```
|
|
1053
|
+
*/
|
|
1054
|
+
distinct<K extends keyof TCols & string>(column: K, options?: {
|
|
1055
|
+
where?: WhereClause<TCols>;
|
|
1056
|
+
orderBy?: 'asc' | 'desc'; /** Include null values in the result. Defaults to `false`. */
|
|
1057
|
+
includeNull?: boolean;
|
|
1058
|
+
}): Promise<ColumnValue<TCols[K]>[]>;
|
|
1059
|
+
/**
|
|
1060
|
+
* Insert a single row. Returns the inserted row (server-generated
|
|
1061
|
+
* defaults populated).
|
|
1062
|
+
*/
|
|
1063
|
+
insert(values: InferInsertModel<TTable>): Promise<Row<TCols>>;
|
|
1064
|
+
/**
|
|
1065
|
+
* Upsert: insert a row, or update an existing row if a unique constraint
|
|
1066
|
+
* conflict occurs.
|
|
1067
|
+
*
|
|
1068
|
+
* `target` names the column(s) whose unique constraint should trigger
|
|
1069
|
+
* the conflict path. `set` is the patch applied on conflict — when
|
|
1070
|
+
* omitted, the conflict path applies the same `values` (i.e. "last
|
|
1071
|
+
* write wins").
|
|
1072
|
+
*
|
|
1073
|
+
* `setWhere` adds a conditional WHERE to the conflict update path. If
|
|
1074
|
+
* the condition does not match, the update is skipped and the existing
|
|
1075
|
+
* row is returned unchanged (via a fallback SELECT). This enables
|
|
1076
|
+
* patterns like "only update if the new version is higher than the
|
|
1077
|
+
* existing one".
|
|
1078
|
+
*
|
|
1079
|
+
* @example Idempotent "mark as read" tracking
|
|
1080
|
+
* ```ts
|
|
1081
|
+
* await readState.client.upsert(
|
|
1082
|
+
* { ticketId, userId, lastReadAt: new Date() },
|
|
1083
|
+
* { target: ['ticketId', 'userId'] },
|
|
1084
|
+
* )
|
|
1085
|
+
* ```
|
|
1086
|
+
*
|
|
1087
|
+
* @example Conditional update — only bump if newer
|
|
1088
|
+
* ```ts
|
|
1089
|
+
* await state.client.upsert(
|
|
1090
|
+
* { key: 'sync', version: 5, data: '...' },
|
|
1091
|
+
* { target: 'key', setWhere: { version: { lt: 5 } } },
|
|
1092
|
+
* )
|
|
1093
|
+
* ```
|
|
1094
|
+
*
|
|
1095
|
+
* `mergeJsonb` names jsonb columns whose conflict path MERGES instead of
|
|
1096
|
+
* replacing: the stored object becomes `stored || incoming` — a top-level,
|
|
1097
|
+
* right-wins merge, evaluated by Postgres inside the one `ON CONFLICT`
|
|
1098
|
+
* statement. Use it when two writers may each own part of one jsonb object and
|
|
1099
|
+
* a read-then-write would let the later writer erase the earlier one's keys.
|
|
1100
|
+
* The insert path is unaffected (there is nothing to merge with). A stored SQL
|
|
1101
|
+
* NULL or JSON `null` merges as `{}`. The incoming value must be a JSON object
|
|
1102
|
+
* — `||` concatenates arrays and scalars instead of merging — and a key named
|
|
1103
|
+
* in both `set` and `mergeJsonb` is merged (the merge wins).
|
|
1104
|
+
*
|
|
1105
|
+
* @example Merge a partial patch into a staged document
|
|
1106
|
+
* ```ts
|
|
1107
|
+
* await drafts.client.upsert(
|
|
1108
|
+
* { entityId, locale, data: { title: 'New' }, updatedAt: now },
|
|
1109
|
+
* { target: ['entityId', 'locale'], set: { updatedAt: now }, mergeJsonb: ['data'] },
|
|
1110
|
+
* )
|
|
1111
|
+
* ```
|
|
1112
|
+
*/
|
|
1113
|
+
upsert(values: InferInsertModel<TTable>, opts: {
|
|
1114
|
+
target: (keyof TCols & string) | (keyof TCols & string)[];
|
|
1115
|
+
set?: Partial<InferInsertModel<TTable>>; /** Conditional WHERE on the conflict update path. Empty objects are ignored. */
|
|
1116
|
+
setWhere?: WhereClause<TCols>; /** jsonb columns merged (`stored || incoming`) rather than replaced on conflict. */
|
|
1117
|
+
mergeJsonb?: readonly (keyof TCols & string)[];
|
|
1118
|
+
}): Promise<Row<TCols>>;
|
|
1119
|
+
/**
|
|
1120
|
+
* Atomically claim a row by either inserting it or replacing an existing
|
|
1121
|
+
* conflicting row that satisfies `replaceWhen`. The single SQL statement
|
|
1122
|
+
* makes this race-free — concurrent callers cannot both observe an empty
|
|
1123
|
+
* slot and both INSERT.
|
|
1124
|
+
*
|
|
1125
|
+
* Distinct from {@link upsert}:
|
|
1126
|
+
* - `upsert` always wins on conflict (or hits its `setWhere` block and
|
|
1127
|
+
* silently falls back to the existing row). It returns a single
|
|
1128
|
+
* `Row<TCols>` and the caller cannot tell which path fired.
|
|
1129
|
+
* - `tryClaim` returns a discriminated union — the caller knows whether
|
|
1130
|
+
* they took the slot or someone else still holds it.
|
|
1131
|
+
*
|
|
1132
|
+
* Use case: leasing/locking primitives where the caller must distinguish
|
|
1133
|
+
* "I now hold this lock" from "someone else holds it, here is who".
|
|
1134
|
+
*
|
|
1135
|
+
* @example Acquire a lock, replacing only if expired
|
|
1136
|
+
* ```ts
|
|
1137
|
+
* const result = await locks.tryClaim(
|
|
1138
|
+
* { entityId, lockedBy: me, expiresAt: in5Min },
|
|
1139
|
+
* {
|
|
1140
|
+
* target: 'entityId',
|
|
1141
|
+
* replaceWhen: { expiresAt: { lt: now } },
|
|
1142
|
+
* },
|
|
1143
|
+
* )
|
|
1144
|
+
* if (result.acquired) {
|
|
1145
|
+
* // I have the lock
|
|
1146
|
+
* } else {
|
|
1147
|
+
* // result.row.lockedBy holds it (and lock is fresh)
|
|
1148
|
+
* }
|
|
1149
|
+
* ```
|
|
1150
|
+
*
|
|
1151
|
+
* @example Re-entrant lock (claim if expired, or if I already hold it)
|
|
1152
|
+
* ```ts
|
|
1153
|
+
* await locks.tryClaim(
|
|
1154
|
+
* { entityId, lockedBy: me, expiresAt: in5Min },
|
|
1155
|
+
* {
|
|
1156
|
+
* target: 'entityId',
|
|
1157
|
+
* replaceWhen: { $or: [{ expiresAt: { lt: now } }, { lockedBy: me }] },
|
|
1158
|
+
* },
|
|
1159
|
+
* )
|
|
1160
|
+
* ```
|
|
1161
|
+
*/
|
|
1162
|
+
tryClaim(values: InferInsertModel<TTable>, opts: TryClaimOptions<TCols, TTable>): Promise<{
|
|
1163
|
+
acquired: true;
|
|
1164
|
+
row: Row<TCols>;
|
|
1165
|
+
} | {
|
|
1166
|
+
acquired: false;
|
|
1167
|
+
row: Row<TCols>;
|
|
1168
|
+
}>;
|
|
1169
|
+
/**
|
|
1170
|
+
* Insert many rows in a single statement. Capped at {@link MAX_BATCH}.
|
|
1171
|
+
* Returns all inserted rows in input order.
|
|
1172
|
+
*/
|
|
1173
|
+
insertMany(values: InferInsertModel<TTable>[]): Promise<Row<TCols>[]>;
|
|
1174
|
+
/**
|
|
1175
|
+
* Update **at most one** row matching the where clause. Returns the
|
|
1176
|
+
* updated row, or `null` if no row matched.
|
|
1177
|
+
*
|
|
1178
|
+
* Throws `TableClientError` if:
|
|
1179
|
+
* - the where is empty
|
|
1180
|
+
* - the where matches more than one row
|
|
1181
|
+
*
|
|
1182
|
+
* The multi-row guard exists to prevent silent bulk updates from a
|
|
1183
|
+
* mistakenly-broad where. If you genuinely want to update multiple
|
|
1184
|
+
* rows in one call, use {@link updateMany}, which is explicit about
|
|
1185
|
+
* its plurality.
|
|
1186
|
+
*
|
|
1187
|
+
* Implementation note: this issues a single `UPDATE … RETURNING` and
|
|
1188
|
+
* inspects the returned row count *after* the fact. The extra rows
|
|
1189
|
+
* over the wire are the cost of the safety check; the alternative
|
|
1190
|
+
* (a SELECT round-trip first) would be more rows AND a race window.
|
|
1191
|
+
* If the affected count is > 1 the update has *already* happened —
|
|
1192
|
+
* the throw is to surface the bug, not to prevent it. Wrap in a
|
|
1193
|
+
* transaction if you need atomic rollback.
|
|
1194
|
+
*/
|
|
1195
|
+
update(where: WhereClause<TCols>, patch: Partial<InferInsertModel<TTable>>): Promise<Row<TCols> | null>;
|
|
1196
|
+
/**
|
|
1197
|
+
* Update all rows matching the where clause. Returns every updated row.
|
|
1198
|
+
* Throws if the where is empty — there is no "update everything" path.
|
|
1199
|
+
*/
|
|
1200
|
+
updateMany(where: WhereClause<TCols>, patch: Partial<InferInsertModel<TTable>>): Promise<Row<TCols>[]>;
|
|
1201
|
+
/**
|
|
1202
|
+
* Delete **at most one** row matching the where clause. Returns the
|
|
1203
|
+
* deleted row, or `null` if no row matched.
|
|
1204
|
+
*
|
|
1205
|
+
* Throws `TableClientError` if:
|
|
1206
|
+
* - the where is empty
|
|
1207
|
+
* - the where matches more than one row
|
|
1208
|
+
*
|
|
1209
|
+
* Same multi-row safety guard as {@link update}. Use {@link deleteMany}
|
|
1210
|
+
* for explicit bulk deletes.
|
|
1211
|
+
*
|
|
1212
|
+
* Implementation note: same as `update` — the delete has already
|
|
1213
|
+
* happened by the time the throw fires. Wrap in a transaction if you
|
|
1214
|
+
* need atomic rollback.
|
|
1215
|
+
*/
|
|
1216
|
+
delete(where: WhereClause<TCols>): Promise<Row<TCols> | null>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Delete all rows matching the where clause. Returns every deleted row.
|
|
1219
|
+
* Throws if the where is empty — there is no "delete everything" path.
|
|
1220
|
+
*/
|
|
1221
|
+
deleteMany(where: WhereClause<TCols>): Promise<Row<TCols>[]>;
|
|
1222
|
+
/**
|
|
1223
|
+
* Run a callback inside a database transaction. The callback receives a
|
|
1224
|
+
* transactional `TableClient` instance with the same shape as the
|
|
1225
|
+
* top-level client — write code identically inside or outside.
|
|
1226
|
+
*
|
|
1227
|
+
* Throwing from the callback rolls back; returning a value commits.
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```ts
|
|
1231
|
+
* await files.client.transaction(async (tx) => {
|
|
1232
|
+
* const row = await tx.insert(values)
|
|
1233
|
+
* await adapter.upload(row.key, buffer) // if this throws, the row is rolled back
|
|
1234
|
+
* return row
|
|
1235
|
+
* })
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
transaction<T>(fn: (tx: TableClient<TCols, TTable>) => Promise<T>): Promise<T>;
|
|
1239
|
+
/**
|
|
1240
|
+
* Atomically claim rows using `FOR UPDATE SKIP LOCKED`.
|
|
1241
|
+
*
|
|
1242
|
+
* Selects rows matching `where`, locks them (skipping any already locked
|
|
1243
|
+
* by another worker), updates them with `set`/`setSql`, and returns the
|
|
1244
|
+
* claimed rows in a single atomic operation. This is the standard pattern
|
|
1245
|
+
* for job queues, task distribution, and any producer-consumer table.
|
|
1246
|
+
*
|
|
1247
|
+
* Requires the table to have a declared primary key (either via
|
|
1248
|
+
* `column.uuid({ primaryKey: true })` or `defineTable({ primaryKey })`.
|
|
1249
|
+
*
|
|
1250
|
+
* @example Job queue claim
|
|
1251
|
+
* ```ts
|
|
1252
|
+
* const jobs = await client.claim({
|
|
1253
|
+
* where: { status: 'pending', runAt: { lte: new Date() } },
|
|
1254
|
+
* set: { status: 'processing', lockedBy: workerId, lockedAt: new Date() },
|
|
1255
|
+
* setSql: { attempts: sql`${jobsTable.table.attempts} + 1` },
|
|
1256
|
+
* orderBy: [{ column: 'priority', dir: 'desc' }, { column: 'createdAt', dir: 'asc' }],
|
|
1257
|
+
* limit: 5,
|
|
1258
|
+
* })
|
|
1259
|
+
* ```
|
|
1260
|
+
*/
|
|
1261
|
+
claim(options: ClaimOptions<TCols, TTable>): Promise<Row<TCols>[]>;
|
|
1262
|
+
/**
|
|
1263
|
+
* Run an aggregate query with GROUP BY.
|
|
1264
|
+
*
|
|
1265
|
+
* Each key in `select` is an output alias mapped to an aggregate
|
|
1266
|
+
* specification. The `groupBy` columns are included automatically in
|
|
1267
|
+
* the result. Returns typed rows with grouped column values plus the
|
|
1268
|
+
* computed aggregates.
|
|
1269
|
+
*
|
|
1270
|
+
* @example Count jobs by status
|
|
1271
|
+
* ```ts
|
|
1272
|
+
* const rows = await client.aggregate({
|
|
1273
|
+
* select: { count: { fn: 'count' } },
|
|
1274
|
+
* groupBy: ['status'],
|
|
1275
|
+
* })
|
|
1276
|
+
* // → [{ status: 'pending', count: 12 }, { status: 'completed', count: 45 }]
|
|
1277
|
+
* ```
|
|
1278
|
+
*
|
|
1279
|
+
* @example Average priority of open jobs
|
|
1280
|
+
* ```ts
|
|
1281
|
+
* const [row] = await client.aggregate({
|
|
1282
|
+
* select: { avgPriority: { fn: 'avg', column: 'priority' } },
|
|
1283
|
+
* where: { status: 'open' },
|
|
1284
|
+
* })
|
|
1285
|
+
* ```
|
|
1286
|
+
*/
|
|
1287
|
+
aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(options: AggregateOptions<TCols>): Promise<TResult[]>;
|
|
1288
|
+
/**
|
|
1289
|
+
* Fast approximate row count using Postgres `reltuples` statistics.
|
|
1290
|
+
*
|
|
1291
|
+
* Returns the estimated row count from `pg_class` — updated by
|
|
1292
|
+
* `ANALYZE` and autovacuum. Much faster than `COUNT(*)` on large
|
|
1293
|
+
* tables (no sequential scan), but may be stale by up to a few
|
|
1294
|
+
* percent. Returns 0 for tables that have never been analyzed.
|
|
1295
|
+
*
|
|
1296
|
+
* Use `count()` when you need an exact number. Use `countEstimate()`
|
|
1297
|
+
* for UI display, pagination hints, or "is this table large?" checks
|
|
1298
|
+
* where ±5% accuracy is fine.
|
|
1299
|
+
*
|
|
1300
|
+
* @example
|
|
1301
|
+
* ```ts
|
|
1302
|
+
* const approx = await client.countEstimate()
|
|
1303
|
+
* if (approx > 100_000) {
|
|
1304
|
+
* // Switch to keyset pagination instead of offset
|
|
1305
|
+
* }
|
|
1306
|
+
* ```
|
|
1307
|
+
*/
|
|
1308
|
+
countEstimate(): Promise<number>;
|
|
1309
|
+
/** Build a single aggregate SQL expression from a spec. */
|
|
1310
|
+
private buildAggregateExpr;
|
|
1311
|
+
private buildWhereOrThrow;
|
|
1312
|
+
private validateLimit;
|
|
1313
|
+
}
|
|
1314
|
+
//#endregion
|
|
1315
|
+
//#region src/table/columns.d.ts
|
|
1316
|
+
/**
|
|
1317
|
+
* Type interface for the `column` builder namespace.
|
|
1318
|
+
*
|
|
1319
|
+
* Overloads discriminate on whether `default` is present, so
|
|
1320
|
+
* `THasDefault` is inferred correctly:
|
|
1321
|
+
*
|
|
1322
|
+
* - `column.varchar({ length: 20, default: 'x' })` → `hasDefault = true`
|
|
1323
|
+
* - `column.varchar({ length: 20 })` → `hasDefault = false`
|
|
1324
|
+
*
|
|
1325
|
+
* Object-literal methods don't support overloads in TypeScript, so we
|
|
1326
|
+
* declare the overloaded signatures on an interface and cast the
|
|
1327
|
+
* implementation object.
|
|
1328
|
+
*/
|
|
1329
|
+
interface ColumnBuilders {
|
|
1330
|
+
uuid<TPrimaryKey extends boolean = false, TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
|
|
1331
|
+
primaryKey?: TPrimaryKey;
|
|
1332
|
+
notNull?: TNotNull;
|
|
1333
|
+
defaultRandom?: THasDefault;
|
|
1334
|
+
pgName?: string;
|
|
1335
|
+
}): ColumnFactory<string, 'uuid', TPrimaryKey extends true ? true : TNotNull extends true ? true : false, TPrimaryKey extends true ? true : THasDefault extends true ? true : false>;
|
|
1336
|
+
varchar<TNotNull extends boolean = false>(opts: {
|
|
1337
|
+
length: number;
|
|
1338
|
+
notNull?: TNotNull;
|
|
1339
|
+
default: string;
|
|
1340
|
+
pgName?: string;
|
|
1341
|
+
}): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, true>;
|
|
1342
|
+
varchar<TNotNull extends boolean = false>(opts: {
|
|
1343
|
+
length: number;
|
|
1344
|
+
notNull?: TNotNull;
|
|
1345
|
+
pgName?: string;
|
|
1346
|
+
}): ColumnFactory<string, 'varchar', TNotNull extends true ? true : false, false>;
|
|
1347
|
+
text<TNotNull extends boolean = false>(opts: {
|
|
1348
|
+
notNull?: TNotNull;
|
|
1349
|
+
default: string;
|
|
1350
|
+
pgName?: string;
|
|
1351
|
+
}): ColumnFactory<string, 'text', TNotNull extends true ? true : false, true>;
|
|
1352
|
+
text<TNotNull extends boolean = false>(opts?: {
|
|
1353
|
+
notNull?: TNotNull;
|
|
1354
|
+
pgName?: string;
|
|
1355
|
+
}): ColumnFactory<string, 'text', TNotNull extends true ? true : false, false>;
|
|
1356
|
+
integer<TNotNull extends boolean = false>(opts: {
|
|
1357
|
+
notNull?: TNotNull;
|
|
1358
|
+
default: number;
|
|
1359
|
+
pgName?: string;
|
|
1360
|
+
}): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, true>;
|
|
1361
|
+
integer<TNotNull extends boolean = false>(opts?: {
|
|
1362
|
+
notNull?: TNotNull;
|
|
1363
|
+
pgName?: string;
|
|
1364
|
+
}): ColumnFactory<number, 'integer', TNotNull extends true ? true : false, false>;
|
|
1365
|
+
bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts: {
|
|
1366
|
+
mode?: TMode;
|
|
1367
|
+
notNull?: TNotNull;
|
|
1368
|
+
default: TMode extends 'bigint' ? bigint : number;
|
|
1369
|
+
pgName?: string;
|
|
1370
|
+
}): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, true>;
|
|
1371
|
+
bigint<TMode extends 'number' | 'bigint' = 'number', TNotNull extends boolean = false>(opts?: {
|
|
1372
|
+
mode?: TMode;
|
|
1373
|
+
notNull?: TNotNull;
|
|
1374
|
+
pgName?: string;
|
|
1375
|
+
}): ColumnFactory<TMode extends 'bigint' ? bigint : number, 'bigint', TNotNull extends true ? true : false, false>;
|
|
1376
|
+
double<TNotNull extends boolean = false>(opts: {
|
|
1377
|
+
notNull?: TNotNull;
|
|
1378
|
+
default: number;
|
|
1379
|
+
pgName?: string;
|
|
1380
|
+
}): ColumnFactory<number, 'double', TNotNull extends true ? true : false, true>;
|
|
1381
|
+
double<TNotNull extends boolean = false>(opts?: {
|
|
1382
|
+
notNull?: TNotNull;
|
|
1383
|
+
pgName?: string;
|
|
1384
|
+
}): ColumnFactory<number, 'double', TNotNull extends true ? true : false, false>;
|
|
1385
|
+
boolean<TNotNull extends boolean = false>(opts: {
|
|
1386
|
+
notNull?: TNotNull;
|
|
1387
|
+
default: boolean;
|
|
1388
|
+
pgName?: string;
|
|
1389
|
+
}): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, true>;
|
|
1390
|
+
boolean<TNotNull extends boolean = false>(opts?: {
|
|
1391
|
+
notNull?: TNotNull;
|
|
1392
|
+
pgName?: string;
|
|
1393
|
+
}): ColumnFactory<boolean, 'boolean', TNotNull extends true ? true : false, false>;
|
|
1394
|
+
timestamp<TNotNull extends boolean = false, THasDefault extends boolean = false>(opts?: {
|
|
1395
|
+
notNull?: TNotNull;
|
|
1396
|
+
defaultNow?: THasDefault;
|
|
1397
|
+
withTimezone?: boolean;
|
|
1398
|
+
pgName?: string;
|
|
1399
|
+
}): ColumnFactory<Date, 'timestamp', TNotNull extends true ? true : false, THasDefault extends true ? true : false>;
|
|
1400
|
+
jsonb<T = unknown, TNotNull extends boolean = false>(opts: {
|
|
1401
|
+
notNull?: TNotNull;
|
|
1402
|
+
default: T;
|
|
1403
|
+
pgName?: string;
|
|
1404
|
+
}): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, true>;
|
|
1405
|
+
jsonb<T = unknown, TNotNull extends boolean = false>(opts?: {
|
|
1406
|
+
notNull?: TNotNull;
|
|
1407
|
+
pgName?: string;
|
|
1408
|
+
}): ColumnFactory<T, 'jsonb', TNotNull extends true ? true : false, false>;
|
|
1409
|
+
uuidArray<TNotNull extends boolean = false>(opts?: {
|
|
1410
|
+
notNull?: TNotNull;
|
|
1411
|
+
pgName?: string;
|
|
1412
|
+
}): ColumnFactory<string[], 'uuidArray', TNotNull extends true ? true : false, false>;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Column builder namespace — overloaded interface applied via single
|
|
1416
|
+
* assertion. Each builder's implementation returns `makeFactory(...)` with
|
|
1417
|
+
* widened boolean params; the `ColumnBuilders` overloads narrow them for
|
|
1418
|
+
* callers based on whether `default`/`primaryKey` etc. are present.
|
|
1419
|
+
*/
|
|
1420
|
+
declare const column: ColumnBuilders;
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/table/define.d.ts
|
|
1423
|
+
/**
|
|
1424
|
+
* The result of `defineTable()`.
|
|
1425
|
+
*
|
|
1426
|
+
* @template TCols The columns record passed to `defineTable`.
|
|
1427
|
+
* @template TTable The concrete Drizzle `pgTable` type produced by the
|
|
1428
|
+
* definition. Defaulted to the non-generic `PgTable` for callers that
|
|
1429
|
+
* pass `DefinedTable` around without propagating inference — inside
|
|
1430
|
+
* `defineTable` itself, the actual `typeof table` is captured so
|
|
1431
|
+
* `makeClient` returns a fully-typed `TableClient<TCols, typeof table>`.
|
|
1432
|
+
*/
|
|
1433
|
+
interface DefinedTable<TCols extends Record<string, ColumnFactory>, TTable extends PgTable = PgTableWithColumns<TableConfig>> {
|
|
1434
|
+
/**
|
|
1435
|
+
* The underlying Drizzle `pgTable`. Exposed so callers can use it in
|
|
1436
|
+
* typed JOINs (the one sanctioned escape valve), and so it can be
|
|
1437
|
+
* included in `Plugin.tables` for migration discovery by `lumi migrate`.
|
|
1438
|
+
*/
|
|
1439
|
+
table: TTable;
|
|
1440
|
+
/**
|
|
1441
|
+
* The original schema metadata. Useful for introspection / tooling
|
|
1442
|
+
* that needs to know about indexes, unique constraints, etc.
|
|
1443
|
+
*/
|
|
1444
|
+
schema: TableDefinition<TCols>;
|
|
1445
|
+
/**
|
|
1446
|
+
* Per-column kind metadata (column name → kind tag). Used internally
|
|
1447
|
+
* by the where-builder for jsonb path validation.
|
|
1448
|
+
*/
|
|
1449
|
+
columnKinds: Readonly<Record<string, ColumnKind>>;
|
|
1450
|
+
/**
|
|
1451
|
+
* Primary key column names (JS property names). Detected from either
|
|
1452
|
+
* `schema.primaryKey` or column-level `{ primaryKey: true }`. Empty
|
|
1453
|
+
* array if no PK is declared. Required by `claim()`.
|
|
1454
|
+
*/
|
|
1455
|
+
primaryKeyColumns: readonly string[];
|
|
1456
|
+
/**
|
|
1457
|
+
* Build a typed CRUD client wired to the given database handle.
|
|
1458
|
+
*
|
|
1459
|
+
* Callers typically construct one client per logical "module" and
|
|
1460
|
+
* cache it. The client is stateless aside from the db handle, so it
|
|
1461
|
+
* is safe to share.
|
|
1462
|
+
*/
|
|
1463
|
+
makeClient(db: PostgresJsDatabase, partitionValue?: string): TableClient<TCols, TTable>;
|
|
1464
|
+
/** The confining column, when the table declares one (`partitionColumn`). */
|
|
1465
|
+
partitionColumn?: string | undefined;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Define a typed table.
|
|
1469
|
+
*
|
|
1470
|
+
* The function signature uses two const-generic parameters to preserve
|
|
1471
|
+
* column literal types end-to-end. This is what enables `Row`,
|
|
1472
|
+
* `InsertRow`, and `WhereClause` to know exact column shapes at the call
|
|
1473
|
+
* site.
|
|
1474
|
+
*
|
|
1475
|
+
* @throws if the table name doesn't match the snake_case regex, or if
|
|
1476
|
+
* the same name is registered twice with different table objects.
|
|
1477
|
+
*/
|
|
1478
|
+
declare function defineTable<const TCols extends Record<string, ColumnFactory>>(def: TableDefinition<TCols>): {
|
|
1479
|
+
table: PgTableWithColumns<{
|
|
1480
|
+
name: string;
|
|
1481
|
+
schema: undefined;
|
|
1482
|
+
columns: {
|
|
1483
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
1484
|
+
name: string;
|
|
1485
|
+
tableName: string;
|
|
1486
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
1487
|
+
columnType: string;
|
|
1488
|
+
data: unknown;
|
|
1489
|
+
driverParam: unknown;
|
|
1490
|
+
notNull: false;
|
|
1491
|
+
hasDefault: false;
|
|
1492
|
+
isPrimaryKey: false;
|
|
1493
|
+
isAutoincrement: false;
|
|
1494
|
+
hasRuntimeDefault: false;
|
|
1495
|
+
enumValues: string[] | undefined;
|
|
1496
|
+
baseColumn: never;
|
|
1497
|
+
identity: undefined;
|
|
1498
|
+
generated: undefined;
|
|
1499
|
+
}, {}, {}>;
|
|
1500
|
+
};
|
|
1501
|
+
dialect: "pg";
|
|
1502
|
+
}>;
|
|
1503
|
+
schema: TableDefinition<TCols>;
|
|
1504
|
+
columnKinds: Readonly<Record<string, ColumnKind>>;
|
|
1505
|
+
primaryKeyColumns: readonly string[];
|
|
1506
|
+
makeClient: (database: PostgresJsDatabase, partitionValue?: string) => TableClient<TCols, PgTableWithColumns<{
|
|
1507
|
+
name: string;
|
|
1508
|
+
schema: undefined;
|
|
1509
|
+
columns: {
|
|
1510
|
+
[x: string]: import("drizzle-orm/pg-core").PgColumn<{
|
|
1511
|
+
name: string;
|
|
1512
|
+
tableName: string;
|
|
1513
|
+
dataType: import("drizzle-orm").ColumnDataType;
|
|
1514
|
+
columnType: string;
|
|
1515
|
+
data: unknown;
|
|
1516
|
+
driverParam: unknown;
|
|
1517
|
+
notNull: false;
|
|
1518
|
+
hasDefault: false;
|
|
1519
|
+
isPrimaryKey: false;
|
|
1520
|
+
isAutoincrement: false;
|
|
1521
|
+
hasRuntimeDefault: false;
|
|
1522
|
+
enumValues: string[] | undefined;
|
|
1523
|
+
baseColumn: never;
|
|
1524
|
+
identity: undefined;
|
|
1525
|
+
generated: undefined;
|
|
1526
|
+
}, {}, {}>;
|
|
1527
|
+
};
|
|
1528
|
+
dialect: "pg";
|
|
1529
|
+
}>>;
|
|
1530
|
+
partitionColumn: (keyof TCols & string) | undefined;
|
|
1531
|
+
};
|
|
1532
|
+
//#endregion
|
|
1533
|
+
//#region src/table/dependency-registry.d.ts
|
|
1534
|
+
/** Runs the typed cleanup for one dependency, inside the caller's tx. */
|
|
1535
|
+
type DependentCleanup = (tx: PostgresJsDatabase, entityIds: string[]) => Promise<void>;
|
|
1536
|
+
/** A single table→entity cleanup descriptor produced by `defineTable`. */
|
|
1537
|
+
interface TableCleanupDescriptor {
|
|
1538
|
+
/** Entity whose deletion triggers this cleanup. */
|
|
1539
|
+
entity: string;
|
|
1540
|
+
/** The typed, tx-bound cleanup executor. */
|
|
1541
|
+
cleanup: DependentCleanup;
|
|
1542
|
+
}
|
|
1543
|
+
/** Register a cleanup executor for `entityName`. Called by the merge step. */
|
|
1544
|
+
declare function registerDependentCleanup(entityName: string, cleanup: DependentCleanup): void;
|
|
1545
|
+
/** All cleanup executors declared as depending on `entityName` (may be empty). */
|
|
1546
|
+
declare function getDependentCleanups(entityName: string): readonly DependentCleanup[];
|
|
1547
|
+
/** Test seam — clears all entity-keyed registrations. */
|
|
1548
|
+
declare function __clearDependentCleanups(): void;
|
|
1549
|
+
/** Read the cleanup descriptors for a raw Drizzle table (may be empty). */
|
|
1550
|
+
declare function getTableCleanupDescriptors(table: Table): readonly TableCleanupDescriptor[];
|
|
1551
|
+
/**
|
|
1552
|
+
* The column a raw Drizzle table is partitioned by (`TableDefinition.
|
|
1553
|
+
* partitionColumn`), or `undefined`. Lets whole-install maintenance — the one
|
|
1554
|
+
* sanctioned site rename (`lumi site:rename-default`) — find every table that
|
|
1555
|
+
* keys rows by a site handle without an FK to cascade through.
|
|
1556
|
+
*/
|
|
1557
|
+
declare function getTablePartitionColumn(table: Table): string | undefined;
|
|
1558
|
+
//#endregion
|
|
1559
|
+
//#region src/table/registry.d.ts
|
|
1560
|
+
declare class TableRegistryImpl {
|
|
1561
|
+
private byName;
|
|
1562
|
+
/** Table names we've already surfaced a bundler-duplicate warning for (once per name). */
|
|
1563
|
+
private warnedDuplicates;
|
|
1564
|
+
/**
|
|
1565
|
+
* Register a defined table. Throws if a table with the same name is
|
|
1566
|
+
* already registered (catches accidental double-registration from a
|
|
1567
|
+
* file being imported under two paths).
|
|
1568
|
+
*/
|
|
1569
|
+
register(name: string, table: PgTable, source?: string): void;
|
|
1570
|
+
/**
|
|
1571
|
+
* Get a defined table by name.
|
|
1572
|
+
*/
|
|
1573
|
+
get(name: string): PgTable | undefined;
|
|
1574
|
+
/**
|
|
1575
|
+
* Returns true if a table with the given name has been registered.
|
|
1576
|
+
*/
|
|
1577
|
+
has(name: string): boolean;
|
|
1578
|
+
/**
|
|
1579
|
+
* Return every registered table as a `{ [name]: PgTable }` map.
|
|
1580
|
+
*
|
|
1581
|
+
* Useful for `createTestDb().push(tableRegistry.allTables())` and
|
|
1582
|
+
* runtime introspection.
|
|
1583
|
+
*/
|
|
1584
|
+
allTables(): Record<string, PgTable>;
|
|
1585
|
+
/**
|
|
1586
|
+
* Clear the registry. **Test-only.**
|
|
1587
|
+
*
|
|
1588
|
+
* Production code should never call this — clearing the registry
|
|
1589
|
+
* silently breaks migration discovery and any code path that looks
|
|
1590
|
+
* up a table by name. The runtime guard below refuses to run when
|
|
1591
|
+
* `NODE_ENV === 'production'` to catch accidental misuse.
|
|
1592
|
+
*
|
|
1593
|
+
* Test runners typically set `NODE_ENV=test` (Vitest does), but the
|
|
1594
|
+
* guard is permissive about other values — it only blocks the one
|
|
1595
|
+
* environment where calling it is definitely wrong.
|
|
1596
|
+
*/
|
|
1597
|
+
clear(): void;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Process-wide table registry singleton.
|
|
1601
|
+
*
|
|
1602
|
+
* Tables register themselves on import via `defineTable()`. The registry
|
|
1603
|
+
* is module-level state, which means a single Node process sees one
|
|
1604
|
+
* consistent view of all defined tables across packages.
|
|
1605
|
+
*/
|
|
1606
|
+
declare const tableRegistry: TableRegistryImpl;
|
|
1607
|
+
//#endregion
|
|
1608
|
+
//#region src/table/where-builder.d.ts
|
|
1609
|
+
/**
|
|
1610
|
+
* A custom error thrown by the where-builder for any caller-side mistake.
|
|
1611
|
+
*
|
|
1612
|
+
* Catching `WhereBuilderError` lets the table client surface a 400-style
|
|
1613
|
+
* error to its caller without conflating it with database errors.
|
|
1614
|
+
*/
|
|
1615
|
+
declare class WhereBuilderError extends Error {
|
|
1616
|
+
constructor(message: string);
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Returns true if a {@link WhereClause} produces a non-trivial filter (at
|
|
1620
|
+
* least one column predicate or sub-clause). Useful for rejecting empty
|
|
1621
|
+
* `where` arguments to `update`/`delete`/`updateMany`/`deleteMany`.
|
|
1622
|
+
*
|
|
1623
|
+
* Note: this does not check whether `buildWhere()` would actually emit SQL
|
|
1624
|
+
* for the clause — it just checks that the user supplied something.
|
|
1625
|
+
*/
|
|
1626
|
+
declare function isNonEmptyWhere(clause: unknown): boolean;
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/transaction-hooks.d.ts
|
|
1629
|
+
/** Work to run once the transaction it was registered on has committed. */
|
|
1630
|
+
type CommitCallback = () => void | Promise<void>;
|
|
1631
|
+
/**
|
|
1632
|
+
* Install on-commit tracking on a read-write client, in place, and return it.
|
|
1633
|
+
*
|
|
1634
|
+
* Idempotent: a client that is already tracked is returned untouched, so a
|
|
1635
|
+
* callback never runs twice because two layers each installed tracking.
|
|
1636
|
+
*/
|
|
1637
|
+
declare function trackTransactions<TDb extends PostgresJsDatabase>(db: TDb): TDb;
|
|
1638
|
+
/**
|
|
1639
|
+
* Register `callback` to run after the transaction `executor` belongs to
|
|
1640
|
+
* commits. Returns `true` when it was registered, and `false` — having done
|
|
1641
|
+
* nothing — when `executor` is not an open tracked transaction, in which case
|
|
1642
|
+
* the caller decides whether to run the work now.
|
|
1643
|
+
*/
|
|
1644
|
+
declare function onCommit(executor: object | null | undefined, callback: CommitCallback): boolean;
|
|
1645
|
+
/** Whether `executor` is a transaction handle that is open and tracked right now. */
|
|
1646
|
+
declare function isTrackedTransaction(executor: object | null | undefined): boolean;
|
|
1647
|
+
//#endregion
|
|
1648
|
+
export { type AdvisoryLockKey, type AdvisoryLockResult, type AnyPgColumn, type ClaimOptions, type ColumnFactory, type ColumnKind, type ColumnOperators, type ColumnValue, type CommitCallback, DEFAULT_LIMIT, DEFAULT_READ_ONLY_STATEMENT_TIMEOUT_MS, DEPLOY_STEPS_LOCK, DEPLOY_STEPS_TABLE_NAME, DEPLOY_STEP_STATES, type DbConfig, type DefinedTable, type DependentCleanup, type DeployStepState, EXTENSION_NAME_PATTERN, type FindManyOptions, type IndexDefinition, type InsertRow, MAX_BATCH, MAX_LIMIT, MAX_STATEMENT_TIMEOUT_MS, type MigrateArgs, type MigrationLoader, type MigrationModule, type MigrationSource, type MigrationStatus, type OrderBySpec, type QueryStats, type Row, type SchemaRegistry, TOOLKIT_ADVISORY_LOCK_SPACE, type TableCleanupDescriptor, TableClient, TableClientError, type TableDefinition, type TableDependency, type UniqueDefinition, WhereBuilderError, type WhereClause, __clearDependentCleanups, assertValidExtensionName, column, createDbClient, createExtensionStatement, createListenClient, createReadOnlyClient, defineTable, deployStepsTable, getDependentCleanups, getMigrationStatus, getQueryStats, getTableCleanupDescriptors, getTablePartitionColumn, isNonEmptyWhere, isQueryCounterEnabled, isTrackedTransaction, onCommit, parseStatementTimeoutEnv, queryCountingLogger, registerDependentCleanup, rollbackMigrations, runMigrations, runWithQueryStats, schemaRegistry, tableRegistry, toolkitDeploySteps, trackTransactions, withAdvisoryLock };
|
|
1649
|
+
//# sourceMappingURL=index.d.mts.map
|