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"); 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 = env.DATABASE_SSL_MODE?.trim().toLowerCase() || "disable"; if (sslMode === "verify-full") { const caPath = env.DATABASE_CA_CERT_PATH?.trim(); if (!caPath) throw new Error("DATABASE_CA_CERT_PATH is required when DATABASE_SSL_MODE=verify-full"); config.ssl = { ca: readFileSync(caPath, "utf8"), rejectUnauthorized: true }; } else if (sslMode !== "disable") { throw new Error("DATABASE_SSL_MODE must be 'disable' or 'verify-full'"); } 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")); if (sslParameters.length > 0) { throw new Error( `DATABASE_URL must not contain SSL query parameters (${sslParameters.join(", ")}); use DATABASE_SSL_MODE and DATABASE_CA_CERT_PATH` ); } } 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; }