@softure-ai/db 0.1.6 → 0.1.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done"). Versions before the first one below are described in their GitHub Releases (`db@x.y.z`).
6
6
 
7
+ ## 0.1.8
8
+
9
+ - `createProcessDatabase(url, { globalKey })` keeps the open database on `globalThis`, so a module `next dev`
10
+ re-evaluates finds it; `closeProcessDatabases()` closes every process database, for tests (#404).
11
+ - `@softure-ai/db/testing`: `expectEntriesOpenDatabase({ entries, clientModule, opens })` and
12
+ `findEntriesReachingDatabase(...)`, an entry-point guard over esbuild's import graph (`esbuild` is an optional
13
+ peer) (#404).
14
+ - `drizzleAppMigrations(folder)`: an app migration hook running drizzle's migrator for the handle's driver (#404).
15
+ - `queryDatabase(handle, text, values)` and `openRawDatabase(url)`: parameterized raw SQL for scripts on either
16
+ driver (#404).
17
+
18
+ ## 0.1.7
19
+
20
+ - `createProcessDatabase(url, { schema, max })` returns `{ db, open, close, withDatabase, isOpen }`: a module-level,
21
+ synchronous `db` over the process-wide handle for the URL. A query before `open()` throws `Database is not open`;
22
+ `withDatabase(main)` opens, runs and closes for a script. `url` may be a function, read on `open()` (#313).
23
+ - `findDriverError(error)` (`{ code, constraint, message }` of the Postgres error under drizzle's wrapper) and
24
+ `isConstraintViolation(error, { code, constraint })`, so apps and modules stop copying them (#313).
25
+ - `closeSharedDatabase(url)` closes and forgets one shared handle.
26
+
7
27
  ## 0.1.6
8
28
 
9
29
  - One database handle per process: `getConfiguredDatabase` opens the config's `database.handle` or URL once, and modules, health route and commands share it.
package/README.md CHANGED
@@ -7,7 +7,10 @@ The database layer every SOFTURE module stands on. Standard:
7
7
 
8
8
  A pg/PGlite client chosen by URL, a migrator that runs each module's SQL in the module's own
9
9
  Postgres schema (with a ledger, checksums, a lock, a dry run and adoption of existing tables),
10
- `createTestDatabase` for unit tests, and the `softure migrate` command.
10
+ `createTestDatabase` for unit tests, the `softure migrate` command, a module-level process database
11
+ (`createProcessDatabase`) with an entry-point guard for tests, `drizzleAppMigrations` for the app's own
12
+ drizzle folder, raw parameterized queries for scripts (`queryDatabase`, `openRawDatabase`) and helpers that
13
+ read the Postgres error behind a failed query (`findDriverError`, `isConstraintViolation`).
11
14
 
12
15
  ## 2. Installation
13
16
 
@@ -140,6 +143,100 @@ export async function getDb() {
140
143
  the modules use can call it too. A migration run on a PGlite handle the app shares keeps the app's
141
144
  session settings (`TimeZone`, `search_path`); only what the run changed is put back.
142
145
 
146
+ ### A module-level `db`
147
+
148
+ An app whose code imports one `db` everywhere, instead of awaiting a handle in each function,
149
+ creates it once with `createProcessDatabase`. `db` stands in for the shared handle's drizzle instance
150
+ (the one `getSharedDatabase(url)` gives the modules, so there is still one pool); every entry point
151
+ opens it first, and a query before that throws `Database is not open`:
152
+
153
+ ```ts
154
+ // db/index.ts
155
+ import { createProcessDatabase } from "@softure-ai/db";
156
+ import * as schema from "./schema";
157
+
158
+ // A function: read on open(), so a script may load its environment after importing this file.
159
+ export const { db, open, close, withDatabase } = createProcessDatabase(() => process.env.DATABASE_URL ?? "", { schema });
160
+
161
+ // instrumentation.ts (the server): open once at startup
162
+ export async function register() {
163
+ if (process.env.NEXT_RUNTIME === "nodejs") await (await import("./db")).open();
164
+ }
165
+
166
+ // scripts/import.ts: opens, runs, closes (also when main throws)
167
+ await withDatabase(async (db) => { /* … */ });
168
+ ```
169
+
170
+ `open()` is idempotent; `close()` closes the shared handle for that URL, which the modules use too,
171
+ so only a script's end calls it. With `schema`, `db.query.<table>` is typed and works.
172
+
173
+ `next dev` re-evaluates `db/index.ts` on a change, and a new evaluation would refuse queries until
174
+ something opens it again. `globalKey` keeps the open database on `globalThis`, so every evaluation
175
+ finds it (the open `db` keeps the schema it was opened with until a restart):
176
+
177
+ ```ts
178
+ export const { db, open, close, withDatabase } = createProcessDatabase(() => process.env.DATABASE_URL ?? "", {
179
+ schema,
180
+ globalKey: "app",
181
+ });
182
+ ```
183
+
184
+ In tests, `closeProcessDatabases()` in `afterEach` closes every process database the test opened.
185
+
186
+ **Entry-point guard.** An entry that forgets `open()` fails only at its first query. A test in the app
187
+ fails earlier: `expectEntriesOpenDatabase` (from `@softure-ai/db/testing`, needs `esbuild` as a dev
188
+ dependency) follows each entry's imports and throws, naming them, when an entry reaches the database
189
+ module without a call to `withDatabase(` or `open(` in its own source.
190
+ `findEntriesReachingDatabase` returns `{ entry, opens }[]` for the same check by hand:
191
+
192
+ ```ts
193
+ // tests/entry-points.test.ts
194
+ import { expectEntriesOpenDatabase } from "@softure-ai/db/testing";
195
+
196
+ it("opens the database at every entry", async () => {
197
+ await expectEntriesOpenDatabase({
198
+ entries: ["src/instrumentation.ts", "src/mcp/stdio.ts", "scripts/import.ts"],
199
+ clientModule: "src/db/index.ts",
200
+ // an app helper that opens it counts too
201
+ opens: [/\bwithDatabase\s*\(/, /\bopen\s*\(/, /\bopenConfiguredDatabase\s*\(/],
202
+ });
203
+ });
204
+ ```
205
+
206
+ ### Raw queries for scripts
207
+
208
+ An operator or report script that reads with plain SQL opens its own handle on the URL, whichever
209
+ driver it names, and closes it at the end. Values bind to `$1`, `$2`, …; they never go into the text:
210
+
211
+ ```ts
212
+ import { openRawDatabase } from "@softure-ai/db";
213
+
214
+ const database = await openRawDatabase(process.env.DATABASE_URL ?? "");
215
+ try {
216
+ const rows = await database.query<{ email: string }>("select email from users where created_at > $1", [since]);
217
+ } finally {
218
+ await database.close();
219
+ }
220
+ ```
221
+
222
+ `queryDatabase(handle, text, values)` does the same over a handle the script already has.
223
+
224
+ ### Driver errors
225
+
226
+ `findDriverError(error)` returns `{ code, constraint, message }` of the Postgres error behind a failed
227
+ query (node-postgres and PGlite alike; drizzle wraps it as the `cause`), or `undefined`.
228
+ `isConstraintViolation(error, { code, constraint })` answers whether it is that SQLSTATE on that
229
+ constraint (any constraint when `constraint` is left out):
230
+
231
+ ```ts
232
+ try {
233
+ await db.insert(articles).values(article);
234
+ } catch (error) {
235
+ if (!isConstraintViolation(error, { code: "23505", constraint: "articles_slug_key" })) throw error;
236
+ return err("slug_taken");
237
+ }
238
+ ```
239
+
143
240
  ## 4. Mounting
144
241
 
145
242
  Nothing to mount. Migrations run as a deploy step:
@@ -193,22 +290,12 @@ object to the migrate script and to `createTestDatabase`:
193
290
 
194
291
  ```ts
195
292
  // db/app-migrations.ts
196
- import type { AppMigrations, DatabaseHandle } from "@softure-ai/db";
197
-
198
- async function runDrizzle(handle: DatabaseHandle, migrationsFolder: string): Promise<void> {
199
- if (handle.kind === "postgres") {
200
- const { migrate } = await import("drizzle-orm/node-postgres/migrator");
201
- await migrate(handle.db, { migrationsFolder });
202
- } else {
203
- const { migrate } = await import("drizzle-orm/pglite/migrator");
204
- await migrate(handle.db, { migrationsFolder });
205
- }
206
- }
293
+ import { drizzleAppMigrations, type AppMigrations } from "@softure-ai/db";
207
294
 
208
295
  export const appMigrations: AppMigrations = {
209
- before: (handle) => runDrizzle(handle, "./drizzle"),
296
+ before: drizzleAppMigrations("./drizzle"),
210
297
  // only when an app migration needs a module table: a second drizzle folder
211
- // after: (handle) => runDrizzle(handle, "./drizzle-after"),
298
+ // after: drizzleAppMigrations("./drizzle-after"),
212
299
  };
213
300
 
214
301
  // scripts/migrate.ts
@@ -231,7 +318,7 @@ file the app's own history creates; `migrate` then adopts or migrates by itself,
231
318
 
232
319
  ```ts
233
320
  export const appMigrations: AppMigrations = {
234
- before: (handle) => runDrizzle(handle, "./drizzle"),
321
+ before: drizzleAppMigrations("./drizzle"),
235
322
  // the app's history creates auth's 0001 (users, sessions); 0002.. are new to it
236
323
  baseline: { auth: 1 },
237
324
  };
@@ -0,0 +1,21 @@
1
+ export interface DriverError {
2
+ /** The SQLSTATE, e.g. `23505` (unique violation) or `23P01` (exclusion violation). */
3
+ readonly code: string;
4
+ /** The violated constraint's name, when the error is about one. */
5
+ readonly constraint: string | undefined;
6
+ readonly message: string;
7
+ }
8
+ export interface ConstraintViolation {
9
+ /** The SQLSTATE to match, e.g. `23505`. */
10
+ readonly code: string;
11
+ /** The constraint's name; when left out, any constraint (or none) matches. */
12
+ readonly constraint?: string;
13
+ }
14
+ /** The driver error behind `error` (itself or one of its causes), or undefined when there is none. */
15
+ export declare function findDriverError(error: unknown): DriverError | undefined;
16
+ /**
17
+ * Whether `error` is the driver's `violation.code`, on `violation.constraint` when given:
18
+ * `isConstraintViolation(error, { code: "23505", constraint: "articles_slug_key" })` for a taken slug.
19
+ */
20
+ export declare function isConstraintViolation(error: unknown, violation: ConstraintViolation): boolean;
21
+ //# sourceMappingURL=driver-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver-errors.d.ts","sourceRoot":"","sources":["../src/driver-errors.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAMvE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAI7F"}
@@ -0,0 +1,25 @@
1
+ // The Postgres error behind a failed query. node-postgres and PGlite both put the SQLSTATE in `code` and the
2
+ // violated constraint's name in `constraint`; drizzle wraps that error as the `cause` of its own query error.
3
+ // App and module code that turns a known violation into a result (a taken slug, a conflicting row) reads it here.
4
+ /** How many `cause` links are followed: drizzle adds one, an app's own wrapper may add another. */
5
+ const MAX_CAUSE_DEPTH = 5;
6
+ /** The driver error behind `error` (itself or one of its causes), or undefined when there is none. */
7
+ export function findDriverError(error) {
8
+ for (let current = error, depth = 0; current instanceof Error && depth < MAX_CAUSE_DEPTH; current = current.cause, depth += 1) {
9
+ const { code, constraint } = current;
10
+ if (typeof code === "string")
11
+ return { code, constraint: typeof constraint === "string" ? constraint : undefined, message: current.message };
12
+ }
13
+ return undefined;
14
+ }
15
+ /**
16
+ * Whether `error` is the driver's `violation.code`, on `violation.constraint` when given:
17
+ * `isConstraintViolation(error, { code: "23505", constraint: "articles_slug_key" })` for a taken slug.
18
+ */
19
+ export function isConstraintViolation(error, violation) {
20
+ const driverError = findDriverError(error);
21
+ if (driverError?.code !== violation.code)
22
+ return false;
23
+ return violation.constraint === undefined || driverError.constraint === violation.constraint;
24
+ }
25
+ //# sourceMappingURL=driver-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver-errors.js","sourceRoot":"","sources":["../src/driver-errors.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,8GAA8G;AAC9G,kHAAkH;AAElH,mGAAmG;AACnG,MAAM,eAAe,GAAG,CAAC,CAAC;AAiB1B,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,KAAK,IAAI,OAAO,GAAY,KAAK,EAAE,KAAK,GAAG,CAAC,EAAE,OAAO,YAAY,KAAK,IAAI,KAAK,GAAG,eAAe,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACvI,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAmD,CAAC;QACjF,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/I,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc,EAAE,SAA8B;IAClF,MAAM,WAAW,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,WAAW,EAAE,IAAI,KAAK,SAAS,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACvD,OAAO,SAAS,CAAC,UAAU,KAAK,SAAS,IAAI,WAAW,CAAC,UAAU,KAAK,SAAS,CAAC,UAAU,CAAC;AAC/F,CAAC"}
@@ -0,0 +1,28 @@
1
+ /** A call to `withDatabase(` or `open(` (`processDb.open(` too): what `createProcessDatabase` gives an entry. */
2
+ export declare const DEFAULT_DATABASE_OPENERS: readonly RegExp[];
3
+ export interface EntryGuardOptions {
4
+ /** The process entry points, as paths (relative to `workingDir`) or absolute paths. */
5
+ readonly entries: readonly string[];
6
+ /** The app's module that calls `createProcessDatabase` (e.g. `src/db/index.ts`). */
7
+ readonly clientModule: string;
8
+ /** Patterns an entry's own source must match to count as opening the database. Default: `DEFAULT_DATABASE_OPENERS`. */
9
+ readonly opens?: readonly RegExp[];
10
+ /** The directory paths and the app's tsconfig resolve from. Default: `process.cwd()`. */
11
+ readonly workingDir?: string;
12
+ /** Extra import paths esbuild leaves out of the graph (packages are always left out). */
13
+ readonly external?: readonly string[];
14
+ }
15
+ export interface EntryReach {
16
+ /** The entry as given in `entries`. */
17
+ readonly entry: string;
18
+ /** Whether the entry's own source opens the database. */
19
+ readonly opens: boolean;
20
+ }
21
+ /**
22
+ * The entries whose import graph (static and dynamic imports) reaches `clientModule`, in the order given, each
23
+ * with whether its own source opens the database. Entries that never reach it are left out.
24
+ */
25
+ export declare function findEntriesReachingDatabase(options: EntryGuardOptions): Promise<EntryReach[]>;
26
+ /** Throws, naming each one, when an entry reaches `clientModule` without opening the database. */
27
+ export declare function expectEntriesOpenDatabase(options: EntryGuardOptions): Promise<void>;
28
+ //# sourceMappingURL=entry-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-guard.d.ts","sourceRoot":"","sources":["../src/entry-guard.ts"],"names":[],"mappings":"AAQA,iHAAiH;AACjH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAA2C,CAAC;AAElG,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,oFAAoF;IACpF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,uHAAuH;IACvH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,yFAAyF;IACzF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,UAAU;IACzB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAID;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAenG;AAED,kGAAkG;AAClG,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzF"}
@@ -0,0 +1,87 @@
1
+ // The entry-point guard for an app on `createProcessDatabase`: `db` is synchronous but opening is not, so every
2
+ // process entry (the server's instrumentation, an MCP stdio server, each script) must open the database before its
3
+ // first query. esbuild resolves each entry's import graph (path aliases and re-exports included); an entry that
4
+ // reaches the app's client module must call an opener in its own source. esbuild is an optional peer, loaded here.
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ /** A call to `withDatabase(` or `open(` (`processDb.open(` too): what `createProcessDatabase` gives an entry. */
8
+ export const DEFAULT_DATABASE_OPENERS = [/\bwithDatabase\s*\(/, /\bopen\s*\(/];
9
+ const ASSET_EXTENSIONS = [".css", ".scss", ".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".woff", ".woff2"];
10
+ /**
11
+ * The entries whose import graph (static and dynamic imports) reaches `clientModule`, in the order given, each
12
+ * with whether its own source opens the database. Entries that never reach it are left out.
13
+ */
14
+ export async function findEntriesReachingDatabase(options) {
15
+ const workingDir = options.workingDir ?? process.cwd();
16
+ const clientPath = resolve(workingDir, options.clientModule);
17
+ if (!existsSync(clientPath)) {
18
+ throw new Error(`findEntriesReachingDatabase: the client module does not exist: ${options.clientModule}`);
19
+ }
20
+ const metafile = await buildMetafile(options.entries.map((entry) => resolve(workingDir, entry)), workingDir, options.external ?? []);
21
+ const opens = options.opens ?? DEFAULT_DATABASE_OPENERS;
22
+ return options.entries.flatMap((entry) => {
23
+ const entryPath = resolve(workingDir, entry);
24
+ if (!reaches(metafile, workingDir, entryPath, clientPath))
25
+ return [];
26
+ const source = readFileSync(entryPath, "utf8");
27
+ return [{ entry, opens: opens.some((pattern) => pattern.test(source)) }];
28
+ });
29
+ }
30
+ /** Throws, naming each one, when an entry reaches `clientModule` without opening the database. */
31
+ export async function expectEntriesOpenDatabase(options) {
32
+ const missing = (await findEntriesReachingDatabase(options)).filter((reach) => !reach.opens);
33
+ if (missing.length === 0)
34
+ return;
35
+ throw new Error(`These entries reach the database client without opening it (call withDatabase() or open() at the entry):\n` +
36
+ missing.map((reach) => ` - ${reach.entry}`).join("\n"));
37
+ }
38
+ async function buildMetafile(entryPoints, workingDir, external) {
39
+ const { build } = await loadEsbuild();
40
+ const result = await build({
41
+ entryPoints: [...entryPoints],
42
+ absWorkingDir: workingDir,
43
+ // Never written (`write: false`); esbuild needs an output directory for several entries.
44
+ outdir: resolve(workingDir, ".entry-guard"),
45
+ bundle: true,
46
+ write: false,
47
+ metafile: true,
48
+ platform: "node",
49
+ format: "esm",
50
+ packages: "external",
51
+ external: [...external],
52
+ loader: Object.fromEntries(ASSET_EXTENSIONS.map((extension) => [extension, "empty"])),
53
+ logLevel: "silent",
54
+ }).catch((error) => {
55
+ const reason = error instanceof Error ? error.message : String(error);
56
+ throw new Error(`findEntriesReachingDatabase: esbuild could not follow the entries' imports: ${reason}`, { cause: error });
57
+ });
58
+ return result.metafile;
59
+ }
60
+ async function loadEsbuild() {
61
+ try {
62
+ return await import("esbuild");
63
+ }
64
+ catch (error) {
65
+ throw new Error("findEntriesReachingDatabase: needs the \"esbuild\" package; run `npm install --save-dev esbuild`", { cause: error });
66
+ }
67
+ }
68
+ function reaches(metafile, workingDir, entryPath, clientPath) {
69
+ const keyByPath = new Map(Object.keys(metafile.inputs).map((key) => [resolve(workingDir, key), key]));
70
+ const start = keyByPath.get(entryPath);
71
+ if (start === undefined)
72
+ return false;
73
+ const seen = new Set([start]);
74
+ const queue = [start];
75
+ for (let key = queue.shift(); key !== undefined; key = queue.shift()) {
76
+ if (resolve(workingDir, key) === clientPath)
77
+ return true;
78
+ for (const imported of metafile.inputs[key]?.imports ?? []) {
79
+ if (imported.external === true || seen.has(imported.path))
80
+ continue;
81
+ seen.add(imported.path);
82
+ queue.push(imported.path);
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ //# sourceMappingURL=entry-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-guard.js","sourceRoot":"","sources":["../src/entry-guard.ts"],"names":[],"mappings":"AAAA,gHAAgH;AAChH,mHAAmH;AACnH,gHAAgH;AAChH,mHAAmH;AACnH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,iHAAiH;AACjH,MAAM,CAAC,MAAM,wBAAwB,GAAsB,CAAC,qBAAqB,EAAE,aAAa,CAAC,CAAC;AAsBlG,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AAEhH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,OAA0B;IAC1E,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACvD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,kEAAkE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC5G,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IACrI,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,wBAAwB,CAAC;IAExD,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QACvC,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC;YAAE,OAAO,EAAE,CAAC;QACrE,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC/C,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,kGAAkG;AAClG,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,OAA0B;IACxE,MAAM,OAAO,GAAG,CAAC,MAAM,2BAA2B,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7F,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,MAAM,IAAI,KAAK,CACb,4GAA4G;QAC1G,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAC1D,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,WAA8B,EAAE,UAAkB,EAAE,QAA2B;IAC1G,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC;QACzB,WAAW,EAAE,CAAC,GAAG,WAAW,CAAC;QAC7B,aAAa,EAAE,UAAU;QACzB,yFAAyF;QACzF,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC;QAC3C,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,KAAK;QACZ,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,KAAK;QACb,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC;QACvB,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,OAAgB,CAAC,CAAC,CAAC;QAC9F,QAAQ,EAAE,QAAQ;KACnB,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QAC1B,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,+EAA+E,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7H,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,QAAQ,CAAC;AACzB,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,kGAAkG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACxI,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,QAAkB,EAAE,UAAkB,EAAE,SAAiB,EAAE,UAAkB;IAC5F,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACtG,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC;IACtB,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,KAAK,SAAS,EAAE,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QACrE,IAAI,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC;QACzD,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;YAC3D,IAAI,QAAQ,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,SAAS;YACpE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  export { createDatabase, createPgliteHandle, createPostgresHandle, isDatabaseHandle, type CreateDatabaseOptions, type Database, type DatabaseHandle, type DatabaseSchema, type PgliteClientDatabase, type PostgresDatabase, type Queryable, } from "./client.js";
2
- export { closeSharedDatabases, getSharedDatabase } from "./shared.js";
2
+ export { closeSharedDatabase, closeSharedDatabases, getSharedDatabase } from "./shared.js";
3
+ export { closeProcessDatabases, createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
4
+ export { openRawDatabase, queryDatabase, type RawDatabase } from "./raw.js";
5
+ export { findDriverError, isConstraintViolation, type ConstraintViolation, type DriverError } from "./driver-errors.js";
3
6
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase, type CommandDatabase } from "./configured.js";
4
7
  export { computeChecksum, readMigrationFiles, type MigrationFile } from "./migrations/files.js";
5
8
  export { describeProblem, type MigrationErrorCode, type MigrationFailure, type MigrationProblem, type MigrationResult, } from "./migrations/problems.js";
6
9
  export { migrate, planMigrations, runAppMigrations, type AppMigrationHook, type AppMigrationPhase, type AppMigrations, type MigrateOptions, type MigrationPlan, type MigrationReport, type MigrationStep, } from "./migrations/migrator.js";
10
+ export { drizzleAppMigrations } from "./migrations/drizzle.js";
7
11
  export { adoptModule, type AdoptionReport, type AdoptOptions } from "./migrations/adopt.js";
8
12
  export { exportMigrations } from "./migrations/export.js";
9
13
  export { checkExportedMigrations, type ExportedMigration, type ExportedMigrationsCheck, } from "./migrations/exported.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC/H,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACxH,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  // Public API of @softure-ai/db: the database client and the module migrator
2
2
  // (docs/02-module-standard.md §4). Test databases: `@softure-ai/db/testing`; CLI: `@softure-ai/db/cli`.
3
3
  export { createDatabase, createPgliteHandle, createPostgresHandle, isDatabaseHandle, } from "./client.js";
4
- export { closeSharedDatabases, getSharedDatabase } from "./shared.js";
4
+ export { closeSharedDatabase, closeSharedDatabases, getSharedDatabase } from "./shared.js";
5
+ export { closeProcessDatabases, createProcessDatabase } from "./process.js";
6
+ export { openRawDatabase, queryDatabase } from "./raw.js";
7
+ export { findDriverError, isConstraintViolation } from "./driver-errors.js";
5
8
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase } from "./configured.js";
6
9
  export { computeChecksum, readMigrationFiles } from "./migrations/files.js";
7
10
  export { describeProblem, } from "./migrations/problems.js";
8
11
  export { migrate, planMigrations, runAppMigrations, } from "./migrations/migrator.js";
12
+ export { drizzleAppMigrations } from "./migrations/drizzle.js";
9
13
  export { adoptModule } from "./migrations/adopt.js";
10
14
  export { exportMigrations } from "./migrations/export.js";
11
15
  export { checkExportedMigrations, } from "./migrations/exported.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wGAAwG;AACxG,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,GAQjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAwB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAsB,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,GAKhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,GAQjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAA0C,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,GAGxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAyC,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wGAAwG;AACxG,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,GAQjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAqD,MAAM,cAAc,CAAC;AAC/H,OAAO,EAAE,eAAe,EAAE,aAAa,EAAoB,MAAM,UAAU,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAA8C,MAAM,oBAAoB,CAAC;AACxH,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAwB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAsB,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,GAKhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,GAQjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAA0C,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,GAGxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAyC,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { AppMigrationHook } from "./migrator.js";
2
+ /**
3
+ * A hook for `app.before` (or `app.after`) that applies the drizzle-kit folder (`meta/_journal.json` and its SQL
4
+ * files) with drizzle's migrator. Drizzle skips what its ledger (`drizzle.__drizzle_migrations`) already holds, so
5
+ * the hook is safe on every `migrate` run. `folder` is a path (relative to the working directory) or a file URL.
6
+ */
7
+ export declare function drizzleAppMigrations(folder: string | URL): AppMigrationHook;
8
+ //# sourceMappingURL=drizzle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../../src/migrations/drizzle.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,GAAG,gBAAgB,CAW3E"}
@@ -0,0 +1,21 @@
1
+ // The app's own drizzle-kit migrations as an app migration hook: drizzle's migrator for the handle's driver,
2
+ // loaded only when the hook runs, so an app ships just its own driver.
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * A hook for `app.before` (or `app.after`) that applies the drizzle-kit folder (`meta/_journal.json` and its SQL
6
+ * files) with drizzle's migrator. Drizzle skips what its ledger (`drizzle.__drizzle_migrations`) already holds, so
7
+ * the hook is safe on every `migrate` run. `folder` is a path (relative to the working directory) or a file URL.
8
+ */
9
+ export function drizzleAppMigrations(folder) {
10
+ const migrationsFolder = typeof folder === "string" ? folder : fileURLToPath(folder);
11
+ return async (handle) => {
12
+ if (handle.kind === "postgres") {
13
+ const { migrate } = await import("drizzle-orm/node-postgres/migrator");
14
+ await migrate(handle.db, { migrationsFolder });
15
+ return;
16
+ }
17
+ const { migrate } = await import("drizzle-orm/pglite/migrator");
18
+ await migrate(handle.db, { migrationsFolder });
19
+ };
20
+ }
21
+ //# sourceMappingURL=drizzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../../src/migrations/drizzle.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,uEAAuE;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAoB;IACvD,MAAM,gBAAgB,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACrF,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE;QACtB,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,oCAAoC,CAAC,CAAC;YACvE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAC;YAC/C,OAAO;QACT,CAAC;QACD,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAC;QAChE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { Database, DatabaseHandle, DatabaseSchema, CreateDatabaseOptions } from "./client.js";
2
+ export interface ProcessDatabaseOptions<TSchema extends DatabaseSchema> extends CreateDatabaseOptions {
3
+ /** The app's drizzle schema: `db` is built with it, so `db.query.<table>` is typed and works. */
4
+ readonly schema?: TSchema;
5
+ /**
6
+ * Keeps the open database on `globalThis` under this key, so a module that `next dev` re-evaluates (HMR) finds the
7
+ * database its previous evaluation opened instead of refusing queries. One key per database in the process. The
8
+ * open `db` keeps the schema it was opened with; a changed schema takes effect after `close()` or a restart.
9
+ */
10
+ readonly globalKey?: string;
11
+ }
12
+ export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema> {
13
+ /** The open database. Usable from module scope; a query before `open()` resolves throws "Database is not open". */
14
+ readonly db: Database<TSchema>;
15
+ /** Opens the process-wide handle for the URL (`getSharedDatabase`), once; later calls return the same handle. */
16
+ readonly open: () => Promise<DatabaseHandle>;
17
+ /** Closes the handle and forgets it, so `db` refuses queries again. Does nothing when not open. */
18
+ readonly close: () => Promise<void>;
19
+ /** For a script: opens, runs `main` with `db`, and closes whether `main` resolves or throws. */
20
+ readonly withDatabase: <T>(main: (db: Database<TSchema>) => Promise<T>) => Promise<T>;
21
+ readonly isOpen: () => boolean;
22
+ }
23
+ /**
24
+ * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
25
+ * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
26
+ * modules, so the app and the modules share one pool (db README §3, "One handle per process").
27
+ */
28
+ export declare function createProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema>(url: string | (() => string), options?: ProcessDatabaseOptions<TSchema>): ProcessDatabase<TSchema>;
29
+ /**
30
+ * Closes every process database this process opened (with or without `globalKey`), so each `db` refuses queries
31
+ * until its `open()`. For tests' `afterEach`; a server's shutdown closes its own.
32
+ */
33
+ export declare function closeProcessDatabases(): Promise<void>;
34
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAgBnG,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,qBAAqB;IACnG,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe,CAAC,OAAO,SAAS,cAAc,GAAG,cAAc;IAC9E,mHAAmH;IACnH,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,iHAAiH;IACjH,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7C,mGAAmG;IACnG,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACtF,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,cAAc,GAAG,cAAc,EACnF,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EAC5B,OAAO,GAAE,sBAAsB,CAAC,OAAO,CAAM,GAC5C,eAAe,CAAC,OAAO,CAAC,CAsC1B;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3D"}
@@ -0,0 +1,116 @@
1
+ import { closeSharedDatabase, getSharedDatabase } from "./shared.js";
2
+ const NOT_OPEN_MESSAGE = "Database is not open: call open() or withDatabase() at the process entry before querying";
3
+ const REGISTRY_KEY = Symbol.for("@softure-ai/db/process-databases");
4
+ const host = globalThis;
5
+ /**
6
+ * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
7
+ * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
8
+ * modules, so the app and the modules share one pool (db README §3, "One handle per process").
9
+ */
10
+ export function createProcessDatabase(url, options = {}) {
11
+ const { schema, globalKey, ...databaseOptions } = options;
12
+ const state = getProcessState(globalKey);
13
+ const open = () => {
14
+ if (state.opening === undefined) {
15
+ getRegistry().add(state);
16
+ state.opening = openHandle().catch((error) => {
17
+ state.opening = undefined;
18
+ getRegistry().delete(state);
19
+ throw error;
20
+ });
21
+ }
22
+ return state.opening;
23
+ };
24
+ async function openHandle() {
25
+ const resolvedUrl = typeof url === "function" ? url() : url;
26
+ const handle = await getSharedDatabase(resolvedUrl, databaseOptions);
27
+ state.opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
28
+ return handle;
29
+ }
30
+ const close = () => closeProcessState(state);
31
+ const withDatabase = async (main) => {
32
+ await open();
33
+ try {
34
+ return await main(db);
35
+ }
36
+ finally {
37
+ await close();
38
+ }
39
+ };
40
+ // The state holds whichever schema opened it; this instance's callers see it typed with their own.
41
+ const db = createDatabaseProxy(() => state.opened?.db);
42
+ return { db, open, close, withDatabase, isOpen: () => state.opened !== undefined };
43
+ }
44
+ /**
45
+ * Closes every process database this process opened (with or without `globalKey`), so each `db` refuses queries
46
+ * until its `open()`. For tests' `afterEach`; a server's shutdown closes its own.
47
+ */
48
+ export async function closeProcessDatabases() {
49
+ await Promise.all([...getRegistry()].map(closeProcessState));
50
+ }
51
+ function getProcessState(globalKey) {
52
+ if (globalKey === undefined)
53
+ return { opening: undefined, opened: undefined };
54
+ const key = Symbol.for(`@softure-ai/db/process:${globalKey}`);
55
+ const existing = host[key];
56
+ if (existing !== undefined && !(existing instanceof Set))
57
+ return existing;
58
+ const created = { opening: undefined, opened: undefined };
59
+ host[key] = created;
60
+ return created;
61
+ }
62
+ function getRegistry() {
63
+ const existing = host[REGISTRY_KEY];
64
+ if (existing instanceof Set)
65
+ return existing;
66
+ const created = new Set();
67
+ host[REGISTRY_KEY] = created;
68
+ return created;
69
+ }
70
+ async function closeProcessState(state) {
71
+ getRegistry().delete(state);
72
+ if (state.opening === undefined)
73
+ return;
74
+ const pending = state.opening;
75
+ state.opening = undefined;
76
+ await pending.catch(() => undefined);
77
+ const current = state.opened;
78
+ state.opened = undefined;
79
+ if (current !== undefined)
80
+ await closeSharedDatabase(current.url);
81
+ }
82
+ /** The handle's drizzle instance, or a new one over the same pool or client when the app passes its schema. */
83
+ async function buildDatabase(handle, schema) {
84
+ // The handle's own instance is typed with the default schema; without the app's schema the two types agree.
85
+ if (schema === undefined)
86
+ return handle.db;
87
+ if (handle.kind === "postgres") {
88
+ const { drizzle } = await import("drizzle-orm/node-postgres");
89
+ return drizzle({ client: handle.pool, schema });
90
+ }
91
+ const { drizzle } = await import("drizzle-orm/pglite");
92
+ return drizzle({ client: handle.client, schema });
93
+ }
94
+ /** A stand-in that forwards every property to the open database, or throws when none is open. */
95
+ function createDatabaseProxy(getDatabase) {
96
+ return new Proxy({}, {
97
+ get(_target, property) {
98
+ const database = getDatabase();
99
+ if (database === undefined) {
100
+ // `await db` and `Promise.resolve(db)` look for `then`: a closed database is just not a thenable.
101
+ if (property === "then")
102
+ return undefined;
103
+ throw new Error(NOT_OPEN_MESSAGE);
104
+ }
105
+ const value = Reflect.get(database, property, database);
106
+ return typeof value === "function" ? value.bind(database) : value;
107
+ },
108
+ has(_target, property) {
109
+ const database = getDatabase();
110
+ if (database === undefined)
111
+ throw new Error(NOT_OPEN_MESSAGE);
112
+ return Reflect.has(database, property);
113
+ },
114
+ });
115
+ }
116
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"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"}
package/dist/raw.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type CreateDatabaseOptions, type DatabaseHandle } from "./client.js";
2
+ export interface RawDatabase {
3
+ readonly kind: DatabaseHandle["kind"];
4
+ /** Runs `text` with `values` bound to `$1`, `$2`, … and returns the rows. */
5
+ readonly query: <TRow = Record<string, unknown>>(text: string, values?: readonly unknown[]) => Promise<TRow[]>;
6
+ /** Closes the handle this database opened; a `pglite://<dir>` database is written back only then. */
7
+ readonly close: () => Promise<void>;
8
+ }
9
+ /** Runs a parameterized query on the handle's pool (node-postgres) or client (PGlite) and returns its rows. */
10
+ export declare function queryDatabase<TRow = Record<string, unknown>>(handle: DatabaseHandle, text: string, values?: readonly unknown[]): Promise<TRow[]>;
11
+ /**
12
+ * Opens a new handle on `url` (not the process-wide one) for a script's raw queries. The script closes it at its
13
+ * end. Throws on an empty URL or an unsupported scheme, as `createDatabase` does.
14
+ */
15
+ export declare function openRawDatabase(url: string, options?: CreateDatabaseOptions): Promise<RawDatabase>;
16
+ //# sourceMappingURL=raw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw.d.ts","sourceRoot":"","sources":["../src/raw.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/G,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,+GAA+G;AAC/G,wBAAsB,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,SAAS,OAAO,EAAO,GAC9B,OAAO,CAAC,IAAI,EAAE,CAAC,CAOjB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,WAAW,CAAC,CAO5G"}
package/dist/raw.js ADDED
@@ -0,0 +1,25 @@
1
+ // Raw SQL over whichever driver a handle or URL uses, for operator and report scripts that read with plain SQL
2
+ // instead of drizzle. Values are always passed separately ($1, $2, …), never spliced into the text.
3
+ import { createDatabase } from "./client.js";
4
+ /** Runs a parameterized query on the handle's pool (node-postgres) or client (PGlite) and returns its rows. */
5
+ export async function queryDatabase(handle, text, values = []) {
6
+ if (handle.kind === "postgres") {
7
+ const result = await handle.pool.query(text, [...values]);
8
+ return result.rows;
9
+ }
10
+ const result = await handle.client.query(text, [...values]);
11
+ return result.rows;
12
+ }
13
+ /**
14
+ * Opens a new handle on `url` (not the process-wide one) for a script's raw queries. The script closes it at its
15
+ * end. Throws on an empty URL or an unsupported scheme, as `createDatabase` does.
16
+ */
17
+ export async function openRawDatabase(url, options = {}) {
18
+ const handle = await createDatabase(url, options);
19
+ return {
20
+ kind: handle.kind,
21
+ query: (text, values) => queryDatabase(handle, text, values),
22
+ close: handle.close,
23
+ };
24
+ }
25
+ //# sourceMappingURL=raw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw.js","sourceRoot":"","sources":["../src/raw.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,oGAAoG;AACpG,OAAO,EAAE,cAAc,EAAmD,MAAM,aAAa,CAAC;AAU9F,+GAA+G;AAC/G,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAsB,EACtB,IAAY,EACZ,SAA6B,EAAE;IAE/B,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;QAC1D,OAAO,MAAM,CAAC,IAAc,CAAC;IAC/B,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAO,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IAClE,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,UAAiC,EAAE;IACpF,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAClD,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC;QAC5D,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC"}
package/dist/shared.d.ts CHANGED
@@ -4,6 +4,8 @@ import { type CreateDatabaseOptions, type DatabaseHandle } from "./client.js";
4
4
  * A failed open is not kept, so the next call tries again.
5
5
  */
6
6
  export declare function getSharedDatabase(url: string, options?: CreateDatabaseOptions): Promise<DatabaseHandle>;
7
+ /** Closes and forgets the shared handle for `url`, if one is open. For a script's end. */
8
+ export declare function closeSharedDatabase(url: string): Promise<void>;
7
9
  /** Closes and forgets every shared handle. For tests and graceful shutdown. */
8
10
  export declare function closeSharedDatabases(): Promise<void>;
9
11
  //# sourceMappingURL=shared.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAQ9F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,cAAc,CAAC,CAS3G;AAED,+EAA+E;AAC/E,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1D"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAQ9F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,cAAc,CAAC,CAS3G;AAED,0FAA0F;AAC1F,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMpE;AAED,+EAA+E;AAC/E,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1D"}
package/dist/shared.js CHANGED
@@ -19,6 +19,15 @@ export function getSharedDatabase(url, options = {}) {
19
19
  }
20
20
  return handle;
21
21
  }
22
+ /** Closes and forgets the shared handle for `url`, if one is open. For a script's end. */
23
+ export async function closeSharedDatabase(url) {
24
+ const handle = host[HANDLES_KEY]?.get(url);
25
+ if (handle === undefined)
26
+ return;
27
+ host[HANDLES_KEY]?.delete(url);
28
+ const opened = await handle.catch(() => undefined);
29
+ await opened?.close();
30
+ }
22
31
  /** Closes and forgets every shared handle. For tests and graceful shutdown. */
23
32
  export async function closeSharedDatabases() {
24
33
  const handles = host[HANDLES_KEY];
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,oGAAoG;AACpG,8FAA8F;AAC9F,sFAAsF;AACtF,OAAO,EAAE,cAAc,EAAmD,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;AAIhE,MAAM,IAAI,GAAG,UAAmC,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,UAAiC,EAAE;IAChF,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,GAAG,EAAmC,CAAC,CAAC;IACnF,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAClC,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChH,CAAC"}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,oGAAoG;AACpG,8FAA8F;AAC9F,sFAAsF;AACtF,OAAO,EAAE,cAAc,EAAmD,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;AAIhE,MAAM,IAAI,GAAG,UAAmC,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,UAAiC,EAAE;IAChF,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,GAAG,EAAmC,CAAC,CAAC;IACnF,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAW;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO;IACjC,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACnD,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC;AACxB,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAClC,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChH,CAAC"}
package/dist/testing.d.ts CHANGED
@@ -2,6 +2,7 @@ import { PGlite } from "@electric-sql/pglite";
2
2
  import type { AnySoftureModule } from "@softure-ai/core";
3
3
  import { type PgliteClientDatabase } from "./client.js";
4
4
  import { type AppMigrations } from "./migrations/migrator.js";
5
+ export { DEFAULT_DATABASE_OPENERS, expectEntriesOpenDatabase, findEntriesReachingDatabase, type EntryGuardOptions, type EntryReach, } from "./entry-guard.js";
5
6
  export interface TestDatabase {
6
7
  readonly db: PgliteClientDatabase;
7
8
  readonly client: PGlite;
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG9F,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,oBAAoB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;CAC9B;AAMD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,SAAS,gBAAgB,EAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,OAAO,CAAC,YAAY,CAAC,CAIlJ"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,EACL,wBAAwB,EACxB,yBAAyB,EACzB,2BAA2B,EAC3B,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,oBAAoB,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;CAC9B;AAMD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,SAAS,gBAAgB,EAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,OAAO,CAAC,YAAY,CAAC,CAIlJ"}
package/dist/testing.js CHANGED
@@ -8,6 +8,7 @@ import { PGlite } from "@electric-sql/pglite";
8
8
  import { createPgliteHandle } from "./client.js";
9
9
  import { migrate } from "./migrations/migrator.js";
10
10
  import { describeProblem } from "./migrations/problems.js";
11
+ export { DEFAULT_DATABASE_OPENERS, expectEntriesOpenDatabase, findEntriesReachingDatabase, } from "./entry-guard.js";
11
12
  const templates = new Map();
12
13
  const hookIds = new WeakMap();
13
14
  let nextHookId = 1;
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,4FAA4F;AAC5F,iGAAiG;AACjG,mGAAmG;AACnG,iGAAiG;AACjG,oFAAoF;AACpF,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAA6B,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,EAA6C,MAAM,0BAA0B,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAgB3D,MAAM,SAAS,GAAG,IAAI,GAAG,EAAgC,CAAC;AAC1D,MAAM,OAAO,GAAG,IAAI,OAAO,EAA4B,CAAC;AACxD,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAAuC,EAAE,EAAE,UAAqC,EAAE;IACzH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1F,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;AACxD,CAAC;AAED,SAAS,WAAW,CAAC,OAAoC,EAAE,GAAkB;IAC3E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1G,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,IAAI,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjK,MAAM,GAAG,GAAG,GAAG,SAAS,WAAW,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,WAAW,EAAE,CAAC;IACvH,IAAI,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,QAAQ,GAAG,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACvC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC7B,yEAAyE;QACzE,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2FAA2F;AAC3F,SAAS,YAAY,CAAC,IAAkC;IACtD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC;IACb,CAAC;IACD,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,EAAE,GAAG,UAAU,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC;AACpB,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,OAAoC,EAAE,GAAkB;IACnF,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,2CAA2C,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChH,CAAC;QACD,OAAO,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softure-ai/db",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "SOFTURE AI database layer: pg/PGlite client, per-module migrator with plan and adopt, PGlite test databases.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,6 +54,7 @@
54
54
  "@electric-sql/pglite": "^0.5.0",
55
55
  "@types/pg": "^8.11.0",
56
56
  "drizzle-orm": "^0.45.2",
57
+ "esbuild": ">=0.25.0",
57
58
  "pg": "^8.11.0"
58
59
  },
59
60
  "peerDependenciesMeta": {
@@ -63,6 +64,9 @@
63
64
  "@types/pg": {
64
65
  "optional": true
65
66
  },
67
+ "esbuild": {
68
+ "optional": true
69
+ },
66
70
  "pg": {
67
71
  "optional": true
68
72
  }
@@ -0,0 +1,40 @@
1
+ // The Postgres error behind a failed query. node-postgres and PGlite both put the SQLSTATE in `code` and the
2
+ // violated constraint's name in `constraint`; drizzle wraps that error as the `cause` of its own query error.
3
+ // App and module code that turns a known violation into a result (a taken slug, a conflicting row) reads it here.
4
+
5
+ /** How many `cause` links are followed: drizzle adds one, an app's own wrapper may add another. */
6
+ const MAX_CAUSE_DEPTH = 5;
7
+
8
+ export interface DriverError {
9
+ /** The SQLSTATE, e.g. `23505` (unique violation) or `23P01` (exclusion violation). */
10
+ readonly code: string;
11
+ /** The violated constraint's name, when the error is about one. */
12
+ readonly constraint: string | undefined;
13
+ readonly message: string;
14
+ }
15
+
16
+ export interface ConstraintViolation {
17
+ /** The SQLSTATE to match, e.g. `23505`. */
18
+ readonly code: string;
19
+ /** The constraint's name; when left out, any constraint (or none) matches. */
20
+ readonly constraint?: string;
21
+ }
22
+
23
+ /** The driver error behind `error` (itself or one of its causes), or undefined when there is none. */
24
+ export function findDriverError(error: unknown): DriverError | undefined {
25
+ for (let current: unknown = error, depth = 0; current instanceof Error && depth < MAX_CAUSE_DEPTH; current = current.cause, depth += 1) {
26
+ const { code, constraint } = current as { code?: unknown; constraint?: unknown };
27
+ if (typeof code === "string") return { code, constraint: typeof constraint === "string" ? constraint : undefined, message: current.message };
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ /**
33
+ * Whether `error` is the driver's `violation.code`, on `violation.constraint` when given:
34
+ * `isConstraintViolation(error, { code: "23505", constraint: "articles_slug_key" })` for a taken slug.
35
+ */
36
+ export function isConstraintViolation(error: unknown, violation: ConstraintViolation): boolean {
37
+ const driverError = findDriverError(error);
38
+ if (driverError?.code !== violation.code) return false;
39
+ return violation.constraint === undefined || driverError.constraint === violation.constraint;
40
+ }
@@ -0,0 +1,111 @@
1
+ // The entry-point guard for an app on `createProcessDatabase`: `db` is synchronous but opening is not, so every
2
+ // process entry (the server's instrumentation, an MCP stdio server, each script) must open the database before its
3
+ // first query. esbuild resolves each entry's import graph (path aliases and re-exports included); an entry that
4
+ // reaches the app's client module must call an opener in its own source. esbuild is an optional peer, loaded here.
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import type { Metafile } from "esbuild";
8
+
9
+ /** A call to `withDatabase(` or `open(` (`processDb.open(` too): what `createProcessDatabase` gives an entry. */
10
+ export const DEFAULT_DATABASE_OPENERS: readonly RegExp[] = [/\bwithDatabase\s*\(/, /\bopen\s*\(/];
11
+
12
+ export interface EntryGuardOptions {
13
+ /** The process entry points, as paths (relative to `workingDir`) or absolute paths. */
14
+ readonly entries: readonly string[];
15
+ /** The app's module that calls `createProcessDatabase` (e.g. `src/db/index.ts`). */
16
+ readonly clientModule: string;
17
+ /** Patterns an entry's own source must match to count as opening the database. Default: `DEFAULT_DATABASE_OPENERS`. */
18
+ readonly opens?: readonly RegExp[];
19
+ /** The directory paths and the app's tsconfig resolve from. Default: `process.cwd()`. */
20
+ readonly workingDir?: string;
21
+ /** Extra import paths esbuild leaves out of the graph (packages are always left out). */
22
+ readonly external?: readonly string[];
23
+ }
24
+
25
+ export interface EntryReach {
26
+ /** The entry as given in `entries`. */
27
+ readonly entry: string;
28
+ /** Whether the entry's own source opens the database. */
29
+ readonly opens: boolean;
30
+ }
31
+
32
+ const ASSET_EXTENSIONS = [".css", ".scss", ".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".woff", ".woff2"];
33
+
34
+ /**
35
+ * The entries whose import graph (static and dynamic imports) reaches `clientModule`, in the order given, each
36
+ * with whether its own source opens the database. Entries that never reach it are left out.
37
+ */
38
+ export async function findEntriesReachingDatabase(options: EntryGuardOptions): Promise<EntryReach[]> {
39
+ const workingDir = options.workingDir ?? process.cwd();
40
+ const clientPath = resolve(workingDir, options.clientModule);
41
+ if (!existsSync(clientPath)) {
42
+ throw new Error(`findEntriesReachingDatabase: the client module does not exist: ${options.clientModule}`);
43
+ }
44
+ const metafile = await buildMetafile(options.entries.map((entry) => resolve(workingDir, entry)), workingDir, options.external ?? []);
45
+ const opens = options.opens ?? DEFAULT_DATABASE_OPENERS;
46
+
47
+ return options.entries.flatMap((entry) => {
48
+ const entryPath = resolve(workingDir, entry);
49
+ if (!reaches(metafile, workingDir, entryPath, clientPath)) return [];
50
+ const source = readFileSync(entryPath, "utf8");
51
+ return [{ entry, opens: opens.some((pattern) => pattern.test(source)) }];
52
+ });
53
+ }
54
+
55
+ /** Throws, naming each one, when an entry reaches `clientModule` without opening the database. */
56
+ export async function expectEntriesOpenDatabase(options: EntryGuardOptions): Promise<void> {
57
+ const missing = (await findEntriesReachingDatabase(options)).filter((reach) => !reach.opens);
58
+ if (missing.length === 0) return;
59
+ throw new Error(
60
+ `These entries reach the database client without opening it (call withDatabase() or open() at the entry):\n` +
61
+ missing.map((reach) => ` - ${reach.entry}`).join("\n"),
62
+ );
63
+ }
64
+
65
+ async function buildMetafile(entryPoints: readonly string[], workingDir: string, external: readonly string[]): Promise<Metafile> {
66
+ const { build } = await loadEsbuild();
67
+ const result = await build({
68
+ entryPoints: [...entryPoints],
69
+ absWorkingDir: workingDir,
70
+ // Never written (`write: false`); esbuild needs an output directory for several entries.
71
+ outdir: resolve(workingDir, ".entry-guard"),
72
+ bundle: true,
73
+ write: false,
74
+ metafile: true,
75
+ platform: "node",
76
+ format: "esm",
77
+ packages: "external",
78
+ external: [...external],
79
+ loader: Object.fromEntries(ASSET_EXTENSIONS.map((extension) => [extension, "empty" as const])),
80
+ logLevel: "silent",
81
+ }).catch((error: unknown) => {
82
+ const reason = error instanceof Error ? error.message : String(error);
83
+ throw new Error(`findEntriesReachingDatabase: esbuild could not follow the entries' imports: ${reason}`, { cause: error });
84
+ });
85
+ return result.metafile;
86
+ }
87
+
88
+ async function loadEsbuild(): Promise<typeof import("esbuild")> {
89
+ try {
90
+ return await import("esbuild");
91
+ } catch (error) {
92
+ throw new Error("findEntriesReachingDatabase: needs the \"esbuild\" package; run `npm install --save-dev esbuild`", { cause: error });
93
+ }
94
+ }
95
+
96
+ function reaches(metafile: Metafile, workingDir: string, entryPath: string, clientPath: string): boolean {
97
+ const keyByPath = new Map(Object.keys(metafile.inputs).map((key) => [resolve(workingDir, key), key]));
98
+ const start = keyByPath.get(entryPath);
99
+ if (start === undefined) return false;
100
+ const seen = new Set<string>([start]);
101
+ const queue = [start];
102
+ for (let key = queue.shift(); key !== undefined; key = queue.shift()) {
103
+ if (resolve(workingDir, key) === clientPath) return true;
104
+ for (const imported of metafile.inputs[key]?.imports ?? []) {
105
+ if (imported.external === true || seen.has(imported.path)) continue;
106
+ seen.add(imported.path);
107
+ queue.push(imported.path);
108
+ }
109
+ }
110
+ return false;
111
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,10 @@ export {
13
13
  type PostgresDatabase,
14
14
  type Queryable,
15
15
  } from "./client.js";
16
- export { closeSharedDatabases, getSharedDatabase } from "./shared.js";
16
+ export { closeSharedDatabase, closeSharedDatabases, getSharedDatabase } from "./shared.js";
17
+ export { closeProcessDatabases, createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
18
+ export { openRawDatabase, queryDatabase, type RawDatabase } from "./raw.js";
19
+ export { findDriverError, isConstraintViolation, type ConstraintViolation, type DriverError } from "./driver-errors.js";
17
20
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase, type CommandDatabase } from "./configured.js";
18
21
  export { computeChecksum, readMigrationFiles, type MigrationFile } from "./migrations/files.js";
19
22
  export {
@@ -35,6 +38,7 @@ export {
35
38
  type MigrationReport,
36
39
  type MigrationStep,
37
40
  } from "./migrations/migrator.js";
41
+ export { drizzleAppMigrations } from "./migrations/drizzle.js";
38
42
  export { adoptModule, type AdoptionReport, type AdoptOptions } from "./migrations/adopt.js";
39
43
  export { exportMigrations } from "./migrations/export.js";
40
44
  export {
@@ -0,0 +1,22 @@
1
+ // The app's own drizzle-kit migrations as an app migration hook: drizzle's migrator for the handle's driver,
2
+ // loaded only when the hook runs, so an app ships just its own driver.
3
+ import { fileURLToPath } from "node:url";
4
+ import type { AppMigrationHook } from "./migrator.js";
5
+
6
+ /**
7
+ * A hook for `app.before` (or `app.after`) that applies the drizzle-kit folder (`meta/_journal.json` and its SQL
8
+ * files) with drizzle's migrator. Drizzle skips what its ledger (`drizzle.__drizzle_migrations`) already holds, so
9
+ * the hook is safe on every `migrate` run. `folder` is a path (relative to the working directory) or a file URL.
10
+ */
11
+ export function drizzleAppMigrations(folder: string | URL): AppMigrationHook {
12
+ const migrationsFolder = typeof folder === "string" ? folder : fileURLToPath(folder);
13
+ return async (handle) => {
14
+ if (handle.kind === "postgres") {
15
+ const { migrate } = await import("drizzle-orm/node-postgres/migrator");
16
+ await migrate(handle.db, { migrationsFolder });
17
+ return;
18
+ }
19
+ const { migrate } = await import("drizzle-orm/pglite/migrator");
20
+ await migrate(handle.db, { migrationsFolder });
21
+ };
22
+ }
package/src/process.ts ADDED
@@ -0,0 +1,160 @@
1
+ // A process-wide database for an app whose code imports one module-level `db` (drizzle queries everywhere)
2
+ // instead of awaiting a handle in every function. `db` is a synchronous stand-in for the shared handle's
3
+ // drizzle instance: each entry point (the server's startup, a script's main) opens it first, and a query
4
+ // before that throws instead of silently opening a second pool.
5
+ import type { Database, DatabaseHandle, DatabaseSchema, CreateDatabaseOptions } from "./client.js";
6
+ import { closeSharedDatabase, getSharedDatabase } from "./shared.js";
7
+
8
+ const NOT_OPEN_MESSAGE = "Database is not open: call open() or withDatabase() at the process entry before querying";
9
+ const REGISTRY_KEY = Symbol.for("@softure-ai/db/process-databases");
10
+
11
+ /** What one process database holds between `open()` and `close()`; on `globalThis` when it has a `globalKey`. */
12
+ interface ProcessState {
13
+ opening: Promise<DatabaseHandle> | undefined;
14
+ opened: { readonly url: string; readonly db: Database<DatabaseSchema> } | undefined;
15
+ }
16
+
17
+ type StateHost = Record<symbol, ProcessState | Set<ProcessState> | undefined>;
18
+
19
+ const host = globalThis as unknown as StateHost;
20
+
21
+ export interface ProcessDatabaseOptions<TSchema extends DatabaseSchema> extends CreateDatabaseOptions {
22
+ /** The app's drizzle schema: `db` is built with it, so `db.query.<table>` is typed and works. */
23
+ readonly schema?: TSchema;
24
+ /**
25
+ * Keeps the open database on `globalThis` under this key, so a module that `next dev` re-evaluates (HMR) finds the
26
+ * database its previous evaluation opened instead of refusing queries. One key per database in the process. The
27
+ * open `db` keeps the schema it was opened with; a changed schema takes effect after `close()` or a restart.
28
+ */
29
+ readonly globalKey?: string;
30
+ }
31
+
32
+ export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema> {
33
+ /** The open database. Usable from module scope; a query before `open()` resolves throws "Database is not open". */
34
+ readonly db: Database<TSchema>;
35
+ /** Opens the process-wide handle for the URL (`getSharedDatabase`), once; later calls return the same handle. */
36
+ readonly open: () => Promise<DatabaseHandle>;
37
+ /** Closes the handle and forgets it, so `db` refuses queries again. Does nothing when not open. */
38
+ readonly close: () => Promise<void>;
39
+ /** For a script: opens, runs `main` with `db`, and closes whether `main` resolves or throws. */
40
+ readonly withDatabase: <T>(main: (db: Database<TSchema>) => Promise<T>) => Promise<T>;
41
+ readonly isOpen: () => boolean;
42
+ }
43
+
44
+ /**
45
+ * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
46
+ * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
47
+ * modules, so the app and the modules share one pool (db README §3, "One handle per process").
48
+ */
49
+ export function createProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema>(
50
+ url: string | (() => string),
51
+ options: ProcessDatabaseOptions<TSchema> = {},
52
+ ): ProcessDatabase<TSchema> {
53
+ const { schema, globalKey, ...databaseOptions } = options;
54
+ const state = getProcessState(globalKey);
55
+
56
+ const open = (): Promise<DatabaseHandle> => {
57
+ if (state.opening === undefined) {
58
+ getRegistry().add(state);
59
+ state.opening = openHandle().catch((error: unknown) => {
60
+ state.opening = undefined;
61
+ getRegistry().delete(state);
62
+ throw error;
63
+ });
64
+ }
65
+ return state.opening;
66
+ };
67
+
68
+ async function openHandle(): Promise<DatabaseHandle> {
69
+ const resolvedUrl = typeof url === "function" ? url() : url;
70
+ const handle = await getSharedDatabase(resolvedUrl, databaseOptions);
71
+ state.opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
72
+ return handle;
73
+ }
74
+
75
+ const close = (): Promise<void> => closeProcessState(state);
76
+
77
+ const withDatabase = async <T>(main: (db: Database<TSchema>) => Promise<T>): Promise<T> => {
78
+ await open();
79
+ try {
80
+ return await main(db);
81
+ } finally {
82
+ await close();
83
+ }
84
+ };
85
+
86
+ // The state holds whichever schema opened it; this instance's callers see it typed with their own.
87
+ const db = createDatabaseProxy(() => state.opened?.db as Database<TSchema> | undefined);
88
+
89
+ return { db, open, close, withDatabase, isOpen: () => state.opened !== undefined };
90
+ }
91
+
92
+ /**
93
+ * Closes every process database this process opened (with or without `globalKey`), so each `db` refuses queries
94
+ * until its `open()`. For tests' `afterEach`; a server's shutdown closes its own.
95
+ */
96
+ export async function closeProcessDatabases(): Promise<void> {
97
+ await Promise.all([...getRegistry()].map(closeProcessState));
98
+ }
99
+
100
+ function getProcessState(globalKey: string | undefined): ProcessState {
101
+ if (globalKey === undefined) return { opening: undefined, opened: undefined };
102
+ const key = Symbol.for(`@softure-ai/db/process:${globalKey}`);
103
+ const existing = host[key];
104
+ if (existing !== undefined && !(existing instanceof Set)) return existing;
105
+ const created: ProcessState = { opening: undefined, opened: undefined };
106
+ host[key] = created;
107
+ return created;
108
+ }
109
+
110
+ function getRegistry(): Set<ProcessState> {
111
+ const existing = host[REGISTRY_KEY];
112
+ if (existing instanceof Set) return existing;
113
+ const created = new Set<ProcessState>();
114
+ host[REGISTRY_KEY] = created;
115
+ return created;
116
+ }
117
+
118
+ async function closeProcessState(state: ProcessState): Promise<void> {
119
+ getRegistry().delete(state);
120
+ if (state.opening === undefined) return;
121
+ const pending = state.opening;
122
+ state.opening = undefined;
123
+ await pending.catch(() => undefined);
124
+ const current = state.opened;
125
+ state.opened = undefined;
126
+ if (current !== undefined) await closeSharedDatabase(current.url);
127
+ }
128
+
129
+ /** The handle's drizzle instance, or a new one over the same pool or client when the app passes its schema. */
130
+ async function buildDatabase<TSchema extends DatabaseSchema>(handle: DatabaseHandle, schema: TSchema | undefined): Promise<Database<TSchema>> {
131
+ // The handle's own instance is typed with the default schema; without the app's schema the two types agree.
132
+ if (schema === undefined) return handle.db as unknown as Database<TSchema>;
133
+ if (handle.kind === "postgres") {
134
+ const { drizzle } = await import("drizzle-orm/node-postgres");
135
+ return drizzle({ client: handle.pool, schema });
136
+ }
137
+ const { drizzle } = await import("drizzle-orm/pglite");
138
+ return drizzle({ client: handle.client, schema });
139
+ }
140
+
141
+ /** A stand-in that forwards every property to the open database, or throws when none is open. */
142
+ function createDatabaseProxy<TSchema extends DatabaseSchema>(getDatabase: () => Database<TSchema> | undefined): Database<TSchema> {
143
+ return new Proxy({} as Database<TSchema>, {
144
+ get(_target, property) {
145
+ const database = getDatabase();
146
+ if (database === undefined) {
147
+ // `await db` and `Promise.resolve(db)` look for `then`: a closed database is just not a thenable.
148
+ if (property === "then") return undefined;
149
+ throw new Error(NOT_OPEN_MESSAGE);
150
+ }
151
+ const value: unknown = Reflect.get(database, property, database);
152
+ return typeof value === "function" ? (value as (...args: unknown[]) => unknown).bind(database) : value;
153
+ },
154
+ has(_target, property) {
155
+ const database = getDatabase();
156
+ if (database === undefined) throw new Error(NOT_OPEN_MESSAGE);
157
+ return Reflect.has(database, property);
158
+ },
159
+ });
160
+ }
package/src/raw.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Raw SQL over whichever driver a handle or URL uses, for operator and report scripts that read with plain SQL
2
+ // instead of drizzle. Values are always passed separately ($1, $2, …), never spliced into the text.
3
+ import { createDatabase, type CreateDatabaseOptions, type DatabaseHandle } from "./client.js";
4
+
5
+ export interface RawDatabase {
6
+ readonly kind: DatabaseHandle["kind"];
7
+ /** Runs `text` with `values` bound to `$1`, `$2`, … and returns the rows. */
8
+ readonly query: <TRow = Record<string, unknown>>(text: string, values?: readonly unknown[]) => Promise<TRow[]>;
9
+ /** Closes the handle this database opened; a `pglite://<dir>` database is written back only then. */
10
+ readonly close: () => Promise<void>;
11
+ }
12
+
13
+ /** Runs a parameterized query on the handle's pool (node-postgres) or client (PGlite) and returns its rows. */
14
+ export async function queryDatabase<TRow = Record<string, unknown>>(
15
+ handle: DatabaseHandle,
16
+ text: string,
17
+ values: readonly unknown[] = [],
18
+ ): Promise<TRow[]> {
19
+ if (handle.kind === "postgres") {
20
+ const result = await handle.pool.query(text, [...values]);
21
+ return result.rows as TRow[];
22
+ }
23
+ const result = await handle.client.query<TRow>(text, [...values]);
24
+ return result.rows;
25
+ }
26
+
27
+ /**
28
+ * Opens a new handle on `url` (not the process-wide one) for a script's raw queries. The script closes it at its
29
+ * end. Throws on an empty URL or an unsupported scheme, as `createDatabase` does.
30
+ */
31
+ export async function openRawDatabase(url: string, options: CreateDatabaseOptions = {}): Promise<RawDatabase> {
32
+ const handle = await createDatabase(url, options);
33
+ return {
34
+ kind: handle.kind,
35
+ query: (text, values) => queryDatabase(handle, text, values),
36
+ close: handle.close,
37
+ };
38
+ }
package/src/shared.ts CHANGED
@@ -25,6 +25,15 @@ export function getSharedDatabase(url: string, options: CreateDatabaseOptions =
25
25
  return handle;
26
26
  }
27
27
 
28
+ /** Closes and forgets the shared handle for `url`, if one is open. For a script's end. */
29
+ export async function closeSharedDatabase(url: string): Promise<void> {
30
+ const handle = host[HANDLES_KEY]?.get(url);
31
+ if (handle === undefined) return;
32
+ host[HANDLES_KEY]?.delete(url);
33
+ const opened = await handle.catch(() => undefined);
34
+ await opened?.close();
35
+ }
36
+
28
37
  /** Closes and forgets every shared handle. For tests and graceful shutdown. */
29
38
  export async function closeSharedDatabases(): Promise<void> {
30
39
  const handles = host[HANDLES_KEY];
package/src/testing.ts CHANGED
@@ -10,6 +10,14 @@ import { createPgliteHandle, type PgliteClientDatabase } from "./client.js";
10
10
  import { migrate, type AppMigrationHook, type AppMigrations } from "./migrations/migrator.js";
11
11
  import { describeProblem } from "./migrations/problems.js";
12
12
 
13
+ export {
14
+ DEFAULT_DATABASE_OPENERS,
15
+ expectEntriesOpenDatabase,
16
+ findEntriesReachingDatabase,
17
+ type EntryGuardOptions,
18
+ type EntryReach,
19
+ } from "./entry-guard.js";
20
+
13
21
  export interface TestDatabase {
14
22
  readonly db: PgliteClientDatabase;
15
23
  readonly client: PGlite;