Introduce ModuleDatabase (a typed, restricted Drizzle query surface) and the frozen ModuleRuntimeServices boundary in @ekklesia/contracts, and thread the same services object through Core's module start/stop lifecycle hooks and apps/api's authorized module HTTP handler context. apps/api builds the facade via a new core/src/modules/database.ts view (createModuleDatabaseView) over the one existing shared PostgreSQL pool, alongside the identity and authorization views, and wires it into startApi's fixed startup order ahead of Identity/Authorization construction. Registration stays database-free; a module reaches its database only through an authorized start, stop, or HTTP handler invocation.
44 lines
2.6 KiB
TypeScript
44 lines
2.6 KiB
TypeScript
import type { ModuleDatabase } from "@ekklesia/contracts";
|
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
import type { Pool } from "pg";
|
|
|
|
/**
|
|
* A typed `ModuleDatabase` view over an externally owned pool — the module-facing counterpart to
|
|
* `createIdentityDrizzleView` (`core/src/identity/database.ts`) and
|
|
* `createAuthorizationDrizzleView` (`core/src/authorization/database.ts`): the same "one pool, one
|
|
* typed view per domain" composition, extended to a third domain. Never constructs, registers an
|
|
* error listener on, or closes a pool itself — the caller (`apps/api/src/database.ts`) retains that
|
|
* responsibility exactly once, regardless of how many typed views it creates over the same pool.
|
|
*
|
|
* Carries no schema, unlike the identity and authorization views: a module brings its own Drizzle
|
|
* table objects to each call rather than selecting against a schema this facade would otherwise
|
|
* have to know about, so there is no `TFullSchema` for `drizzle(...)` to accept here. Building this
|
|
* view performs no query and no migration — `drizzle(pool)` only wraps the given client, the same
|
|
* synchronous, I/O-free construction `createIdentityDrizzleView`/`createAuthorizationDrizzleView`
|
|
* already rely on.
|
|
*
|
|
* Deliberately returns a fresh object with each operation explicitly bound to the underlying
|
|
* Drizzle instance, rather than the `drizzle(pool)` instance itself: Drizzle's own query methods
|
|
* are plain prototype methods that read `this.session`/`this.dialect` internally (not
|
|
* arrow-function instance properties), so handing that instance straight to a module would both
|
|
* break the moment a module destructured a method off it (`const { select } = database` loses
|
|
* `this`) and, more importantly, would leave every non-public property (`$client`, `_`, `query`,
|
|
* `$cache`, `with`) physically reachable on the object at runtime — reachable to a module written
|
|
* in plain JavaScript or one that bypasses the `ModuleDatabase` type with a cast, not just hidden
|
|
* by TypeScript. Building this literal, `Object.freeze`d object closes both gaps: only the eight
|
|
* intended operations exist on it at all, each correctly bound, and nothing else does.
|
|
*/
|
|
export function createModuleDatabaseView(pool: Pool): ModuleDatabase {
|
|
const db = drizzle(pool);
|
|
return Object.freeze({
|
|
delete: db.delete.bind(db),
|
|
execute: db.execute.bind(db),
|
|
insert: db.insert.bind(db),
|
|
select: db.select.bind(db),
|
|
selectDistinct: db.selectDistinct.bind(db),
|
|
selectDistinctOn: db.selectDistinctOn.bind(db),
|
|
transaction: db.transaction.bind(db),
|
|
update: db.update.bind(db),
|
|
});
|
|
}
|