CI / verify (pull_request) Successful in 1m1s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m54s
CI / image-build (pull_request) Successful in 31s
CI / deploy-smoke (same-origin) (pull_request) Successful in 52s
CI / deploy-smoke (split) (pull_request) Successful in 1m32s
50 lines
2.7 KiB
TypeScript
50 lines
2.7 KiB
TypeScript
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}` })));
|
|
},
|
|
});
|
|
},
|
|
});
|