/** * The legacy module schema definition (ADR 0007): a v8 module that declares no `database` default- * exports `{ moduleId, tables }` with its Drizzle table objects themselves. Core alone validates it, * for the legacy facade's deprecation window (ADR 0050). It lives here, beside Core's own Drizzle * dependency, so that no package a module installs needs Drizzle at run time (ADR 0053). * * Erasable TypeScript with no relative imports: the verifier's probe imports this source file * directly in tests (`module-package-probe.ts`). */ import { is } from "drizzle-orm"; import { type AnyPgTable, PgTable } from "drizzle-orm/pg-core"; import { type ModuleId, moduleIdSchema } from "@ekklesia/contracts"; export interface ModuleSchemaDefinition< Tables extends Readonly> = Readonly>, > { readonly moduleId: ModuleId; readonly tables: Tables; } const schemaDefinitionKeys = new Set(["moduleId", "tables"]); const drizzleTableIsAlias = Symbol.for("drizzle:IsAlias"); function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function hasOnlyStringKeys(value: object, allowedKeys?: ReadonlySet): boolean { return Reflect.ownKeys(value).every( (key) => typeof key === "string" && (!allowedKeys || allowedKeys.has(key)), ); } export function isModuleSchemaDefinition(value: unknown): value is ModuleSchemaDefinition { try { if ( !isRecord(value) || !hasOnlyStringKeys(value, schemaDefinitionKeys) || !moduleIdSchema.safeParse(value.moduleId).success || !isRecord(value.tables) || !hasOnlyStringKeys(value.tables) ) { return false; } const tables = Object.values(value.tables); return ( tables.length > 0 && tables.every( (table) => is(table, PgTable) && (table as unknown as Record)[drizzleTableIsAlias] === false, ) ); } catch { return false; } } export function parseModuleSchemaDefinition(value: unknown): ModuleSchemaDefinition { if (!isModuleSchemaDefinition(value)) { throw new TypeError("Invalid Ekklesia module schema definition."); } const tables = Object.freeze( Object.fromEntries( Object.entries(value.tables).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0, ), ) as Record, ); return Object.freeze({ moduleId: value.moduleId, tables }); }