@softure-ai/db 0.1.7 → 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,17 @@ 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
+
7
18
  ## 0.1.7
8
19
 
9
20
  - `createProcessDatabase(url, { schema, max })` returns `{ db, open, close, withDatabase, isOpen }`: a module-level,
package/README.md CHANGED
@@ -8,8 +8,9 @@ The database layer every SOFTURE module stands on. Standard:
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
10
  `createTestDatabase` for unit tests, the `softure migrate` command, a module-level process database
11
- (`createProcessDatabase`) and helpers that read the Postgres error behind a failed query
12
- (`findDriverError`, `isConstraintViolation`).
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`).
13
14
 
14
15
  ## 2. Installation
15
16
 
@@ -169,6 +170,57 @@ await withDatabase(async (db) => { /* … */ });
169
170
  `open()` is idempotent; `close()` closes the shared handle for that URL, which the modules use too,
170
171
  so only a script's end calls it. With `schema`, `db.query.<table>` is typed and works.
171
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
+
172
224
  ### Driver errors
173
225
 
174
226
  `findDriverError(error)` returns `{ code, constraint, message }` of the Postgres error behind a failed
@@ -238,22 +290,12 @@ object to the migrate script and to `createTestDatabase`:
238
290
 
239
291
  ```ts
240
292
  // db/app-migrations.ts
241
- import type { AppMigrations, DatabaseHandle } from "@softure-ai/db";
242
-
243
- async function runDrizzle(handle: DatabaseHandle, migrationsFolder: string): Promise<void> {
244
- if (handle.kind === "postgres") {
245
- const { migrate } = await import("drizzle-orm/node-postgres/migrator");
246
- await migrate(handle.db, { migrationsFolder });
247
- } else {
248
- const { migrate } = await import("drizzle-orm/pglite/migrator");
249
- await migrate(handle.db, { migrationsFolder });
250
- }
251
- }
293
+ import { drizzleAppMigrations, type AppMigrations } from "@softure-ai/db";
252
294
 
253
295
  export const appMigrations: AppMigrations = {
254
- before: (handle) => runDrizzle(handle, "./drizzle"),
296
+ before: drizzleAppMigrations("./drizzle"),
255
297
  // only when an app migration needs a module table: a second drizzle folder
256
- // after: (handle) => runDrizzle(handle, "./drizzle-after"),
298
+ // after: drizzleAppMigrations("./drizzle-after"),
257
299
  };
258
300
 
259
301
  // scripts/migrate.ts
@@ -276,7 +318,7 @@ file the app's own history creates; `migrate` then adopts or migrates by itself,
276
318
 
