88 lines
3.3 KiB
JavaScript
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;
|
|
}
|