@softure-ai/db 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ 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.7
8
+
9
+ - `createProcessDatabase(url, { schema, max })` returns `{ db, open, close, withDatabase, isOpen }`: a module-level,
10
+ synchronous `db` over the process-wide handle for the URL. A query before `open()` throws `Database is not open`;
11
+ `withDatabase(main)` opens, runs and closes for a script. `url` may be a function, read on `open()` (#313).
12
+ - `findDriverError(error)` (`{ code, constraint, message }` of the Postgres error under drizzle's wrapper) and
13
+ `isConstraintViolation(error, { code, constraint })`, so apps and modules stop copying them (#313).
14
+ - `closeSharedDatabase(url)` closes and forgets one shared handle.
15
+
7
16
  ## 0.1.6
8
17
 
9
18
  - 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,9 @@ The database layer every SOFTURE module stands on. Standard:
7
7
 
8
8
  A pg/PGlite client chosen by URL, a migrator that runs each module's SQL in the module's own
9
9
  Postgres schema (with a ledger, checksums, a lock, a dry run and adoption of existing tables),
10
- `createTestDatabase` for unit tests, and the `softure migrate` command.
10
+ `createTestDatabase` for unit tests, the `softure migrate` command, a module-level process database
11
+ (`createProcessDatabase`) and helpers that read the Postgres error behind a failed query
12
+ (`findDriverError`, `isConstraintViolation`).
11
13
 
12
14
  ## 2. Installation
13
15
 
@@ -140,6 +142,49 @@ export async function getDb() {
140
142
  the modules use can call it too. A migration run on a PGlite handle the app shares keeps the app's
141
143
  session settings (`TimeZone`, `search_path`); only what the run changed is put back.
142
144
 
145
+ ### A module-level `db`
146
+
147
+ An app whose code imports one `db` everywhere, instead of awaiting a handle in each function,
148
+ creates it once with `createProcessDatabase`. `db` stands in for the shared handle's drizzle instance
149
+ (the one `getSharedDatabase(url)` gives the modules, so there is still one pool); every entry point
150
+ opens it first, and a query before that throws `Database is not open`:
151
+
152
+ ```ts
153
+ // db/index.ts
154
+ import { createProcessDatabase } from "@softure-ai/db";
155
+ import * as schema from "./schema";
156
+
157
+ // A function: read on open(), so a script may load its environment after importing this file.
158
+ export const { db, open, close, withDatabase } = createProcessDatabase(() => process.env.DATABASE_URL ?? "", { schema });
159
+
160
+ // instrumentation.ts (the server): open once at startup
161
+ export async function register() {
162
+ if (process.env.NEXT_RUNTIME === "nodejs") await (await import("./db")).open();
163
+ }
164
+
165
+ // scripts/import.ts: opens, runs, closes (also when main throws)
166
+ await withDatabase(async (db) => { /* … */ });
167
+ ```
168
+
169
+ `open()` is idempotent; `close()` closes the shared handle for that URL, which the modules use too,
170
+ so only a script's end calls it. With `schema`, `db.query.<table>` is typed and works.
171
+
172
+ ### Driver errors
173
+
174
+ `findDriverError(error)` returns `{ code, constraint, message }` of the Postgres error behind a failed
175
+ query (node-postgres and PGlite alike; drizzle wraps it as the `cause`), or `undefined`.
176
+ `isConstraintViolation(error, { code, constraint })` answers whether it is that SQLSTATE on that
177
+ constraint (any constraint when `constraint` is left out):
178
+
179
+ ```ts
180
+ try {
181
+ await db.insert(articles).values(article);
182
+ } catch (error) {
183
+ if (!isConstraintViolation(error, { code: "23505", constraint: "articles_slug_key" })) throw error;
184
+ return err("slug_taken");
185
+ }
186
+ ```
187
+
143
188
  ## 4. Mounting
144
189
 
145
190
  Nothing to mount. Migrations run as a deploy step:
@@ -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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
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 { createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
4
+ export { findDriverError, isConstraintViolation, type ConstraintViolation, type DriverError } from "./driver-errors.js";
3
5
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase, type CommandDatabase } from "./configured.js";
4
6
  export { computeChecksum, readMigrationFiles, type MigrationFile } from "./migrations/files.js";
5
7
  export { describeProblem, type MigrationErrorCode, type MigrationFailure, type MigrationProblem, type MigrationResult, } from "./migrations/problems.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACxG,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACxH,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC5F,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
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 { createProcessDatabase } from "./process.js";
6
+ export { findDriverError, isConstraintViolation } from "./driver-errors.js";
5
7
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase } from "./configured.js";
6
8
  export { computeChecksum, readMigrationFiles } from "./migrations/files.js";
7
9
  export { describeProblem, } from "./migrations/problems.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wGAAwG;AACxG,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,GAQjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAwB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAsB,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,GAKhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,GAQjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAA0C,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,GAGxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAyC,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wGAAwG;AACxG,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,GAQjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAqD,MAAM,cAAc,CAAC;AACxG,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAA8C,MAAM,oBAAoB,CAAC;AACxH,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,mBAAmB,EAAwB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAsB,MAAM,uBAAuB,CAAC;AAChG,OAAO,EACL,eAAe,GAKhB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,OAAO,EACP,cAAc,EACd,gBAAgB,GAQjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAA0C,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EACL,uBAAuB,GAGxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAyC,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,23 @@
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
+ export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema> {
7
+ /** The open database. Usable from module scope; a query before `open()` resolves throws "Database is not open". */
8
+ readonly db: Database<TSchema>;
9
+ /** Opens the process-wide handle for the URL (`getSharedDatabase`), once; later calls return the same handle. */
10
+ readonly open: () => Promise<DatabaseHandle>;
11
+ /** Closes the handle and forgets it, so `db` refuses queries again. Does nothing when not open. */
12
+ readonly close: () => Promise<void>;
13
+ /** For a script: opens, runs `main` with `db`, and closes whether `main` resolves or throws. */
14
+ readonly withDatabase: <T>(main: (db: Database<TSchema>) => Promise<T>) => Promise<T>;
15
+ readonly isOpen: () => boolean;
16
+ }
17
+ /**
18
+ * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
19
+ * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
20
+ * modules, so the app and the modules share one pool (db README §3, "One handle per process").
21
+ */
22
+ export declare function createProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema>(url: string | (() => string), options?: ProcessDatabaseOptions<TSchema>): ProcessDatabase<TSchema>;
23
+ //# 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;AAKnG,MAAM,WAAW,sBAAsB,CAAC,OAAO,SAAS,cAAc,CAAE,SAAQ,qBAAqB;IACnG,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe,CAAC,OAAO,SAAS,cAAc,GAAG,cAAc;IAC9E,mHAAmH;IACnH,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,iHAAiH;IACjH,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7C,mGAAmG;IACnG,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACtF,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,cAAc,GAAG,cAAc,EACnF,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,EAC5B,OAAO,GAAE,sBAAsB,CAAC,OAAO,CAAM,GAC5C,eAAe,CAAC,OAAO,CAAC,CA0C1B"}
@@ -0,0 +1,82 @@
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
+ /**
4
+ * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
5
+ * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
6
+ * modules, so the app and the modules share one pool (db README §3, "One handle per process").
7
+ */
8
+ export function createProcessDatabase(url, options = {}) {
9
+ const { schema, ...databaseOptions } = options;
10
+ let opening;
11
+ let opened;
12
+ const open = () => {
13
+ opening ??= openHandle().catch((error) => {
14
+ opening = undefined;
15
+ throw error;
16
+ });
17
+ return opening;
18
+ };
19
+ async function openHandle() {
20
+ const resolvedUrl = typeof url === "function" ? url() : url;
21
+ const handle = await getSharedDatabase(resolvedUrl, databaseOptions);
22
+ opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
23
+ return handle;
24
+ }
25
+ const close = async () => {
26
+ if (opening === undefined)
27
+ return;
28
+ const pending = opening;
29
+ opening = undefined;
30
+ await pending.catch(() => undefined);
31
+ const current = opened;
32
+ opened = undefined;
33
+ if (current !== undefined)
34
+ await closeSharedDatabase(current.url);
35
+ };
36
+ const withDatabase = async (main) => {
37
+ await open();
38
+ try {
39
+ return await main(db);
40
+ }
41
+ finally {
42
+ await close();
43
+ }
44
+ };
45
+ const db = createDatabaseProxy(() => opened?.db);
46
+ return { db, open, close, withDatabase, isOpen: () => opened !== undefined };
47
+ }
48
+ /** The handle's drizzle instance, or a new one over the same pool or client when the app passes its schema. */
49
+ async function buildDatabase(handle, schema) {
50
+ // The handle's own instance is typed with the default schema; without the app's schema the two types agree.
51
+ if (schema === undefined)
52
+ return handle.db;
53
+ if (handle.kind === "postgres") {
54
+ const { drizzle } = await import("drizzle-orm/node-postgres");
55
+ return drizzle({ client: handle.pool, schema });
56
+ }
57
+ const { drizzle } = await import("drizzle-orm/pglite");
58
+ return drizzle({ client: handle.client, schema });
59
+ }
60
+ /** A stand-in that forwards every property to the open database, or throws when none is open. */
61
+ function createDatabaseProxy(getDatabase) {
62
+ return new Proxy({}, {
63
+ get(_target, property) {
64
+ const database = getDatabase();
65
+ if (database === undefined) {
66
+ // `await db` and `Promise.resolve(db)` look for `then`: a closed database is just not a thenable.
67
+ if (property === "then")
68
+ return undefined;
69
+ throw new Error(NOT_OPEN_MESSAGE);
70
+ }
71
+ const value = Reflect.get(database, property, database);
72
+ return typeof value === "function" ? value.bind(database) : value;
73
+ },
74
+ has(_target, property) {
75
+ const database = getDatabase();
76
+ if (database === undefined)
77
+ throw new Error(NOT_OPEN_MESSAGE);
78
+ return Reflect.has(database, property);
79
+ },
80
+ });
81
+ }
82
+ //# 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/shared.d.ts CHANGED
@@ -4,6 +4,8 @@ import { type CreateDatabaseOptions, type DatabaseHandle } from "./client.js";
4
4
  * A failed open is not kept, so the next call tries again.
5
5
  */
6
6
  export declare function getSharedDatabase(url: string, options?: CreateDatabaseOptions): Promise<DatabaseHandle>;
7
+ /** Closes and forgets the shared handle for `url`, if one is open. For a script's end. */
8
+ export declare function closeSharedDatabase(url: string): Promise<void>;
7
9
  /** Closes and forgets every shared handle. For tests and graceful shutdown. */
8
10
  export declare function closeSharedDatabases(): Promise<void>;
9
11
  //# sourceMappingURL=shared.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAQ9F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,cAAc,CAAC,CAS3G;AAED,+EAA+E;AAC/E,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1D"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAQ9F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,OAAO,CAAC,cAAc,CAAC,CAS3G;AAED,0FAA0F;AAC1F,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMpE;AAED,+EAA+E;AAC/E,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1D"}
package/dist/shared.js CHANGED
@@ -19,6 +19,15 @@ export function getSharedDatabase(url, options = {}) {
19
19
  }
20
20
  return handle;
21
21
  }
22
+ /** Closes and forgets the shared handle for `url`, if one is open. For a script's end. */
23
+ export async function closeSharedDatabase(url) {
24
+ const handle = host[HANDLES_KEY]?.get(url);
25
+ if (handle === undefined)
26
+ return;
27
+ host[HANDLES_KEY]?.delete(url);
28
+ const opened = await handle.catch(() => undefined);
29
+ await opened?.close();
30
+ }
22
31
  /** Closes and forgets every shared handle. For tests and graceful shutdown. */
23
32
  export async function closeSharedDatabases() {
24
33
  const handles = host[HANDLES_KEY];
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,oGAAoG;AACpG,8FAA8F;AAC9F,sFAAsF;AACtF,OAAO,EAAE,cAAc,EAAmD,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;AAIhE,MAAM,IAAI,GAAG,UAAmC,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,UAAiC,EAAE;IAChF,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,GAAG,EAAmC,CAAC,CAAC;IACnF,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAClC,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChH,CAAC"}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,oGAAoG;AACpG,8FAA8F;AAC9F,sFAAsF;AACtF,OAAO,EAAE,cAAc,EAAmD,MAAM,aAAa,CAAC;AAE9F,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;AAIhE,MAAM,IAAI,GAAG,UAAmC,CAAC;AAEjD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,UAAiC,EAAE;IAChF,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,GAAG,EAAmC,CAAC,CAAC;IACnF,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAW;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO;IACjC,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACnD,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC;AACxB,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO;IAClC,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softure-ai/db",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
@@ -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
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,9 @@ 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 { createProcessDatabase, type ProcessDatabase, type ProcessDatabaseOptions } from "./process.js";
18
+ export { findDriverError, isConstraintViolation, type ConstraintViolation, type DriverError } from "./driver-errors.js";
17
19
  export { closeConfiguredDatabases, getConfiguredDatabase, openCommandDatabase, type CommandDatabase } from "./configured.js";
18
20
  export { computeChecksum, readMigrationFiles, type MigrationFile } from "./migrations/files.js";
19
21
  export {
package/src/process.ts ADDED
@@ -0,0 +1,110 @@
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
+
10
+ export interface ProcessDatabaseOptions<TSchema extends DatabaseSchema> extends CreateDatabaseOptions {
11
+ /** The app's drizzle schema: `db` is built with it, so `db.query.<table>` is typed and works. */
12
+ readonly schema?: TSchema;
13
+ }
14
+
15
+ export interface ProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema> {
16
+ /** The open database. Usable from module scope; a query before `open()` resolves throws "Database is not open". */
17
+ readonly db: Database<TSchema>;
18
+ /** Opens the process-wide handle for the URL (`getSharedDatabase`), once; later calls return the same handle. */
19
+ readonly open: () => Promise<DatabaseHandle>;
20
+ /** Closes the handle and forgets it, so `db` refuses queries again. Does nothing when not open. */
21
+ readonly close: () => Promise<void>;
22
+ /** For a script: opens, runs `main` with `db`, and closes whether `main` resolves or throws. */
23
+ readonly withDatabase: <T>(main: (db: Database<TSchema>) => Promise<T>) => Promise<T>;
24
+ readonly isOpen: () => boolean;
25
+ }
26
+
27
+ /**
28
+ * A module-level database for the process. `url` may be a function, read on `open()`, so a script can load its
29
+ * environment after importing the module that creates it. The handle is the one `getSharedDatabase(url)` gives the
30
+ * modules, so the app and the modules share one pool (db README §3, "One handle per process").
31
+ */
32
+ export function createProcessDatabase<TSchema extends DatabaseSchema = DatabaseSchema>(
33
+ url: string | (() => string),
34
+ options: ProcessDatabaseOptions<TSchema> = {},
35
+ ): ProcessDatabase<TSchema> {
36
+ const { schema, ...databaseOptions } = options;
37
+ let opening: Promise<DatabaseHandle> | undefined;
38
+ let opened: { readonly url: string; readonly db: Database<TSchema> } | undefined;
39
+
40
+ const open = (): Promise<DatabaseHandle> => {
41
+ opening ??= openHandle().catch((error: unknown) => {
42
+ opening = undefined;
43
+ throw error;
44
+ });
45
+ return opening;
46
+ };
47
+
48
+ async function openHandle(): Promise<DatabaseHandle> {
49
+ const resolvedUrl = typeof url === "function" ? url() : url;
50
+ const handle = await getSharedDatabase(resolvedUrl, databaseOptions);
51
+ opened = { url: resolvedUrl, db: await buildDatabase(handle, schema) };
52
+ return handle;
53
+ }
54
+
55
+ const close = async (): Promise<void> => {
56
+ if (opening === undefined) return;
57
+ const pending = opening;
58
+ opening = undefined;
59
+ await pending.catch(() => undefined);
60
+ const current = opened;
61
+ opened = undefined;
62
+ if (current !== undefined) await closeSharedDatabase(current.url);
63
+ };
64
+
65
+ const withDatabase = async <T>(main: (db: Database<TSchema>) => Promise<T>): Promise<T> => {
66
+ await open();
67
+ try {
68
+ return await main(db);
69
+ } finally {
70
+ await close();
71
+ }
72
+ };
73
+
74
+ const db = createDatabaseProxy(() => opened?.db);
75
+
76
+ return { db, open, close, withDatabase, isOpen: () => opened !== undefined };
77
+ }
78
+
79
+ /** The handle's drizzle instance, or a new one over the same pool or client when the app passes its schema. */
80
+ async function buildDatabase<TSchema extends DatabaseSchema>(handle: DatabaseHandle, schema: TSchema | undefined): Promise<Database<TSchema>> {
81
+ // The handle's own instance is typed with the default schema; without the app's schema the two types agree.
82
+ if (schema === undefined) return handle.db as unknown as Database<TSchema>;
83
+ if (handle.kind === "postgres") {
84
+ const { drizzle } = await import("drizzle-orm/node-postgres");
85
+ return drizzle({ client: handle.pool, schema });
86
+ }
87
+ const { drizzle } = await import("drizzle-orm/pglite");
88
+ return drizzle({ client: handle.client, schema });
89
+ }
90
+
91
+ /** A stand-in that forwards every property to the open database, or throws when none is open. */
92
+ function createDatabaseProxy<TSchema extends DatabaseSchema>(getDatabase: () => Database<TSchema> | undefined): Database<TSchema> {
93
+ return new Proxy({} as Database<TSchema>, {
94
+ get(_target, property) {
95
+ const database = getDatabase();
96
+ if (database === undefined) {
97
+ // `await db` and `Promise.resolve(db)` look for `then`: a closed database is just not a thenable.
98
+ if (property === "then") return undefined;
99
+ throw new Error(NOT_OPEN_MESSAGE);
100
+ }
101
+ const value: unknown = Reflect.get(database, property, database);
102
+ return typeof value === "function" ? (value as (...args: unknown[]) => unknown).bind(database) : value;
103
+ },
104
+ has(_target, property) {
105
+ const database = getDatabase();
106
+ if (database === undefined) throw new Error(NOT_OPEN_MESSAGE);
107
+ return Reflect.has(database, property);
108
+ },
109
+ });
110
+ }
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];