import pg from "pg"; import logger from "./logger.js"; const { Pool } = pg; const TRANSIENT_ERRORS = new Set([ "ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "57P01", "57P02", "57P03", "08006", "08003", "08001", ]); const pool = new Pool({ host: process.env.DATABASE_HOST || "main-db-pooler.postgres.svc", port: parseInt(process.env.DATABASE_PORT || "5432", 10), database: process.env.DATABASE_NAME || "snapapi", user: process.env.DATABASE_USER || "docfast", password: process.env.DATABASE_PASSWORD || "docfast", max: 10, idleTimeoutMillis: 10000, connectionTimeoutMillis: 5000, keepAlive: true, keepAliveInitialDelayMillis: 10000, }); pool.on("error", (err) => { logger.error({ err }, "Unexpected error on idle PostgreSQL client"); }); function isTransientError(err: any): boolean { if (!err) return false; const code = err.code || ""; const msg = (err.message || "").toLowerCase(); if (TRANSIENT_ERRORS.has(code)) return true; if (msg.includes("no available server") || msg.includes("connection terminated") || msg.includes("connection refused")) return true; return false; } export async function queryWithRetry(text: string, params?: any[], maxRetries = 3): Promise { let lastError: any; for (let attempt = 0; attempt <= maxRetries; attempt++) { let client: pg.PoolClient | undefined; try { client = await pool.connect(); const result = await client.query(text, params); client.release(); return result; } catch (err: any) { if (client) try { client.release(true); } catch (_) {} lastError = err; if (!isTransientError(err) || attempt === maxRetries) throw err; const delayMs = Math.min(1000 * Math.pow(2, attempt), 5000); logger.warn({ err: err.message, attempt: attempt + 1 }, "Transient DB error, retrying..."); await new Promise(r => setTimeout(r, delayMs)); } } throw lastError; } export async function connectWithRetry(maxRetries = 3): Promise { let lastError: any; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const client = await pool.connect(); await client.query("SELECT 1"); return client; } catch (err: any) { lastError = err; if (!isTransientError(err) || attempt === maxRetries) throw err; await new Promise(r => setTimeout(r, Math.min(1000 * Math.pow(2, attempt), 5000))); } } throw lastError; } export async function initDatabase(): Promise { const client = await connectWithRetry(); try { await client.query(` CREATE TABLE IF NOT EXISTS api_keys ( key TEXT PRIMARY KEY, tier TEXT NOT NULL DEFAULT 'free', email TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), stripe_customer_id TEXT ); CREATE INDEX IF NOT EXISTS idx_api_keys_email ON api_keys(email); CREATE TABLE IF NOT EXISTS usage ( key TEXT NOT NULL, month_key TEXT NOT NULL, count INT NOT NULL DEFAULT 0, PRIMARY KEY (key, month_key) ); `); logger.info("PostgreSQL tables initialized"); } finally { client.release(); } } export { pool }; export default pool;