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:
@@ -1,4 +1,9 @@
|
||||
import { ANONYMOUS_PRINCIPAL, createAuthenticatedPrincipal } from "@ekklesia/contracts";
|
||||
import {
|
||||
ANONYMOUS_PRINCIPAL,
|
||||
createAuthenticatedPrincipal,
|
||||
type ModuleDatabase,
|
||||
type ModuleRuntimeServices,
|
||||
} from "@ekklesia/contracts";
|
||||
import {
|
||||
type AuthorizationEvaluator,
|
||||
type AuthorizationStore,
|
||||
@@ -21,6 +26,22 @@ function fakeRuntime(overrides: Partial<EkklesiaIdentityRuntime> = {}): Ekklesia
|
||||
};
|
||||
}
|
||||
|
||||
/** A small typed fake `ModuleRuntimeServices` — these tests exercise `createApiApp`'s own wiring,
|
||||
* never real database behavior, so no PostgreSQL is required. */
|
||||
function fakeModuleServices(): ModuleRuntimeServices {
|
||||
const database = Object.freeze({
|
||||
delete: vi.fn(),
|
||||
execute: vi.fn(),
|
||||
insert: vi.fn(),
|
||||
select: vi.fn(),
|
||||
selectDistinct: vi.fn(),
|
||||
selectDistinctOn: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
update: vi.fn(),
|
||||
}) as unknown as ModuleDatabase;
|
||||
return Object.freeze({ database });
|
||||
}
|
||||
|
||||
/** A fully stubbed authorization dependency, denying everything by default — tests that need a
|
||||
* specific decision or store behavior override the relevant method explicitly. The registry
|
||||
* defaults to Core's own (containing `core.authorization.grants.manage`, exactly what the
|
||||
@@ -53,6 +74,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
const runtime = fakeRuntime();
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -69,6 +91,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
const runtime = fakeRuntime();
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -86,6 +109,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
});
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -105,6 +129,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
});
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -126,6 +151,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
});
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -147,6 +173,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
});
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -165,6 +192,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
});
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime,
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -182,6 +210,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("has deterministic 404 behavior for an unknown route", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -196,6 +225,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("reflects an allowed origin in Access-Control-Allow-Origin with credentials support", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -212,6 +242,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("never returns Access-Control-Allow-Origin for a disallowed origin", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -227,6 +258,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("supports requests with no Origin header at all (native/server clients)", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -240,6 +272,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("handles preflight OPTIONS requests deterministically for an allowed origin", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -260,6 +293,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("never wildcards Access-Control-Allow-Origin", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -283,6 +317,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
const evaluatorAuthorize = vi.fn().mockResolvedValue({ allowed: false });
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({ evaluator: { authorize: evaluatorAuthorize } }),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
@@ -309,6 +344,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
};
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({
|
||||
evaluator: { authorize: evaluatorAuthorize },
|
||||
registry: emptyRegistry,
|
||||
@@ -326,6 +362,7 @@ describe("Ekklesia API Hono application", () => {
|
||||
it("applies the same CORS policy to /api/v1/grants as the rest of /api/v1/*", async () => {
|
||||
const app = createApiApp({
|
||||
moduleRoutes: Object.freeze([]),
|
||||
moduleServices: fakeModuleServices(),
|
||||
authorization: fakeAuthorization({}),
|
||||
runtime: fakeRuntime(),
|
||||
trustedOrigins: [trustedOrigin],
|
||||
|
||||
Reference in New Issue
Block a user