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,95 @@
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ModuleDatabase, ModuleRuntimeServices } from "../src/index.js";
|
||||
|
||||
const testTable = pgTable("contracts_module_database_test", {
|
||||
id: integer("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Never invoked — exists solely so `pnpm typecheck` (part of `pnpm verify`) proves `ModuleDatabase`
|
||||
* preserves Drizzle's own table-aware argument and result inference for every required operation,
|
||||
* with no `any`/`unknown` erasure anywhere in the chain. A regression that widened any of these
|
||||
* signatures to `any` would still compile a *call* to the method but would break one of the
|
||||
* explicit row-shape assertions below.
|
||||
*/
|
||||
function typeOnlyOperationInference(database: ModuleDatabase): void {
|
||||
const selected = database.select({ id: testTable.id, name: testTable.name }).from(testTable);
|
||||
const selectedRow: { id: number; name: string } = {} as Awaited<typeof selected>[number];
|
||||
void selectedRow;
|
||||
|
||||
const distinct = database.selectDistinct().from(testTable);
|
||||
const distinctRow: { id: number; name: string } = {} as Awaited<typeof distinct>[number];
|
||||
void distinctRow;
|
||||
|
||||
const distinctOn = database.selectDistinctOn([testTable.name]).from(testTable);
|
||||
const distinctOnRow: { id: number; name: string } = {} as Awaited<typeof distinctOn>[number];
|
||||
void distinctOnRow;
|
||||
|
||||
const inserted = database.insert(testTable).values({ id: 1, name: "a" });
|
||||
void inserted;
|
||||
|
||||
const updated = database.update(testTable).set({ name: "b" });
|
||||
void updated;
|
||||
|
||||
const deleted = database.delete(testTable);
|
||||
void deleted;
|
||||
|
||||
const executed = database.execute("select 1");
|
||||
void executed;
|
||||
|
||||
void database.transaction(async (tx) => {
|
||||
const rows: Array<{ id: number }> = await tx.select({ id: testTable.id }).from(testTable);
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
void typeOnlyOperationInference;
|
||||
|
||||
/**
|
||||
* Never invoked — proves at typecheck time that `ModuleDatabase` structurally excludes every raw
|
||||
* PostgreSQL/Drizzle escape hatch a module must never reach through this facade. If any of these
|
||||
* properties were ever (re)added to the picked surface, the corresponding `@ts-expect-error` would
|
||||
* itself become a type error ("unused '@ts-expect-error' directive"), failing `pnpm typecheck`.
|
||||
*/
|
||||
function typeOnlyExclusions(database: ModuleDatabase): void {
|
||||
// @ts-expect-error `$client` (the raw pg client/pool) is not part of the public facade.
|
||||
void database.$client;
|
||||
// @ts-expect-error `_` (internal session/dialect/schema state) is not part of the public facade.
|
||||
void database._;
|
||||
// @ts-expect-error `query` (a schema-bound relational query builder) is not part of the public facade.
|
||||
void database.query;
|
||||
// @ts-expect-error a raw connection pool is not part of the public facade.
|
||||
void database.pool;
|
||||
// @ts-expect-error a raw connection string is not part of the public facade.
|
||||
void database.connectionString;
|
||||
// @ts-expect-error migration operations are not part of the public facade.
|
||||
void database.migrate;
|
||||
}
|
||||
void typeOnlyExclusions;
|
||||
|
||||
/** Never invoked — proves `ModuleRuntimeServices` has exactly one field, `database`, typed as
|
||||
* `ModuleDatabase`, with no storage/jobs/configuration/secrets/authorization field this phase. */
|
||||
function typeOnlyServicesShape(services: ModuleRuntimeServices): void {
|
||||
const database: ModuleDatabase = services.database;
|
||||
void database;
|
||||
// @ts-expect-error `storage` does not exist on `ModuleRuntimeServices` yet.
|
||||
void services.storage;
|
||||
// @ts-expect-error `jobs` does not exist on `ModuleRuntimeServices` yet.
|
||||
void services.jobs;
|
||||
// @ts-expect-error `authorization` does not exist on `ModuleRuntimeServices` yet.
|
||||
void services.authorization;
|
||||
}
|
||||
void typeOnlyServicesShape;
|
||||
|
||||
describe("ModuleDatabase", () => {
|
||||
it("is a real, importable type contract enforced by pnpm typecheck", () => {
|
||||
// The meaningful assertions for this module-level type contract are compile-time
|
||||
// (`typeOnlyOperationInference`, `typeOnlyExclusions`, `typeOnlyServicesShape` above, checked
|
||||
// by `pnpm typecheck`); this keeps the file a genuine, executed Vitest suite rather than a
|
||||
// file Vitest would otherwise skip for declaring no runnable test.
|
||||
expect(typeof typeOnlyOperationInference).toBe("function");
|
||||
expect(typeof typeOnlyExclusions).toBe("function");
|
||||
expect(typeof typeOnlyServicesShape).toBe("function");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user