import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; import { Pool, type PoolConfig } from "pg"; import { identityAdapterSchema } from "./schema.js"; export type IdentityDrizzleDatabase = NodePgDatabase; export type IdentityDatabaseErrorCode = "IDENTITY_DATABASE_CONFIGURATION_INVALID"; const maximumPostgresTimeoutMs = 2_147_483_647; /** Never carries the offending connection string, host, or underlying driver error. */ export class IdentityDatabaseError extends Error { readonly code: IdentityDatabaseErrorCode; constructor(code: IdentityDatabaseErrorCode) { super("Identity database configuration is invalid."); this.name = "IdentityDatabaseError"; this.code = code; this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } export interface IdentityDatabaseOptions { readonly connectionTimeoutMs?: number; /** Bounds both `query_timeout` and `statement_timeout`: a hung or unexpectedly slow query on * the identity connection is aborted rather than holding the pool connection (and the caller) * indefinitely. `connectionTimeoutMs` alone only bounds establishing a connection — it does * nothing once a query is already running on one. */ readonly operationTimeoutMs?: number; } export interface IdentityDatabaseHandle { readonly close: () => Promise; readonly db: IdentityDrizzleDatabase; } function validateConnectionString(connectionString: string): string { if (connectionString.trim().length === 0) { throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID"); } let protocol: string; try { protocol = new URL(connectionString).protocol; } catch { throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID"); } if (protocol !== "postgres:" && protocol !== "postgresql:") { throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID"); } return connectionString; } /** Mirrors `core/src/migrations/postgres-database.ts`'s timeout validation exactly: an explicit * timeout must be a positive safe integer no larger than PostgreSQL's own maximum, or it is * rejected as invalid configuration rather than silently passed through to `pg`. */ function validateTimeout(value: unknown): number | undefined { if (value === undefined) { return undefined; } if ( typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > maximumPostgresTimeoutMs ) { throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID"); } return value; } /** * Explicitly constructs the Core-owned Drizzle database instance used by the identity factory. * Building the `pg.Pool` performs no network I/O until a query is issued, and this function is * only ever invoked by explicit caller code — never during module import. */ export function createIdentityDrizzleDatabase( connectionString: string, options: IdentityDatabaseOptions = {}, ): IdentityDatabaseHandle { const validatedConnectionString = validateConnectionString(connectionString); const validatedConnectionTimeoutMs = validateTimeout(options.connectionTimeoutMs); const validatedOperationTimeoutMs = validateTimeout(options.operationTimeoutMs); const poolConfig: PoolConfig = { connectionString: validatedConnectionString, ...(validatedConnectionTimeoutMs === undefined ? {} : { connectionTimeoutMillis: validatedConnectionTimeoutMs }), ...(validatedOperationTimeoutMs === undefined ? {} : { query_timeout: validatedOperationTimeoutMs, statement_timeout: validatedOperationTimeoutMs, }), }; const pool = new Pool(poolConfig); // pg's own documentation requires an error listener on the pool: an idle client can be // terminated by the server (for example, its backend is killed, or the network drops) after // being returned to the pool, and the pool surfaces that as an 'error' event on itself rather // than on any in-flight query. Without a listener, that event is an unhandled Node.js error and // can crash the process even though no caller is doing anything wrong at the time. pool.on("error", () => undefined); const db = drizzle(pool, { schema: identityAdapterSchema }); return Object.freeze({ close: () => pool.end(), db, }); } /** * A typed Drizzle view over an externally owned pool — for example one shared with the * authorization schema via `createValidatedPostgresPool` (`core/src/database/pool.ts`). Unlike * `createIdentityDrizzleDatabase`, this function never constructs, registers an error listener * on, or closes a pool itself: the caller retains that responsibility exactly once, regardless of * how many typed views it creates over the same pool. */ export function createIdentityDrizzleView(pool: Pool): IdentityDrizzleDatabase { return drizzle(pool, { schema: identityAdapterSchema }); }