Files
NianAIGC/scripts/postgres-client.mjs

88 lines
3.3 KiB
JavaScript

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);
for (const key of [...parsed.searchParams.keys()]) {
if (["sslmode", "sslrootcert"].includes(key.toLowerCase())) parsed.searchParams.delete(key);
}
parsed.searchParams.set("sslmode", "disable");
const config = {
connectionString: parsed.toString(),
ssl: false,
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
};
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(", ")}); PostgreSQL transport is forced to sslmode=disable`
);
}
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;
}