86 lines
3.1 KiB
JavaScript
86 lines
3.1 KiB
JavaScript
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;
|
|
}
|