/** * T2 — Postgres pool + typed `query` wrapper. PARAMETERIZED ONLY: the wrapper accepts * `(sql, params)` and passes params to the driver separately — there is NO string-interpolation * path, so SQL injection via value concatenation is structurally impossible (INV5-adjacent, §7). * * The client is injected (`Queryable`) so the wrapper is unit-testable without a live DB; the real * `pg.Pool` is constructed by `createPgPool`. Applying the migrations + mapping the repository ports * to SQL over this wrapper is the Testcontainers integration seam (PLAN §10). */ import pg from 'pg' /** Minimal driver surface the wrapper needs — satisfied by `pg.Pool` and `pg.PoolClient`. */ export interface Queryable { query(text: string, params: readonly unknown[]): Promise<{ rows: unknown[] }> } export type QueryFn = (sql: string, params: readonly unknown[]) => Promise /** Build a parameterized-only query function over any `Queryable`. */ export function createQuery(client: Queryable): QueryFn { return async (sql: string, params: readonly unknown[]): Promise => { // Params ALWAYS travel separately from the SQL text — never interpolated into it. const result = await client.query(sql, params) return result.rows as T[] } } /** Construct a real Postgres connection pool (production). */ export function createPgPool(connectionString: string): pg.Pool & Queryable { return new pg.Pool({ connectionString }) }