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.
51 lines
2.8 KiB
TypeScript
51 lines
2.8 KiB
TypeScript
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
|
|
/**
|
|
* 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 (`@ekklesia/module-sdk`'s `defineModuleSchema`) to each call, so there is no global
|
|
* relational schema spanning every installed module for this type to leak.
|
|
*
|
|
* 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 complete set of platform-provided services a trusted module's `start`/`stop` lifecycle hooks
|
|
* and authorized HTTP route handlers receive — for this phase, exactly one: `database`. Frozen, and
|
|
* never handed to a module's `register` hook (`ModuleRegistrationContext` has no `services` field):
|
|
* registration stays declarative and database-free, exactly as before this contract existed.
|
|
* Deliberately has no storage, jobs, configuration, secrets, or authorization field yet — those
|
|
* remain out of scope until a later phase explicitly adds them here, one at a time, the same way
|
|
* `database` was added.
|
|
*/
|
|
export interface ModuleRuntimeServices {
|
|
readonly database: ModuleDatabase;
|
|
}
|