import { readFileSync } from "node:fs"; import pg from "pg"; const { Pool } = pg; export function getScriptDataBackend(env = process.env) { const backend = env.ZHINIAN_DATA_BACKEND?.trim().toLowerCase(); if (backend === "local" || backend === "postgres") return backend; throw new Error("ZHINIAN_DATA_BACKEND must be explicitly set to 'local' or 'postgres'"); } export function createPostgresPool({ env = process.env, applicationName = "zhinian-script" } = {}) { const connectionString = env.DATABASE_URL?.trim(); if (!connectionString) throw new Error("DATABASE_URL is required when ZHINIAN_DATA_BACKEND=postgres"); const parsed = assertConnectionStringContract(connectionString); const config = { connectionString, max: positiveInteger(env, "DATABASE_POOL_MAX", 10), idleTimeoutMillis: nonNegativeInteger(env, "DATABASE_IDLE_TIMEOUT_MS", 30_000), connectionTimeoutMillis: positiveInteger(env, "DATABASE_CONNECTION_TIMEOUT_MS", 10_000), statement_timeout: positiveInteger(env, "DATABASE_STATEMENT_TIMEOUT_MS", 30_000), application_name: applicationName }; const sslMode = parsed.searchParams.get("sslmode")?.trim().toLowerCase() || "verify-full"; if (sslMode === "verify-full") { const caPath = parsed.searchParams.get("sslrootcert")?.trim(); config.ssl = { ...(caPath ? { ca: readFileSync(caPath, "utf8") } : {}), rejectUnauthorized: true }; } else if (sslMode !== "disable") { throw new Error("DATABASE_URL sslmode must be 'disable' or 'verify-full'"); } parsed.searchParams.delete("sslmode"); parsed.searchParams.delete("sslrootcert"); config.connectionString = parsed.toString(); return new Pool(config); } export async function closePostgresPool(pool) { if (pool) await pool.end(); } export function quotePostgresIdentifier(value) { if (typeof value !== "string" || !/^[a-z_][a-z0-9_]{0,62}$/.test(value)) { throw new Error("PostgreSQL identifier must match [a-z_][a-z0-9_]{0,62}"); } return `"${value}"`; } function assertConnectionStringContract(connectionString) { let parsed; try { parsed = new URL(connectionString); } catch { throw new Error("DATABASE_URL must be a valid PostgreSQL connection URI"); } if (parsed.protocol !== "postgres:" && parsed.protocol !== "postgresql:") { throw new Error("DATABASE_URL must use the postgres:// or postgresql:// scheme"); } const sslParameters = [...parsed.searchParams.keys()].filter((key) => key.toLowerCase().startsWith("ssl")); const unsupportedSSLParameters = sslParameters.filter((key) => !["sslmode", "sslrootcert"].includes(key.toLowerCase())); if (unsupportedSSLParameters.length > 0) { throw new Error( `DATABASE_URL contains unsupported SSL query parameters (${unsupportedSSLParameters.join(", ")}); use sslmode and optional sslrootcert` ); } const sslMode = parsed.searchParams.get("sslmode")?.trim().toLowerCase(); if (sslMode && sslMode !== "disable" && sslMode !== "verify-full") { throw new Error("DATABASE_URL sslmode must be 'disable' or 'verify-full'"); } return parsed; } function positiveInteger(env, name, fallback) { const value = integer(env, name, fallback); if (value <= 0) throw new Error(`${name} must be a positive integer`); return value; } function nonNegativeInteger(env, name, fallback) { const value = integer(env, name, fallback); if (value < 0) throw new Error(`${name} must be a non-negative integer`); return value; } function integer(env, name, fallback) { const raw = env[name]?.trim(); if (!raw) return fallback; const value = Number(raw); if (!Number.isSafeInteger(value)) throw new Error(`${name} must be an integer`); return value; }