/** * Module Database ABI v1 (ADR 0050): the stable boundary between Core and a module's database code. * * Only PostgreSQL concepts cross it: SQL text, ordered bound parameter values, decoded result rows * with their column metadata, and transaction scopes that Core controls. **Nothing in this file * names an ORM, a query builder, or a driver.** A module may build its SQL with Drizzle, Kysely, raw * text, or anything else, and Core never learns which; Core may change its own ORM without any * module noticing. * * This is a correctness and compatibility boundary, **not a security sandbox**. Installed modules * remain trusted backend code that runs in the API process with its database role (ADR 0048), and * nothing here, including table prefixes, `SET ROLE`, or PostgreSQL roles, can stop a module from * reaching data that is not its own. */ /** The ABI version these types describe. A module declares it; every executor reports it. */ export const MODULE_DATABASE_ABI_VERSION = 1; export type ModuleDatabaseAbiVersion = typeof MODULE_DATABASE_ABI_VERSION; /** * A scalar parameter value. Deliberately small: each has exactly one PostgreSQL text encoding. * * - `number` is sent as its JavaScript decimal text; `NaN`, `Infinity`, and `-Infinity` as * PostgreSQL's own spellings. * - `bigint` is sent as its decimal text. * - `Uint8Array` is sent as `bytea`. * * A JavaScript `Date` is **not** a parameter: whether it means a `timestamp` or a `timestamptz`, and * in which zone, depends on a column the ABI cannot see. Plain objects are not implicit JSON; encode * JSON explicitly as a string. Identifiers are never parameters. */ export type ModuleDatabaseScalar = null | boolean | number | bigint | string | Uint8Array; /** A parameter value: a scalar, or a PostgreSQL array of scalars, nested for more dimensions. */ export type ModuleDatabaseParameter = ModuleDatabaseScalar | readonly ModuleDatabaseParameter[]; /** * A decoded result value. Values that a generic conversion could change arrive as PostgreSQL's * exact text: `int8`, `numeric`, every date and time type, `json`, and `jsonb`. `bytea` arrives as a * `Uint8Array`, and arrays of the built-in types decode element by element. */ export type ModuleDatabaseValue = | null | boolean | number | string | Uint8Array | readonly ModuleDatabaseValue[]; /** `"object"` rows are keyed by column name; `"array"` rows are positional, in `fields` order. */ export type ModuleDatabaseRowMode = "object" | "array"; export type ModuleDatabaseObjectRow = Readonly>; export type ModuleDatabaseArrayRow = readonly ModuleDatabaseValue[]; /** One query: exactly one PostgreSQL statement with `$1 … $n` placeholders. */ export interface ModuleDatabaseQuery { readonly text: string; readonly values?: readonly ModuleDatabaseParameter[]; /** Defaults to `"object"`. */ readonly rowMode?: ModuleDatabaseRowMode; } /** The ordered column metadata of a result, present even when it has no rows. */ export interface ModuleDatabaseField { readonly name: string; /** The column's PostgreSQL type OID. */ readonly typeOid: number; } export interface ModuleDatabaseQueryResult { /** PostgreSQL's command tag word, such as `SELECT`, `INSERT`, `UPDATE`, or `DELETE`. */ readonly command: string; /** The number of rows PostgreSQL reports as processed, or `null` when it reports none. */ readonly rowCount: number | null; readonly rows: readonly Row[]; readonly fields: readonly ModuleDatabaseField[]; } /** What every executor can do: run one statement. */ export interface ModuleDatabaseQueryable { readonly abiVersion: ModuleDatabaseAbiVersion; query( query: ModuleDatabaseQuery & { readonly rowMode: "array" }, ): Promise>; query( query: ModuleDatabaseQuery & { readonly rowMode?: "object" }, ): Promise>; query( query: ModuleDatabaseQuery, ): Promise>; } export type ModuleDatabaseIsolationLevel = "read committed" | "repeatable read" | "serializable"; /** Transaction options. Any other key or value is refused, never ignored. Omitted, a transaction is * `read committed` and read-write. */ export interface ModuleDatabaseTransactionOptions { readonly isolation?: ModuleDatabaseIsolationLevel; readonly readOnly?: boolean; } /** * The executor inside a transaction or a savepoint. It is valid only while its callback runs: once * that callback settles it is revoked, and every later call fails without reaching PostgreSQL. * * Only the innermost open scope may run statements: while a savepoint is open, its parent refuses * statements and new savepoints. A callback must await every operation it starts before it returns. * One that resolves with work outstanding forgot an `await`: it fails with * `MODULE_DATABASE_SCOPE_WORK_OUTSTANDING` and is rolled back. One that rejects keeps its own error. * Either way, outstanding work never leads to a commit or a release. * * Transaction-control SQL (`BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `RELEASE SAVEPOINT`, …) is * forbidden through `query`; the callbacks control the transaction. */ export interface ModuleDatabaseTransaction extends ModuleDatabaseQueryable { /** Core names the savepoint. A resolved callback with no work outstanding releases it; a rejected * one rolls back to it and keeps its own error. */ savepoint(callback: (savepoint: ModuleDatabaseTransaction) => Promise | T): Promise; } /** * A module's database service under ABI v1. Top-level `query` calls are separate autocommit * statements on whichever pooled connection is free: no session state carries from one to the * next, and none survives the operation that made it. */ export interface ModuleDatabaseExecutor extends ModuleDatabaseQueryable { /** Core begins the transaction, commits when `callback` resolves with no work outstanding, and * rolls back otherwise; a rejected callback's own error is the transaction's error. Core never * retries it. These guarantees apply to conforming use: no transaction-control SQL through * `query`. */ transaction( callback: (transaction: ModuleDatabaseTransaction) => Promise | T, options?: ModuleDatabaseTransactionOptions, ): Promise; } /** * Every failure category an ABI v1 executor reports. A code is a stable category for programmatic * handling and keeps its meaning once published; the specific reason is in the static message. * PostgreSQL's own condition, when there is one, is `sqlState`, a separate field. */ export const MODULE_DATABASE_ERROR_CODES = [ /** The call is not valid ABI use: a malformed query request or callback, a statement text that * holds no statement, an object-mode result with duplicate column names, or a transaction-control * statement sent through `query`. A programming error. * * The last two are only detectable once PostgreSQL has run the statement, so its effects may * already have happened: a mutation with duplicate result columns has been applied (inside a * transaction, it rolls back with it), and transaction control has taken effect, which Core cannot * undo. */ "MODULE_DATABASE_REQUEST_INVALID", /** A parameter value is outside the ABI's parameter set. Refused before anything reaches * PostgreSQL. */ "MODULE_DATABASE_PARAMETER_INVALID", /** The statement did not complete. `sqlState` is present when PostgreSQL reported the condition; * absent when the database could not be reached or the connection failed. */ "MODULE_DATABASE_QUERY_FAILED", /** A transaction option or value is not one the ABI defines. Refused before the transaction * starts. */ "MODULE_DATABASE_TRANSACTION_OPTION_INVALID", /** The transaction could not be begun or completed, or a savepoint could not be completed. * `sqlState` is present when PostgreSQL reported the condition. A transaction left aborted by a * failed statement ends here too: PostgreSQL rolls it back, and nothing is committed. */ "MODULE_DATABASE_TRANSACTION_FAILED", /** The executor's scope has finished, so it is revoked. */ "MODULE_DATABASE_SCOPE_CLOSED", /** The executor's scope is open but not active, because a savepoint inside it is open. */ "MODULE_DATABASE_SCOPE_INACTIVE", /** A transaction or savepoint callback resolved while work it started was still outstanding: a * forgotten `await`. The scope was rolled back, never committed or released. (A callback that * rejects keeps its own error instead.) */ "MODULE_DATABASE_SCOPE_WORK_OUTSTANDING", ] as const; export type ModuleDatabaseErrorCode = (typeof MODULE_DATABASE_ERROR_CODES)[number]; const databaseErrorMessages: Readonly> = Object.freeze({ MODULE_DATABASE_PARAMETER_INVALID: "A database parameter value is not supported.", MODULE_DATABASE_QUERY_FAILED: "The database statement did not complete.", MODULE_DATABASE_REQUEST_INVALID: "The database request is not valid.", MODULE_DATABASE_SCOPE_CLOSED: "The database scope has finished and can no longer be used.", MODULE_DATABASE_SCOPE_INACTIVE: "The database scope cannot be used while a savepoint is open.", MODULE_DATABASE_SCOPE_WORK_OUTSTANDING: "The database scope finished with work still outstanding, so it was rolled back.", MODULE_DATABASE_TRANSACTION_FAILED: "The database transaction could not be completed.", MODULE_DATABASE_TRANSACTION_OPTION_INVALID: "A database transaction option is not supported.", }); const sqlStatePattern = /^[0-9A-Z]{5}$/u; /** * The one error an ABI v1 executor reports, sanitized by construction: a stable `code`, PostgreSQL's * `sqlState` when the database supplied one, and a static message. It never carries SQL text, * parameter values, the driver's message, detail, hint, or object names, a cause, a stack beyond * its own name and message, or any driver or ORM object. * * Recognize it with `isModuleDatabaseError`, never `instanceof`: a module bundles its own copy of * this package (ADR 0049), so class identity across the boundary is not a contract. */ export class ModuleDatabaseError extends Error { readonly code: ModuleDatabaseErrorCode; /** Declared, not initialized, so the property exists only when PostgreSQL supplied a SQLSTATE. */ declare readonly sqlState?: string; constructor(code: ModuleDatabaseErrorCode, options: { readonly sqlState?: string } = {}) { super(databaseErrorMessages[code]); this.name = "ModuleDatabaseError"; this.code = code; if (options.sqlState !== undefined && sqlStatePattern.test(options.sqlState)) { this.sqlState = options.sqlState; } this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } /** Whether `value` is a Module Database ABI error, judged by its shape rather than its class. */ export function isModuleDatabaseError( value: unknown, ): value is Pick { if (!(value instanceof Error) || value.name !== "ModuleDatabaseError") { return false; } const { code, sqlState } = value as { code?: unknown; sqlState?: unknown }; return ( typeof code === "string" && (MODULE_DATABASE_ERROR_CODES as readonly string[]).includes(code) && (sqlState === undefined || (typeof sqlState === "string" && sqlStatePattern.test(sqlState))) ); }