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
@@ -0,0 +1,80 @@
import { z } from "zod";
import { moduleIdSchema } from "./identifiers.js";
import { physicalModuleTableNameSchema } from "./module-schema-names.js";
export const moduleSchemaForeignKeyMetadataSchema = z
.object({ table: physicalModuleTableNameSchema })
.strict();
export const moduleSchemaTableMetadataSchema = z
.object({
name: physicalModuleTableNameSchema,
foreignKeys: z.array(moduleSchemaForeignKeyMetadataSchema).default(() => []),
})
.strict();
/** The ABI-v1 schema entrypoint: inert Ekklesia-owned data, with no ORM object or executable
* callback anywhere in its shape. Ownership and foreign-key targets are validated by Core against
* the manifest module id and the complete table list. */
export const moduleSchemaMetadataDefinitionSchema = z
.object({
moduleId: moduleIdSchema,
tables: z.array(moduleSchemaTableMetadataSchema).min(1),
})
.strict();
export interface ModuleSchemaForeignKeyMetadata {
readonly table: string;
}
export interface ModuleSchemaTableMetadata {
readonly name: string;
readonly foreignKeys: readonly ModuleSchemaForeignKeyMetadata[];
}
export interface ModuleSchemaMetadataDefinition {
readonly moduleId: z.output<typeof moduleIdSchema>;
readonly tables: readonly ModuleSchemaTableMetadata[];
}
function isPlainData(value: unknown): boolean {
if (value === null || typeof value === "string") {
return true;
}
if (Array.isArray(value)) {
return value.every(isPlainData);
}
if (typeof value !== "object") {
return false;
}
const prototype = Object.getPrototypeOf(value);
return (
(prototype === Object.prototype || prototype === null) &&
Reflect.ownKeys(value).every(
(key) => typeof key === "string" && isPlainData((value as Record<string, unknown>)[key]),
)
);
}
export function parseModuleSchemaMetadataDefinition(
value: unknown,
): ModuleSchemaMetadataDefinition {
if (!isPlainData(value)) {
throw new TypeError("Invalid Ekklesia module schema metadata definition.");
}
const parsed = moduleSchemaMetadataDefinitionSchema.safeParse(value);
if (!parsed.success) {
throw new TypeError("Invalid Ekklesia module schema metadata definition.");
}
return Object.freeze({
moduleId: parsed.data.moduleId,
tables: Object.freeze(
parsed.data.tables.map((table) =>
Object.freeze({
foreignKeys: Object.freeze(
table.foreignKeys.map((foreignKey) => Object.freeze(foreignKey)),
),
name: table.name,
}),
),
),
});
}