82 lines
2.3 KiB
TypeScript
82 lines
2.3 KiB
TypeScript
import fs from "node:fs";
|
|
import pg from "pg";
|
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
import * as schema from "./schema/index.js";
|
|
|
|
export type Database = ReturnType<typeof createDatabase>["db"];
|
|
|
|
let sharedPool: pg.Pool | undefined;
|
|
|
|
function resolveSsl(url: string): pg.PoolConfig["ssl"] | undefined {
|
|
const caPath = process.env.DATABASE_CA_CERT;
|
|
if (caPath && fs.existsSync(caPath)) {
|
|
return { ca: fs.readFileSync(caPath, "utf8"), rejectUnauthorized: true };
|
|
}
|
|
if (/sslmode=(require|verify)/.test(url)) {
|
|
return { rejectUnauthorized: false };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function stripSslmode(url: string): string {
|
|
try {
|
|
const u = new URL(url);
|
|
u.searchParams.delete("sslmode");
|
|
return u.toString();
|
|
} catch {
|
|
return url.replace(/[?&]sslmode=[^&]+/, "").replace(/\?$/, "");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Skapar en databasklient. API och worker delar mönster men äger varsin pool.
|
|
* DATABASE_URL pekar på appens separata databas med egen minimalprivilegie-användare
|
|
* (minsta möjliga privilegier, spec §52).
|
|
*/
|
|
export function createDatabase(connectionString?: string) {
|
|
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST !== undefined;
|
|
const rawUrl = connectionString ?? (isTest ? process.env.TEST_DATABASE_URL : process.env.DATABASE_URL);
|
|
|
|
if (!rawUrl) {
|
|
if (isTest) {
|
|
throw new Error(
|
|
"Sätt TEST_DATABASE_URL till en dedikerad testdatabas. Tester får aldrig använda DATABASE_URL.",
|
|
);
|
|
}
|
|
throw new Error("Missing DATABASE_URL. Set it in your .env or environment before starting the app.");
|
|
}
|
|
|
|
const url = stripSslmode(rawUrl);
|
|
const pool = new pg.Pool({
|
|
connectionString: url,
|
|
max: Number(process.env.DATABASE_POOL_MAX ?? 10),
|
|
idleTimeoutMillis: 30_000,
|
|
connectionTimeoutMillis: 10_000,
|
|
ssl: resolveSsl(rawUrl),
|
|
});
|
|
|
|
const db = drizzle(pool, { schema, casing: "snake_case" });
|
|
return { db, pool };
|
|
}
|
|
|
|
/** Singleton för processer som bara behöver en anslutning. */
|
|
export function getDatabase() {
|
|
if (!sharedPool) {
|
|
const { db, pool } = createDatabase();
|
|
sharedPool = pool;
|
|
sharedDb = db;
|
|
}
|
|
return sharedDb!;
|
|
}
|
|
let sharedDb: Database | undefined;
|
|
|
|
export async function closeDatabase(): Promise<void> {
|
|
if (sharedPool) {
|
|
await sharedPool.end();
|
|
sharedPool = undefined;
|
|
sharedDb = undefined;
|
|
}
|
|
}
|
|
|
|
export { schema };
|