@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 +11 -0
- package/README.md +58 -16
- package/dist/entry-guard.d.ts +28 -0
- package/dist/entry-guard.d.ts.map +1 -0
- package/dist/entry-guard.js +87 -0
- package/dist/entry-guard.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/drizzle.d.ts +8 -0
- package/dist/migrations/drizzle.d.ts.map +1 -0
- package/dist/migrations/drizzle.js +21 -0
- package/dist/migrations/drizzle.js.map +1 -0
- package/dist/process.d.ts +11 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +56 -22
- package/dist/process.js.map +1 -1
- package/dist/raw.d.ts +16 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +25 -0
- package/dist/raw.js.map +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/dist/testing.js.map +1 -1
- package/package.json +5 -1
- package/src/entry-guard.ts +111 -0
- package/src/index.ts +3 -1
- package/src/migrations/drizzle.ts +22 -0
- package/src/process.ts +70 -20
- package/src/raw.ts +38 -0
- package/src/testing.ts +8 -0
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`)
|
|
12
|
-
(`
|
|
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
|
|
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: (
|
|
296
|
+
before: drizzleAppMigrations("./drizzle"),
|
|
255
297
|
// only when an app migration needs a module table: a second drizzle folder
|
|
256
|
-
// 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: (
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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
|
package/dist/process.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
11
|
-
let opened;
|
|
11
|
+
const { schema, globalKey, ...databaseOptions } = options;
|
|
12
|
+
const state = getProcessState(globalKey);
|
|
12
13
|
const open = () => {
|
|
13
|
-
opening
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 =
|
|
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
|
-
|
|
46
|
-
|
|
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) {
|
package/dist/process.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErE,MAAM,gBAAgB,GAAG,0FAA0F,CAAC;
|
|
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
|
package/dist/raw.js.map
ADDED
|
@@ -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;
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -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;
|
package/dist/testing.js.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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;
|