feat: deliver module database ABI by manifest
CI / verify (pull_request) Successful in 1m0s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s

This commit is contained in:
ekklesia
2026-10-07 17:29:32 -05:00
parent a1cdcb3ad1
commit 49b4d4165d
60 changed files with 948 additions and 237 deletions
+45 -13
View File
@@ -198,7 +198,7 @@ without executing module code:
```json
{
"manifestVersion": 7,
"manifestVersion": 8,
"id": "calendar",
"name": "Calendar",
"version": "0.1.0",
@@ -275,10 +275,10 @@ has its own exact semantic version because its public contract can evolve indepe
module release. Capability dependencies use semantic-version ranges. Optional means the provider
may be absent; if it is installed, its capability version must be compatible.
`manifestVersion: 7` (`packages/contracts/src/module-manifest.ts`) is a required, exact literal — a
version 6 manifest, and any other version, fails validation outright rather than being silently
`manifestVersion: 8` (`packages/contracts/src/module-manifest.ts`) is a required, exact literal — a
version 7 manifest, and any other version, fails validation outright rather than being silently
reinterpreted; there is no compatibility adapter or automatic migration between manifest versions.
Version 7 adds the optional `contributions` array
Already-built version 7 packages must be rebuilt. Version 7 added the optional `contributions` array
([ADR 0041](../architecture/decisions/0041-module-browser-contributions.md)). A version 6 module that
contributes nothing changes only the version literal; every browser artifact remains independently
optional ([ADR 0033](../architecture/decisions/0033-display-kiosk-host-contract.md)).
@@ -1491,13 +1491,14 @@ destructive behavior. Review those properties explicitly. Core is the only migra
follows Drizzle's exact `--> statement-breakpoint` delimiter and never naively splits semicolons or
falls back to non-transactional execution.
## Module Database ABI v1 adapters (not yet delivered at runtime)
## Module Database ABI v1
The stable database boundary is Module Database ABI v1: SQL text, ordered bound values, an explicit
row mode, low-level PostgreSQL result values, and callback-scoped transactions. ORM objects never
cross it. `@ekklesia/module-database` is module-side code for consuming that boundary once a later
manifest/runtime slice delivers a `ModuleDatabaseExecutor`; it is **not usable by deployed modules
yet**.
cross it. A manifest version 8 module opts in with `"database": { "abi": 1 }`; omitting `database`
selects the temporary legacy facade. “Legacy” here describes that facade, never a version 7
manifest. Core selects the service from the validated manifest and gives each ABI module its own
executor, reused by that module's lifecycle hooks and route handlers.
A Drizzle module imports the optional adapter subpath and declares its own exact compatible
`drizzle-orm` dependency. The adapter's peer range (`>=0.45.2 <0.46.0`) describes adapter API
@@ -1505,13 +1506,25 @@ compatibility; it is not a Core or platform compatibility range:
```ts
import { moduleDrizzle } from "@ekklesia/module-database/drizzle";
import { defineModuleRuntime } from "@ekklesia/module-sdk";
import * as schema from "./schema/tables.js";
// `executor` will be supplied by later ABI service delivery work.
const db = moduleDrizzle(executor, { schema });
const events = await db.select().from(schema.events);
export default defineModuleRuntime({
database: { abi: 1 },
id: "calendar",
version: "0.1.0",
async start({ services }) {
const db = moduleDrizzle(services.database, { schema });
const events = await db.select().from(schema.events);
},
});
```
The repeated runtime discriminator is what gives lifecycle and route-handler code the exact
`ModuleDatabaseExecutor` type instead of a legacy-or-ABI union. Core still chooses from the static
manifest and refuses startup when the runtime discriminator disagrees with it; neither contract can
silently receive the other service.
Drizzle compiles SQL and parameters before calling the ABI. The adapter always requests positional
(`rowMode: "array"`) results, gives those rows back to Drizzle in column order, and Drizzle applies
the module's own column decoders for bigint, numeric, dates/timestamps, JSON, byte arrays, nulls, and
@@ -1560,9 +1573,28 @@ const result = await executor.query({ ...statement, rowMode: "object" });
The tag does not stringify values. Unsupported values pass through unchanged and the ABI executor
rejects them; strings containing quotes or SQL syntax always remain parameters.
## Database access
ABI modules also export plain schema metadata rather than Drizzle objects. Drizzle users can derive
it on the module side; raw-SQL modules can write the same inert shape directly:
`context.services.database` — available in `start`, `stop`, and every authorized route handler,
```ts
import { moduleDrizzleSchemaMetadata } from "@ekklesia/module-database/drizzle";
import * as tables from "./tables.js";
export default moduleDrizzleSchemaMetadata("calendar", tables);
```
The result is `{ moduleId, tables: [{ name, foreignKeys: [{ table }] }] }`. Core validates table
prefixes, duplicates, and same-module foreign-key targets from those strings and never inspects an
ABI module's ORM objects.
The canonical module template has moved to manifest version 8 but intentionally omits `database`,
so it remains on the legacy facade for one more slice. The packager still emits ADR 0049's
`transitional.databaseRuntime`; the next package-transition change will remove that fact for ABI
packages and migrate the template at the same time.
## Legacy database access
For a version 8 manifest that omits `database`, `context.services.database` — available in `start`, `stop`, and every authorized route handler,
never in `register` — is a `ModuleDatabase`: a typed Drizzle query surface over exactly `select`,
`selectDistinct`, `selectDistinctOn`, `insert`, `update`, `delete`, `execute`, and `transaction`,
preserving Drizzle's own table-aware argument and result inference. It is the platform's only