import postgres from 'postgres'; /** * Canonical Postgres client for Deck Hearth. * * Supports: * - Homelab CT 102 (direct LAN — `postgresql://deckhearth@192.168.68.102:5432/deckhearth`) * - Supabase pooler (transaction mode, port 6543) if ever used * * Env: * POSTGRES_URL — runtime (required) * POSTGRES_URL_DIRECT — migrations (`npm run migrate`); on homelab, same as POSTGRES_URL * * API matches the former @vercel/postgres shape: `{ rows, rowCount }`. */ function resolveDatabaseUrl() { const url = process.env.POSTGRES_URL || process.env.DATABASE_URL; if (!url) { throw new Error('POSTGRES_URL is not set'); } return url; } function resolveClientOptions(url) { const isSupabasePooler = url.includes('pgbouncer=true') || /:6543\//.test(url) || url.includes(':6543?'); const isPrivateLan = /(?:^|@)(?:localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test( url ); return { ssl: isPrivateLan ? false : 'require', max: 1, idle_timeout: 20, connect_timeout: 15, prepare: isSupabasePooler ? false : true, }; } /** @type {import('postgres').Sql | null} */ let client = null; function getClient() { if (!client) { const url = resolveDatabaseUrl(); client = postgres(url, resolveClientOptions(url)); } return client; } /** * Tagged-template SQL helper. * @param {TemplateStringsArray} strings * @param {...unknown} values * @returns {Promise<{ rows: Record[], rowCount: number }>} */ export async function sql(strings, ...values) { const pg = getClient(); const result = await pg(strings, ...values); const rows = Array.from(result); return { rows, rowCount: typeof result.count === 'number' ? result.count : rows.length, }; } /** Close the pooled client (tests / long-running scripts). */ export async function closeSqlPool() { if (client) { await client.end({ timeout: 5 }); client = null; } }