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