import type { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg'; /** * The subset of pg's API the worker services actually need: a `query()` that * takes SQL plus positional params. * * Services are typed against this rather than against `Client` so they work * unchanged whether they are handed a `Pool` (the worker process — see * index.ts), a `PoolClient` checked out for a transaction, or a plain `Client` * (the one-off maintenance scripts in ./scripts, which are single-threaded and * own their connection). * * IMPORTANT: a `Queryable` gives NO transaction guarantees. When a `Pool` is * behind it, consecutive `query()` calls may land on different connections, so * bare `BEGIN`/`COMMIT` must never be issued through it — check out a dedicated * client with `withTransaction()` instead. */ export interface Queryable { query( sql: string, params?: any[] ): Promise>; } /** * Run `fn` inside a transaction on a dedicated pooled connection, committing on * success and rolling back on any throw. The client is always released. * * This is the only correct way to run a transaction in the worker: the process * runs BullMQ with `concurrency: 10`, so issuing `BEGIN` on a shared connection * would enrol another job's unrelated queries in this transaction — and discard * them on `ROLLBACK`. (The backend documents the same hazard as its reason for * using a `Pool`; see backend/src/app.ts.) */ export async function withTransaction( pool: Pool, fn: (client: PoolClient) => Promise ): Promise { const client = await pool.connect(); try { await client.query('BEGIN'); const result = await fn(client); await client.query('COMMIT'); return result; } catch (err) { try { await client.query('ROLLBACK'); } catch (rollbackErr) { // A failed ROLLBACK means the connection is unusable; log and move on — // release() below discards it rather than returning it to the pool. console.error('[DB] ROLLBACK failed:', rollbackErr); } throw err; } finally { client.release(); } }