/** * A process-wide database registry. The framework configures it at server * startup from `wrnexus.config.ts`: the `db` setting becomes the **default** * connection, and each entry under `databases` becomes a **named** connection. * Pages and API routes then call `getDb()` for the default, or `getDb("")` * for a named one, to run queries (including the generated typed functions). * * const users = await getDb().all("SELECT * FROM users"); // default db * const events = await getDb("analytics").all("SELECT * FROM hits"); // named db */ import type { Db } from "./driver.ts"; const DEFAULT = "default"; type DbFactory = () => Db; type RegistryEntry = { db?: Db; factory?: DbFactory }; const REGISTRY_KEY = Symbol.for("@wrnexus/db:registry:v1"); type RegistryGlobal = typeof globalThis & { [REGISTRY_KEY]?: Map }; // Production bundlers can include @wrnexus/db more than once when an app and // the server runtime resolve compatible but distinct package installations. // A module-local Map splits configuration from consumers in that case. Store // the registry on globalThis under a stable symbol so every bundled copy in // the process observes the same default and named connections. function databaseRegistry(): Map { const scope = globalThis as RegistryGlobal; return (scope[REGISTRY_KEY] ??= new Map()); } /** Set the default database (called by the runtime at startup). */ export function setDb(db: Db): Db; /** Set a named database (from `databases.` in config). */ export function setDb(name: string, db: Db): Db; export function setDb(a: string | Db, b?: Db): Db { const name = typeof a === "string" ? a : DEFAULT; const db = typeof a === "string" ? b! : a; databaseRegistry().set(name, { db }); return db; } /** Register a named database. Alias of `setDb(name, db)` for readability. */ export function registerDb(name: string, db: Db): Db { return setDb(name, db); } /** * Register a named database without opening its connection pool. The first * `getDb(name)` call creates and caches the connection. */ export function registerLazyDb(name: string, factory: DbFactory): void { databaseRegistry().set(name, { factory }); } /** The default database, or a named one. Throws if it isn't configured. */ export function getDb(name = DEFAULT): Db { const entry = databaseRegistry().get(name); if (!entry) { throw new Error( name === DEFAULT ? "No database configured. Add `db: { driver, url }` to wrnexus.config.ts." : `No database named '${name}'. Add it under \`databases\` in wrnexus.config.ts ` + `(e.g. databases: { ${name}: { driver, url } }).`, ); } if (!entry.db) { if (!entry.factory) throw new Error(`Database '${name}' has no connection factory.`); entry.db = entry.factory(); } return entry.db; } /** Whether the default (or a named) database has been configured. */ export function hasDb(name = DEFAULT): boolean { return databaseRegistry().has(name); } /** Names of all configured databases (the default appears as "default"). */ export function databaseNames(): string[] { return [...databaseRegistry().keys()]; } /** Close every configured database and clear the registry. */ export async function closeDatabases(): Promise { const registry = databaseRegistry(); const databases = [...registry.values()].flatMap((entry) => (entry.db ? [entry.db] : [])); registry.clear(); const results = await Promise.allSettled(databases.map((db) => db.close())); const errors = results.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])); if (errors.length) throw new AggregateError(errors, "One or more databases failed to close"); }