deckhearth/lib/sql.js

75 lines
1.9 KiB
JavaScript
Raw Normal View History

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<string, unknown>[], 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;
}
}