feat: add module database ABI v1 adapters
CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
This commit is contained in:
@@ -136,11 +136,14 @@ These rules map platform ownership to filesystem and package boundaries. The exe
|
||||
declared by the consumer.
|
||||
- Code a workspace package ships (its `src/` and `bin/`, excluding tests and declaration files)
|
||||
declares every runtime import in `dependencies` or `optionalDependencies`. A peer declaration may
|
||||
stay beside it, but never replaces it: a workspace package is linked into its consumers as a
|
||||
directory, so its imports resolve from its own `node_modules`, where a consumer never satisfies a
|
||||
peer, and a production install (`pnpm install --prod`, as the production image runs) omits
|
||||
development dependencies. Type-only imports are exempt; a `.tsx`/`.jsx` file counts as importing
|
||||
`react`, because the automatic JSX runtime imports `react/jsx-runtime` implicitly
|
||||
stay beside it, but ordinarily never replaces it: a workspace package is linked into its consumers
|
||||
as a directory, so its imports resolve from its own `node_modules`, where a consumer never
|
||||
satisfies a peer, and a production install (`pnpm install --prod`, as the production image runs)
|
||||
omits development dependencies. The one narrow exception is
|
||||
`@ekklesia/module-database`'s `drizzle-orm` peer: modules choose that compatible runtime and the
|
||||
Module Package v1 bundler incorporates it into their self-contained backend; Core never loads the
|
||||
adapter package. Type-only imports are exempt; a `.tsx`/`.jsx` file counts as importing `react`,
|
||||
because the automatic JSX runtime imports `react/jsx-runtime` implicitly
|
||||
(`runtime-imports-are-production-dependencies`).
|
||||
- Workspace aliases are forbidden because they obscure package ownership in the dependency graph.
|
||||
- Workspace package names are unique and shared packages declare a recognized architectural role.
|
||||
|
||||
@@ -145,6 +145,12 @@ copies interoperate only when the copies are interchangeable, so the current imp
|
||||
package records it under `transitional`, and an installation refuses a package whose Drizzle differs.
|
||||
This is a constraint of today's implementation, not a permanent contract.
|
||||
|
||||
`@ekklesia/module-database` now contains the module-side ABI v1 adapters that remove that coupling,
|
||||
but deployed modules cannot receive an ABI v1 executor yet. Manifest v8 and per-module runtime
|
||||
service delivery are separate later work. Until those land, the current transitional package rule
|
||||
and database service above remain authoritative; neither Module Package v1 nor the template changed
|
||||
with the adapter package.
|
||||
|
||||
#### Verifying a package
|
||||
|
||||
`node core/dist/cli/verify-module-package.js <archive> --sha256-file <archive>.sha256`, run from an
|
||||
@@ -1485,6 +1491,75 @@ 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)
|
||||
|
||||
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**.
|
||||
|
||||
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
|
||||
compatibility; it is not a Core or platform compatibility range:
|
||||
|
||||
```ts
|
||||
import { moduleDrizzle } from "@ekklesia/module-database/drizzle";
|
||||
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);
|
||||
```
|
||||
|
||||
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
|
||||
supported arrays. This also makes joins with duplicate PostgreSQL column names unambiguous. Core
|
||||
never imports this adapter and has no dependency on the module's Drizzle version. The adapter surface
|
||||
is deliberately limited to `select`, `insert`, `update`, `delete`, and `transaction`; returning,
|
||||
joins, and SQL expressions come from those normal Drizzle builders.
|
||||
|
||||
`db.transaction(callback)` maps to the ABI transaction callback. The callback receives another
|
||||
Drizzle-facing database backed by the scoped ABI executor; a nested `.transaction()` maps to the
|
||||
ABI's `savepoint` callback. Callback values and errors propagate, Core remains authoritative for
|
||||
commit/rollback and escaped-scope behavior, and the adapter adds no retries or transaction-control
|
||||
SQL.
|
||||
|
||||
Drizzle wraps execution failures in `DrizzleQueryError`. Recover the stable sanitized ABI error by
|
||||
shape instead of using a PostgreSQL error instance, parsing a message, or reading `.cause.code`:
|
||||
|
||||
```ts
|
||||
import { findEkklesiaDatabaseError } from "@ekklesia/module-database";
|
||||
|
||||
try {
|
||||
await db.insert(schema.events).values(event);
|
||||
} catch (error) {
|
||||
const databaseError = findEkklesiaDatabaseError(error);
|
||||
if (databaseError?.sqlState === "23505") {
|
||||
// Handle a uniqueness conflict.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Modules that do not want Drizzle use the package's ORM-neutral tag. Every interpolation is one bound
|
||||
value; v1 deliberately provides no dynamic-identifier or trusted-fragment escape hatch:
|
||||
|
||||
```ts
|
||||
import { sql } from "@ekklesia/module-database";
|
||||
|
||||
const statement = sql`
|
||||
SELECT id, title
|
||||
FROM calendar_events
|
||||
WHERE id = ${eventId}
|
||||
`;
|
||||
|
||||
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
|
||||
|
||||
`context.services.database` — available in `start`, `stop`, and every authorized route handler,
|
||||
|
||||
Reference in New Issue
Block a user