Files
NianAIGC/scripts/postgres-client.mjs

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;
}