feat: transition module packages to database ABI v1
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

This commit is contained in:
ekklesia
2026-10-07 18:00:04 -05:00
parent 20ad1ae208
commit 70854d234c
20 changed files with 274 additions and 115 deletions
+35 -14
View File
@@ -6,7 +6,7 @@ import {
EMPTY_MODULE_CONFIGURATION,
EMPTY_MODULE_STORAGE,
type ModuleConfiguration,
type ModuleDatabase,
type ModuleDatabaseExecutor,
type ModuleRouteDefinitionInput,
type ModuleRouteHandlerContext,
type ModuleRuntimeServices,
@@ -20,19 +20,34 @@ import { describe, expect, it } from "vitest";
import moduleSchema, { records } from "../src/schema/index.js";
import moduleRuntime, { recordsCapability } from "../src/server/index.js";
/** A small typed fake `ModuleDatabase` — no PostgreSQL required for this unit test, which only
* proves the example handler queries through the facade and shapes its response correctly. */
function fakeModuleDatabase(rows: readonly { id: string; value: string }[]): ModuleDatabase {
const selectBuilder = { from: () => Promise.resolve(rows) };
return { select: () => selectBuilder } as unknown as ModuleDatabase;
/** A small ABI-v1 executor — no PostgreSQL required for this unit test, which proves the template's
* module-side Drizzle adapter compiles its query to SQL and shapes the positional result. */
function fakeModuleDatabase(
rows: readonly { id: string; value: string }[],
): ModuleDatabaseExecutor {
return Object.freeze({
abiVersion: 1,
query: async () => ({
command: "SELECT",
fields: [
{ name: "id", typeOid: 2950 },
{ name: "value", typeOid: 25 },
],
rowCount: rows.length,
rows: rows.map((row) => [row.id, row.value]),
}),
transaction: async () => {
throw new Error("The example read does not start a transaction.");
},
}) as unknown as ModuleDatabaseExecutor;
}
async function registerAndCaptureRoutes(): Promise<{
provided: Map<CapabilityId, unknown>;
routes: ModuleRouteDefinitionInput[];
routes: ModuleRouteDefinitionInput<ModuleDatabaseExecutor>[];
}> {
const provided = new Map<CapabilityId, unknown>();
const routes: ModuleRouteDefinitionInput[] = [];
const routes: ModuleRouteDefinitionInput<ModuleDatabaseExecutor>[] = [];
await moduleRuntime.register?.({
// The template declares no configuration, so it receives an empty snapshot — proof that a
// backend module needing no settings stays exactly as simple as it was (ADR 0029).
@@ -65,6 +80,7 @@ describe("module template", () => {
const manifest = parseModuleManifest(manifestInput);
expect(manifest.id).toBe("example");
expect(manifest.database).toEqual({ abi: 1 });
expect(manifest.capabilities).toEqual([{ id: "example.records", version: "1.0.0" }]);
expect(manifest.permissions).toEqual([
{
@@ -122,7 +138,11 @@ describe("module template", () => {
it("exports the typed runtime definition used by the compiled server entrypoint, registering its one example route database-free", async () => {
const { provided, routes } = await registerAndCaptureRoutes();
expect(moduleRuntime).toMatchObject({ id: "example", version: "0.1.0" });
expect(moduleRuntime).toMatchObject({
database: { abi: 1 },
id: "example",
version: "0.1.0",
});
expect(provided.get("example.records")).toBe(recordsCapability);
expect(routes).toHaveLength(1);
expect(routes[0]).toMatchObject({
@@ -143,10 +163,10 @@ describe("module template", () => {
}
const fakeRows = [{ id: "9d3f9f7e-3b1a-4c9a-8b8e-2b6b1e6f9a11", value: "hello" }];
const services: ModuleRuntimeServices = Object.freeze({
const services: ModuleRuntimeServices<ModuleDatabaseExecutor> = Object.freeze({
database: fakeModuleDatabase(fakeRows),
});
const context: ModuleRouteHandlerContext = Object.freeze({
const context: ModuleRouteHandlerContext<ModuleDatabaseExecutor> = Object.freeze({
config: EMPTY_MODULE_CONFIGURATION,
moduleId: "example",
params: Object.freeze({}),
@@ -191,7 +211,7 @@ describe("module template", () => {
has: (key: string) => key === "greeting",
keys: () => Object.freeze(["greeting"]),
});
const context: ModuleRouteHandlerContext = Object.freeze({
const context: ModuleRouteHandlerContext<ModuleDatabaseExecutor> = Object.freeze({
config,
moduleId: "example",
params: Object.freeze({}),
@@ -228,9 +248,10 @@ describe("module template", () => {
expect(manifest.configuration.some((entry) => entry.type === "secret")).toBe(false);
});
it("exports a namespaced module-owned Drizzle schema", () => {
it("exports plain schema metadata derived from its namespaced module-owned Drizzle schema", () => {
expect(moduleSchema.moduleId).toBe("example");
expect(moduleSchema.tables.records).toBe(records);
expect(moduleSchema.tables).toEqual([{ name: "example_records", foreignKeys: [] }]);
expect(JSON.parse(JSON.stringify(moduleSchema))).toEqual(moduleSchema);
expect(getTableConfig(records)).toMatchObject({ name: "example_records", schema: undefined });
});
});