feat: add module schema discovery and validation
This commit is contained in:
@@ -10,12 +10,16 @@ strict semantic version, and the module layer marker:
|
||||
|
||||
```text
|
||||
modules/example/
|
||||
├── dist/schema/index.js # when the module owns tables
|
||||
├── dist/server/index.js
|
||||
├── ekklesia.module.json
|
||||
├── package.json
|
||||
├── src/schema/index.ts # when the module owns tables
|
||||
└── src/server/index.ts
|
||||
```
|
||||
|
||||
The schema paths are optional. A tableless module omits them and omits `entrypoints.schema`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "module",
|
||||
@@ -74,7 +78,8 @@ without executing module code:
|
||||
]
|
||||
},
|
||||
"entrypoints": {
|
||||
"server": "./dist/server/index.js"
|
||||
"server": "./dist/server/index.js",
|
||||
"schema": "./dist/schema/index.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -84,9 +89,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.
|
||||
|
||||
Server entrypoints must be compiled ESM `.js` or `.mjs` files, begin with `./`, remain inside the
|
||||
package, and exist. Production never imports source TypeScript directly. 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, 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.
|
||||
|
||||
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;
|
||||
@@ -127,6 +133,44 @@ Entrypoint imports must have no top-level side effects. Modules are trusted back
|
||||
evaluation cannot be rolled back. Keep registration declarative; open resources in `start` and
|
||||
close them in `stop`.
|
||||
|
||||
## Drizzle schema definition
|
||||
|
||||
A module with tables exports a declaration-only schema definition from `src/schema/index.ts`:
|
||||
|
||||
```ts
|
||||
import { createModuleTable, defineModuleSchema } from "@ekklesia/module-sdk";
|
||||
import { integer, text } from "drizzle-orm/pg-core";
|
||||
|
||||
const moduleTable = createModuleTable("calendar");
|
||||
|
||||
export const events = moduleTable("events", {
|
||||
id: integer("id").primaryKey(),
|
||||
title: text("title").notNull()
|
||||
});
|
||||
|
||||
export default defineModuleSchema({
|
||||
moduleId: "calendar",
|
||||
tables: { events }
|
||||
});
|
||||
```
|
||||
|
||||
`createModuleTable` 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.
|
||||
|
||||
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.
|
||||
The `moduleId` is an ownership marker and must match the static manifest. Export physical base
|
||||
tables, never query aliases.
|
||||
|
||||
Foreign keys may reference self or another exported table from the same module. They may not target
|
||||
another module or an omitted private table. Every source column must belong to the declaring table,
|
||||
and all columns in a compound target must belong to the same exported target table. Modules must
|
||||
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.
|
||||
|
||||
## Discovery and lifecycle
|
||||
|
||||
From the repository root, run:
|
||||
@@ -140,8 +184,14 @@ pnpm verify
|
||||
`pnpm modules:validate` builds the workspace and then atomically validates every installed module.
|
||||
It rejects malformed manifests, duplicate IDs or package names, version mismatches, incompatible
|
||||
platform or SDK ranges, missing required modules or capabilities, incompatible installed
|
||||
dependencies, cycles, and missing, escaping, or non-JavaScript server entrypoints. Missing optional
|
||||
dependencies are allowed. Valid modules are ordered dependency-first with stable identifier sorting.
|
||||
dependencies, cycles, and missing, escaping, or non-JavaScript server or schema entrypoints.
|
||||
Missing optional dependencies are allowed. Valid modules are ordered dependency-first with stable
|
||||
identifier sorting.
|
||||
|
||||
After complete static discovery succeeds, validation loads optional schema entrypoints
|
||||
dependency-first and rejects invalid definitions, unnamespaced or duplicate tables, custom
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user