import type { ModuleDatabaseAbiRuntimeServices, ModuleRuntimeServices } from "@ekklesia/contracts"; import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; import type { Pool } from "pg"; /** * A trusted backend module's typed Drizzle query surface — the same query-building methods a * platform-owned Drizzle instance exposes, restricted to exactly the operations a module needs to * read and write its own tables. Derived with `Pick` from the real `drizzle-orm/node-postgres` * database type rather than hand-declared, so every method keeps Drizzle's own table-aware * argument and result inference exactly as-is — a module calling `database.select().from(table)` * or `database.insert(table).values(...)` gets the identical typed builder a direct Drizzle caller * would, with no `any`/`unknown` erasure anywhere in the chain. * * Deliberately excludes everything `NodePgDatabase` or the `drizzle(...)` factory otherwise * exposes: `$client` (the raw `pg` client/pool), `_` (internal session/dialect/schema state), * `query` (a relational-query builder tied to one schema, which would have to span every * installed module's tables to exist here at all), `$cache`, `$count`, `$with`, `with`, and * `refreshMaterializedView`. There is no connection string, pool, session, or dialect object * reachable through this type. Carries no schema generic — a module brings its own Drizzle table * objects to each call, so there is no global relational schema spanning every installed module for * this type to leak. * * Core's own, private type (ADR 0053): it moved here from `@ekklesia/contracts` so no published * package references Drizzle. New modules use the Module Database ABI v1 instead (ADR 0050), and * the Module SDK never exposes this facade's type. * * This is an integration boundary, not a security sandbox: modules are trusted, compiled backend * plugins that already run arbitrary code in this process, exactly like every other lifecycle hook * and HTTP handler they define. `ModuleDatabase` exists to keep a module's code Drizzle-typed and * decoupled from how the platform physically connects to PostgreSQL — not to defend against a * malicious module, which this facade cannot do and does not attempt to do. */ export type ModuleDatabase = Pick< NodePgDatabase, | "select" | "selectDistinct" | "selectDistinctOn" | "insert" | "update" | "delete" | "execute" | "transaction" >; /** The services a legacy module's hooks and handlers receive: the legacy facade. */ export type LegacyModuleRuntimeServices = ModuleRuntimeServices; /** The services Core hands any started module: the legacy facade or the ABI v1 executor. */ export type AnyModuleRuntimeServices = | LegacyModuleRuntimeServices | ModuleDatabaseAbiRuntimeServices; /** * 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), }); }