76 lines
2.9 KiB
TypeScript
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,
|
|
});
|
|
}
|