import { moduleDrizzle } from "@ekklesia/module-database/drizzle"; import { defineModuleRuntime } from "@ekklesia/module-sdk"; import { records } from "../schema/tables.js"; export const recordsCapability = Object.freeze({ kind: "example-records", }); export default defineModuleRuntime({ database: { abi: 1 }, id: "example", version: "0.1.0", register({ capabilities, http }) { capabilities.provide("example.records", recordsCapability); // The accepted module database boundary: `context.services.database` — provided only to an // authorized handler, never to `register` — is this module's ABI-v1 SQL executor. The adapter // keeps this module's own Drizzle query builder and table types entirely module-side; Core sees // only SQL text and bound values, never Drizzle objects, a raw `pg` client, or a connection // string. `example.records.read` is declared in `ekklesia.module.json`, exactly like every // other permission a module route enforces. http.route({ method: "GET", path: "/records", permission: "example.records.read", // This route accepts no request body at all — a GET route never should. Every route must // make this choice consciously: 0 means "no body accepted" (a non-empty declared or streamed // body is rejected before this handler ever runs); a route that does accept a body declares // its own explicit byte maximum instead, up to the platform's ceiling // (`MODULE_ROUTE_MAX_BODY_BYTES_CEILING`, `@ekklesia/contracts`). maxBodyBytes: 0, async handler(_request, context) { const database = moduleDrizzle(context.services.database); const rows = await database.select({ id: records.id, value: records.value }).from(records); // The accepted module configuration boundary: `context.config` is a frozen, already // validated snapshot of exactly this module's own declared keys (ADR 0029). It reaches // `register`, `start`, `stop`, and every handler as the same object. This module never // reads `process.env`, never queries a platform table for a setting, and has no way to see // another module's configuration — which is what keeps it runnable under a future // standalone host that stores configuration somewhere else entirely. // // `greeting` is declared with a default in `ekklesia.module.json`, so it is always present; // the `??` is only here because accessors are typed `T | undefined` for undeclared keys. const greeting = context.config.getString("greeting") ?? "Hello"; return Response.json(rows.map((row) => ({ ...row, value: `${greeting}, ${row.value}` }))); }, }); }, });