277
319
  ```ts
278
320
  export const appMigrations: AppMigrations = {
279
- before: (handle) => runDrizzle(handle, "./drizzle"),
321
+ before: drizzleAppMigrations("./drizzle"),
280
322
  // the app's history creates auth's 0001 (users, sessions); 0002.. are new to it
281
323
  baseline: { auth: 1 },
282
324
  };
@@ -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":"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"}
package/dist/index.d.ts CHANGED
@@ -1,11 +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
2
  export { closeSharedDatabase, closeSharedDatabases, getSharedDatabase } from "./shared.js";
3
- export { createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
3
+ export { closeProcessDatabases, createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
4
+ export { openRawDatabase, queryDatabase, type RawDatabase } from "./raw.js";
4
5
  export { findDriverError, isConstraintViolation, type ConstraintViolation, type DriverError } from "./driver-errors.js";
5
6
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase, type CommandDatabase } from "./configured.js";
6
7
  export { computeChecksum, readMigrationFiles, type MigrationFile } from "./migrations/files.js";
7
8
  export { describeProblem, type MigrationErrorCode, type MigrationFailure, type MigrationProblem, type MigrationResult, } from "./migrations/problems.js";
8
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";
9
11
  export { adoptModule, type AdoptionReport, type AdoptOptions } from "./migrations/adopt.js";
10
12
  export { exportMigrations } from "./migrations/export.js";
11
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,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACxG,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,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
@@ -2,12 +2,14 @@
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
4
  export { closeSharedDatabase, closeSharedDatabases, getSharedDatabase } from "./shared.js";
5
- export { createProcessDatabase } from "./process.js";
5
+ export { closeProcessDatabases, createProcessDatabase } from "./process.js";
6
+ export { openRawDatabase, queryDatabase } from "./raw.js";
6
7
  export { findDriverError, isConstraintViolation } from "./driver-errors.js";
7
8
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase } from "./configured.js";
8
9
  export { computeChecksum, readMigrationFiles } from "./migrations/files.js";
9
10
  export { describeProblem, } from "./migrations/problems.js";
10
11
  export { migrate, planMigrations, runAppMigrations, } from "./migrations/migrator.js";
12
+ export { drizzleAppMigrations } from "./migrations/drizzle.js";
11
13
  export { adoptModule } from "./migrations/adopt.js";
12
14
  export { exportMigrations } from "./migrations/export.js";
13
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,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAqD,MAAM,cAAc,CAAC;AACxG,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,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"}
package/dist/process.d.ts CHANGED
@@ -2,6 +2,12 @@ import type { Database, DatabaseHandle, DatabaseSchema, CreateDatabaseOptions }
2
2
  export interface ProcessDatabaseOptions<TSchema extends DatabaseSchema> extends CreateDatabaseOptions {
3
3
  /** The app's drizzle schema: `db` is built with it, so `db.query.<table>` is typed and works. */
4
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;
5
11
  }
6
12
  export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema> {
7
13
  /** The open database. Usable from module scope; a query before `open()` resolves throws "Database is not open". */
@@ -20,4 +26,9 @@ export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema
20
26
  * modules, so the app and the modules share one pool (db README §3, "One handle per process").
21
27
  */
22
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>;
23
34
  //# sourceMappingURL=process.d.ts.map
@@ -1 +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;AAKnG,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,qBAAqB;IACnG,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;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,CA0C1B"}
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"}
package/dist/process.js CHANGED
@@ -1,38 +1,33 @@
1
1
  import { closeSharedDatabase, getSharedDatabase } from "./shared.js";
2
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;
3
5
  /**
4
6
  * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
5
7
  * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
6
8
  * modules, so the app and the modules share one pool (db README §3, "One handle per process").
7
9
  */
8
10
  export function createProcessDatabase(url, options = {}) {
9
- const { schema, ...databaseOptions } = options;
10
- let opening;
11
- let opened;
11
+ const { schema, globalKey, ...databaseOptions } = options;
12
+ const state = getProcessState(globalKey);
12
13
  const open = () => {
13
- opening ??= openHandle().catch((error) => {
14
- opening = undefined;
15
- throw error;
16
- });
17
- return opening;
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;
18
23
  };
19
24
  async function openHandle() {
20
25
  const resolvedUrl = typeof url === "function" ? url() : url;
21
26
  const handle = await getSharedDatabase(resolvedUrl, databaseOptions);
22
- opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
27
+ state.opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
23
28
  return handle;
24
29
  }
25
- const close = async () => {
26
- if (opening === undefined)
27
- return;
28
- const pending = opening;
29
- opening = undefined;
30
- await pending.catch(() => undefined);
31
- const current = opened;
32
- opened = undefined;
33
- if (current !== undefined)
34
- await closeSharedDatabase(current.url);
35
- };
30
+ const close = () => closeProcessState(state);
36
31
  const withDatabase = async (main) => {
37
32
  await open();
38
33
  try {
@@ -42,8 +37,47 @@ export function createProcessDatabase(url, options = {}) {
42
37
  await close();
43
38
  }
44
39
  };
45
- const db = createDatabaseProxy(() => opened?.db);
46
- return { db, open, close, withDatabase, isOpen: () => opened !== undefined };
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);
47
81
  }
48
82
  /** The handle's drizzle instance, or a new one over the same pool or client when the app passes its schema. */
49
83
  async function buildDatabase(handle, schema) {
@@ -1 +1 @@
1
- {"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"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"}
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/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":"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"}
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.7",
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,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
@@ -14,7 +14,8 @@ export {
14
14
  type Queryable,
15
15
  } from "./client.js";
16
16
  export { closeSharedDatabase, closeSharedDatabases, getSharedDatabase } from "./shared.js";
17
- export { createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
17
+ export { closeProcessDatabases, createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
18
+ export { openRawDatabase, queryDatabase, type RawDatabase } from "./raw.js";
18
19
  export { findDriverError, isConstraintViolation, type ConstraintViolation, type DriverError } from "./driver-errors.js";
19
20
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase, type CommandDatabase } from "./configured.js";
20
21
  export { computeChecksum, readMigrationFiles, type MigrationFile } from "./migrations/files.js";
@@ -37,6 +38,7 @@ export {
37
38
  type MigrationReport,
38
39
  type MigrationStep,
39
40
  } from "./migrations/migrator.js";
41
+ export { drizzleAppMigrations } from "./migrations/drizzle.js";
40
42
  export { adoptModule, type AdoptionReport, type AdoptOptions } from "./migrations/adopt.js";
41
43
  export { exportMigrations } from "./migrations/export.js";
42
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 CHANGED
@@ -6,10 +6,27 @@ import type { Database, DatabaseHandle, DatabaseSchema, CreateDatabaseOptions }
6
6
  import { closeSharedDatabase, getSharedDatabase } from "./shared.js";
7
7
 
8
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;
9
20
 
10
21
  export interface ProcessDatabaseOptions<TSchema extends DatabaseSchema> extends CreateDatabaseOptions {
11
22
  /** The app's drizzle schema: `db` is built with it, so `db.query.<table>` is typed and works. */
12
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;
13
30
  }
14
31
 
15
32
  export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema> {
@@ -33,34 +50,29 @@ export function createProcessDatabase<TSchema extends DatabaseSchema = DatabaseS
33
50
  url: string | (() => string),
34
51
  options: ProcessDatabaseOptions<TSchema> = {},
35
52
  ): ProcessDatabase<TSchema> {
36
- const { schema, ...databaseOptions } = options;
37
- let opening: Promise<DatabaseHandle> | undefined;
38
- let opened: { readonly url: string; readonly db: Database<TSchema> } | undefined;
53
+ const { schema, globalKey, ...databaseOptions } = options;
54
+ const state = getProcessState(globalKey);
39
55
 
40
56
  const open = (): Promise<DatabaseHandle> => {
41
- opening ??= openHandle().catch((error: unknown) => {
42
- opening = undefined;
43
- throw error;
44
- });
45
- return opening;
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;
46
66
  };
47
67
 
48
68
  async function openHandle(): Promise<DatabaseHandle> {
49
69
  const resolvedUrl = typeof url === "function" ? url() : url;
50
70
  const handle = await getSharedDatabase(resolvedUrl, databaseOptions);
51
- opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
71
+ state.opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
52
72
  return handle;
53
73
  }
54
74
 
55
- const close = async (): Promise<void> => {
56
- if (opening === undefined) return;
57
- const pending = opening;
58
- opening = undefined;
59
- await pending.catch(() => undefined);
60
- const current = opened;
61
- opened = undefined;
62
- if (current !== undefined) await closeSharedDatabase(current.url);
63
- };
75
+ const close = (): Promise<void> => closeProcessState(state);
64
76
 
65
77
  const withDatabase = async <T>(main: (db: Database<TSchema>) => Promise<T>): Promise<T> => {
66
78
  await open();
@@ -71,9 +83,47 @@ export function createProcessDatabase<TSchema extends DatabaseSchema = DatabaseS
71
83
  }
72
84
  };
73
85
 
74
- const db = createDatabaseProxy(() => opened?.db);
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
+ }
75
117
 
76
- return { db, open, close, withDatabase, isOpen: () => opened !== undefined };
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);
77
127
  }
78
128
 
79
129
  /** The handle's drizzle instance, or a new one over the same pool or client when the app passes its schema. */
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/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;