feat: define module database access
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.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user