Replace @vercel/postgres, Blob, and Upstash with lib/sql.js, MinIO object storage, and CT 102 Redis rate limits. Add Dockerfile for Dokploy deploy, homelab runbooks, Neon data-copy helper, and point CI smoke/visual at the homelab URL instead of Vercel previews. Co-authored-by: Cursor <cursoragent@cursor.com>
74 lines
1.9 KiB
JavaScript
74 lines
1.9 KiB
JavaScript
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;
|
|
}
|
|
}
|