feat: add module migration contracts and validation
CI / verify (pull_request) Successful in 21s
CI / infrastructure-smoke (pull_request) Successful in 20s

This commit is contained in:
ekklesia
2026-07-21 08:09:30 -05:00
parent 37f54edd89
commit 88c28d75c9
30 changed files with 2947 additions and 48 deletions
+1
View File
@@ -1,6 +1,7 @@
export * from "./identifiers.js";
export * from "./module-lifecycle.js";
export * from "./module-manifest.js";
export * from "./module-migration.js";
export * from "./module-runtime.js";
export * from "./module-schema.js";
export * from "./versions.js";
@@ -0,0 +1,93 @@
import { z } from "zod";
import { moduleIdSchema } from "./identifiers.js";
export const DRIZZLE_MIGRATION_DIALECT = "postgresql";
export const DRIZZLE_MIGRATION_FORMAT_VERSION = "7";
export const DRIZZLE_MIGRATION_META_DIRECTORY = "meta";
export const DRIZZLE_MIGRATION_JOURNAL_FILENAME = "_journal.json";
export const EKKLESIA_MIGRATION_POLICY_FILENAME = "_ekklesia.json";
export const DRIZZLE_MIGRATION_ORIGIN_UUID = "00000000-0000-0000-0000-000000000000";
export const moduleMigrationTagSchema = z
.string()
.min(1)
.max(120)
.regex(
/^\d{4,}_[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/u,
"Must be an indexed lowercase Drizzle migration tag.",
);
export const drizzleMigrationJournalEntrySchema = z
.object({
idx: z.number().int().safe().nonnegative(),
version: z.literal(DRIZZLE_MIGRATION_FORMAT_VERSION),
when: z.number().int().safe().positive(),
tag: z.string().min(1).max(240),
breakpoints: z.boolean(),
})
.strict();
export const drizzleMigrationJournalSchema = z
.object({
version: z.literal(DRIZZLE_MIGRATION_FORMAT_VERSION),
dialect: z.literal(DRIZZLE_MIGRATION_DIALECT),
entries: z.array(drizzleMigrationJournalEntrySchema).min(1),
})
.strict();
export const drizzleMigrationSnapshotHeaderSchema = z
.object({
version: z.literal(DRIZZLE_MIGRATION_FORMAT_VERSION),
dialect: z.literal(DRIZZLE_MIGRATION_DIALECT),
id: z.uuid(),
prevId: z.uuid(),
})
.passthrough();
export const moduleMigrationDestructiveDeclarationSchema = z.discriminatedUnion("mode", [
z.object({ mode: z.literal("none") }).strict(),
z
.object({
mode: z.literal("approval-required"),
reason: z.string().trim().min(1).max(500),
})
.strict(),
]);
export const moduleMigrationPolicyEntrySchema = z
.object({
tag: moduleMigrationTagSchema,
destructive: moduleMigrationDestructiveDeclarationSchema,
})
.strict();
export const moduleMigrationPolicySchema = z
.object({
version: z.literal(1),
moduleId: moduleIdSchema,
entries: z.array(moduleMigrationPolicyEntrySchema).min(1),
})
.strict();
export function parseDrizzleMigrationJournal(input: unknown): DrizzleMigrationJournal {
return drizzleMigrationJournalSchema.parse(input);
}
export function parseDrizzleMigrationSnapshotHeader(
input: unknown,
): DrizzleMigrationSnapshotHeader {
return drizzleMigrationSnapshotHeaderSchema.parse(input);
}
export function parseModuleMigrationPolicy(input: unknown): ModuleMigrationPolicy {
return moduleMigrationPolicySchema.parse(input);
}
export type DrizzleMigrationJournal = z.output<typeof drizzleMigrationJournalSchema>;
export type DrizzleMigrationJournalEntry = z.output<typeof drizzleMigrationJournalEntrySchema>;
export type DrizzleMigrationSnapshotHeader = z.output<typeof drizzleMigrationSnapshotHeaderSchema>;
export type ModuleMigrationDestructiveDeclaration = z.output<
typeof moduleMigrationDestructiveDeclarationSchema
>;
export type ModuleMigrationPolicy = z.output<typeof moduleMigrationPolicySchema>;
export type ModuleMigrationPolicyEntry = z.output<typeof moduleMigrationPolicyEntrySchema>;