Files
Ekklesia/core/src/identity/database.ts
T
ekklesia 91d8f5946f
CI / verify (pull_request) Successful in 27s
CI / infrastructure-smoke (pull_request) Successful in 19s
CI / migration-smoke (pull_request) Successful in 26s
feat: expose core identity transport
2026-07-21 19:15:14 -05:00

76 lines
2.9 KiB
TypeScript

import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
import { Pool, type PoolConfig } from "pg";
import { identityAdapterSchema } from "./schema.js";
export type IdentityDrizzleDatabase = NodePgDatabase<typeof identityAdapterSchema>;
export type IdentityDatabaseErrorCode = "IDENTITY_DATABASE_CONFIGURATION_INVALID";
/** Never carries the offending connection string, host, or underlying driver error. */
export class IdentityDatabaseError extends Error {
readonly code: IdentityDatabaseErrorCode;
constructor(code: IdentityDatabaseErrorCode) {
super("Identity database configuration is invalid.");
this.name = "IdentityDatabaseError";
this.code = code;
this.stack = `${this.name}: ${this.message}`;
Object.freeze(this);
}
}
export interface IdentityDatabaseOptions {
readonly connectionTimeoutMs?: number;
}
export interface IdentityDatabaseHandle {
readonly close: () => Promise<void>;
readonly db: IdentityDrizzleDatabase;
}
function validateConnectionString(connectionString: string): string {
if (connectionString.trim().length === 0) {
throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
}
let protocol: string;
try {
protocol = new URL(connectionString).protocol;
} catch {
throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
}
if (protocol !== "postgres:" && protocol !== "postgresql:") {
throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
}
return connectionString;
}
/**
* Explicitly constructs the Core-owned Drizzle database instance used by the identity factory.
* Building the `pg.Pool` performs no network I/O until a query is issued, and this function is
* only ever invoked by explicit caller code — never during module import.
*/
export function createIdentityDrizzleDatabase(
connectionString: string,
options: IdentityDatabaseOptions = {},
): IdentityDatabaseHandle {
const validatedConnectionString = validateConnectionString(connectionString);
const poolConfig: PoolConfig = { connectionString: validatedConnectionString };
if (options.connectionTimeoutMs !== undefined) {
poolConfig.connectionTimeoutMillis = options.connectionTimeoutMs;
}
const pool = new Pool(poolConfig);
// pg's own documentation requires an error listener on the pool: an idle client can be
// terminated by the server (for example, its backend is killed, or the network drops) after
// being returned to the pool, and the pool surfaces that as an 'error' event on itself rather
// than on any in-flight query. Without a listener, that event is an unhandled Node.js error and
// can crash the process even though no caller is doing anything wrong at the time.
pool.on("error", () => undefined);
const db = drizzle(pool, { schema: identityAdapterSchema });
return Object.freeze({
close: () => pool.end(),
db,
});
}