Repository Public
1/** pg Pool + a tiny typed query helper. Every statement is parameterised. */2import pg from 'pg';34// int8 (bigint counters, sizes) → number: all values here are far below 2^53.5pg.types.setTypeParser(20, (v) => Number(v));67export type SqlParam = string | number | boolean | null | Date | Buffer | string[] | object;8export interface Queryable {9 q<T = Record<string, unknown>>(sql: string, params?: readonly SqlParam[]): Promise<T[]>;10 one<T = Record<string, unknown>>(sql: string, params?: readonly SqlParam[]): Promise<T | null>;11}1213function wrap(runner: { query: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }> }): Queryable {14 return {15 async q<T>(sql: string, params: readonly SqlParam[] = []): Promise<T[]> {16 const r = await runner.query(sql, params as unknown[]);17 return r.rows as T[];18 },19 async one<T>(sql: string, params: readonly SqlParam[] = []): Promise<T | null> {20 const r = await runner.query(sql, params as unknown[]);21 return (r.rows[0] as T | undefined) ?? null;22 },23 };24}2526export interface Db extends Queryable {27 pool: pg.Pool;28 withTx<T>(fn: (tx: Queryable) => Promise<T>): Promise<T>;29 end(): Promise<void>;30}3132export function createDb(databaseUrl: string, max = 10): Db {33 const pool = new pg.Pool({ connectionString: databaseUrl, max, idleTimeoutMillis: 30_000 });34 pool.on('error', () => {35 /* idle client errors are surfaced by the next query; never crash the process */36 });37 const base = wrap(pool);38 return {39 pool,40 ...base,41 async withTx<T>(fn: (tx: Queryable) => Promise<T>): Promise<T> {42 const client = await pool.connect();43 try {44 await client.query('BEGIN');45 const out = await fn(wrap(client));46 await client.query('COMMIT');47 return out;48 } catch (err) {49 await client.query('ROLLBACK').catch(() => undefined);50 throw err;51 } finally {52 client.release();53 }54 },55 end: () => pool.end(),56 };57}58