feat: add module migration contracts and validation
This commit is contained in:
@@ -10,15 +10,24 @@ strict semantic version, and the module layer marker:
|
||||
|
||||
```text
|
||||
modules/example/
|
||||
├── drizzle/ # when the module has migration history
|
||||
│ ├── 0000_initial.sql
|
||||
│ └── meta/
|
||||
│ ├── 0000_snapshot.json
|
||||
│ ├── _ekklesia.json
|
||||
│ └── _journal.json
|
||||
├── dist/schema/index.js # when the module owns tables
|
||||
├── dist/server/index.js
|
||||
├── drizzle.config.ts # when generating migrations
|
||||
├── ekklesia.module.json
|
||||
├── package.json
|
||||
├── src/schema/index.ts # when the module owns tables
|
||||
├── src/schema/tables.ts # pure Drizzle declarations used by Drizzle Kit
|
||||
└── src/server/index.ts
|
||||
```
|
||||
|
||||
The schema paths are optional. A tableless module omits them and omits `entrypoints.schema`.
|
||||
The schema and migration paths are optional. A tableless module omits `entrypoints.schema`; a
|
||||
module with no migration history omits `entrypoints.migrations` and needs no placeholder files.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -79,7 +88,8 @@ without executing module code:
|
||||
},
|
||||
"entrypoints": {
|
||||
"server": "./dist/server/index.js",
|
||||
"schema": "./dist/schema/index.js"
|
||||
"schema": "./dist/schema/index.js",
|
||||
"migrations": "./drizzle"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -89,10 +99,11 @@ 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.
|
||||
|
||||
Server and optional schema entrypoints must be compiled ESM `.js` or `.mjs` files, begin with `./`,
|
||||
remain inside the package, and exist. Production never imports source TypeScript directly. Modules
|
||||
with no tables omit `entrypoints.schema`. Unknown manifest properties are rejected, including
|
||||
presentation fields such as components or renderers.
|
||||
Server and optional schema entrypoints must be compiled ESM `.js` or `.mjs` files. The optional
|
||||
migration entrypoint is a directory. All entrypoints begin with `./`, remain inside the package,
|
||||
and exist. Production never imports source TypeScript directly. Modules with no tables omit
|
||||
`entrypoints.schema`; modules with no migration history omit `entrypoints.migrations`. Unknown
|
||||
manifest properties are rejected, including presentation fields such as components or renderers.
|
||||
|
||||
Storage bucket IDs are logical names scoped to the module. Modules may request multiple buckets.
|
||||
Core will later map them to deployment-specific physical S3 buckets and provide scoped access;
|
||||
@@ -135,18 +146,27 @@ close them in `stop`.
|
||||
|
||||
## Drizzle schema definition
|
||||
|
||||
A module with tables exports a declaration-only schema definition from `src/schema/index.ts`:
|
||||
A module with tables keeps the generator-facing declarations in `src/schema/tables.ts`:
|
||||
|
||||
```ts
|
||||
import { createModuleTable, defineModuleSchema } from "@ekklesia/module-sdk";
|
||||
import { integer, text } from "drizzle-orm/pg-core";
|
||||
import { integer, pgTableCreator, text } from "drizzle-orm/pg-core";
|
||||
|
||||
const moduleTable = createModuleTable("calendar");
|
||||
const moduleTable = pgTableCreator((localName) => `calendar_${localName}`);
|
||||
|
||||
export const events = moduleTable("events", {
|
||||
id: integer("id").primaryKey(),
|
||||
title: text("title").notNull()
|
||||
});
|
||||
```
|
||||
|
||||
The production schema entrypoint in `src/schema/index.ts` wraps those same objects with the Module
|
||||
SDK ownership marker:
|
||||
|
||||
```ts
|
||||
import { defineModuleSchema } from "@ekklesia/module-sdk";
|
||||
import { events } from "./tables.js";
|
||||
|
||||
export { events } from "./tables.js";
|
||||
|
||||
export default defineModuleSchema({
|
||||
moduleId: "calendar",
|
||||
@@ -154,11 +174,12 @@ export default defineModuleSchema({
|
||||
});
|
||||
```
|
||||
|
||||
`createModuleTable` converts the module ID to a snake-case namespace and prefixes every local table
|
||||
The prefix callback converts the module ID to a snake-case namespace and prefixes every local table
|
||||
name. Here the physical PostgreSQL name is `calendar_events`; `care-team` plus `members` becomes
|
||||
`care_team_members`. Local names must be lowercase snake case, and the complete physical identifier
|
||||
must not exceed 63 characters. Core validates the built metadata even when a module does not use
|
||||
the helper.
|
||||
must not exceed 63 characters. The SDK's `createModuleTable` provides the same convenience when a
|
||||
generator-facing pure declaration file is not needed; Core validates built metadata independently
|
||||
of either helper.
|
||||
|
||||
Schema definitions use PostgreSQL tables in the platform-managed default/public schema. They must
|
||||
be side-effect-free: do not connect to a database, read configuration, or perform I/O during import.
|
||||
@@ -171,6 +192,45 @@ and all columns in a compound target must belong to the same exported target tab
|
||||
never import another module's tables, columns, relations, schema entrypoint, or migration files.
|
||||
Store opaque identifiers and integrate through public capabilities, events, or APIs instead.
|
||||
|
||||
## Drizzle migrations
|
||||
|
||||
Module migrations use the stable Drizzle Kit `0.31.10` PostgreSQL format. Pin that exact version in
|
||||
the module, set `migrations.prefix` to `index`, and generate from the module's own source schema:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: "./src/schema/tables.ts",
|
||||
out: "./drizzle",
|
||||
migrations: { prefix: "index" },
|
||||
breakpoints: true
|
||||
});
|
||||
```
|
||||
|
||||
Keep `tables.ts` limited to Drizzle declarations so the stable generator can load it without
|
||||
executing the ESM Module SDK wrapper. `src/schema/index.ts` remains the production compiled schema
|
||||
entrypoint and wraps those same exported table objects with `defineModuleSchema`; tables are not
|
||||
declared twice.
|
||||
|
||||
Run the module's `migration:generate` script with a lowercase snake-case name, review the generated
|
||||
SQL and snapshot, and add the matching entry to `drizzle/meta/_ekklesia.json`. Every entry must
|
||||
explicitly declare either `{"mode":"none"}` or
|
||||
`{"mode":"approval-required","reason":"..."}`. Do not add an `approved` flag; approval will be
|
||||
an external Core-owned record bound to the migration checksum.
|
||||
|
||||
Migration identities are `<module-id>:<generated-tag>`. Core preserves Drizzle journal order inside
|
||||
each module and dependency-first discovery order between modules. It calculates SHA-256 from the
|
||||
exact SQL bytes, so changing whitespace or line endings changes the checksum. Never edit an
|
||||
already-applied migration; the future database history will detect checksum changes. Add a new
|
||||
forward migration instead. Downgrade and uninstall migrations are not supported.
|
||||
|
||||
A module may migrate only its own objects. SQL is intentionally treated as opaque: offline
|
||||
validation checks the artifact and metadata contract but cannot prove SQL ownership or classify
|
||||
destructive behavior. Review those properties explicitly. Core remains the only future migration
|
||||
executor.
|
||||
|
||||
## Discovery and lifecycle
|
||||
|
||||
From the repository root, run:
|
||||
@@ -178,6 +238,7 @@ From the repository root, run:
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm modules:validate
|
||||
pnpm migrations:validate
|
||||
pnpm verify
|
||||
```
|
||||
|
||||
@@ -193,6 +254,12 @@ dependency-first and rejects invalid definitions, unnamespaced or duplicate tabl
|
||||
PostgreSQL schemas, and foreign keys outside the owning module. The aggregate is published only when
|
||||
every schema is valid. This slice does not connect to PostgreSQL or execute migrations.
|
||||
|
||||
`pnpm migrations:validate` performs complete static discovery first, then reads every declared
|
||||
migration directory without importing module code or connecting to PostgreSQL. It validates native
|
||||
Drizzle journals and snapshot chains, Ekklesia policy metadata, ordering, inventory, paths, and
|
||||
checksums atomically. Its concise report lists every installed module and migration in deterministic
|
||||
order.
|
||||
|
||||
Core completes discovery before importing any server entrypoint. It then loads and verifies every
|
||||
runtime definition before running hooks, registers all modules dependency-first into an unpublished
|
||||
staged capability registry, and starts them in the same order. Shutdown is reverse-order. If a later
|
||||
|
||||
Reference in New Issue
Block a user