import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; /** * A minimal, valid installed module written into a directory the caller owns: `package.json`, * `ekklesia.module.json`, a compiled-ESM server stub at `dist/server/index.js`, and, unless * disabled, a native Drizzle migration directory (journal, `_ekklesia.json` policy, snapshot chain, * and SQL) that passes Core's migration discovery. * * Everything is generated here, from nothing in the repository: no build output, no ignored file, * and no `node_modules`. Migration discovery imports no module code, so the stub is never run. */ export const originSnapshotId = "00000000-0000-0000-0000-000000000000"; export interface MigrationFixture { readonly breakpoints?: boolean; readonly destructive?: | { readonly mode: "none" } | { readonly mode: "approval-required"; readonly reason: string }; readonly snapshotId?: string; readonly sql: string | Uint8Array; readonly tag: string; readonly timestamp?: number; } export interface MigrationModuleFixture { readonly dependencies?: Record; readonly manifest?: Record; readonly migrations?: readonly MigrationFixture[] | false; readonly schemaSource?: string; readonly serverSource?: string; } export interface CreatedMigrationModule { readonly migrationDirectory?: string; readonly moduleDirectory: string; } export function snapshotId(index: number): string { return `10000000-0000-4000-8000-${(index + 1).toString(16).padStart(12, "0")}`; } export function manifestFor( id: string, options: { readonly dependencies?: Record; readonly migrations?: boolean; readonly schema?: boolean; } = {}, ): Record { return { manifestVersion: 8, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" }, ...(options.dependencies ? { dependencies: options.dependencies } : {}), capabilities: [], permissions: [], entrypoints: { server: "./dist/server/index.js", ...(options.schema ? { schema: "./dist/schema/index.js" } : {}), ...(options.migrations === false ? {} : { migrations: "./drizzle" }), }, }; } export function defaultMigrations(id: string): readonly MigrationFixture[] { return [ { sql: `CREATE TABLE "${id.replaceAll("-", "_")}_records" ("id" integer);\n`, tag: "0000_initial", }, ]; } export async function createMigrationModule( modulesDirectory: string, id: string, fixture: MigrationModuleFixture = {}, ): Promise { const migrations = fixture.migrations === false ? undefined : (fixture.migrations ?? defaultMigrations(id)); const moduleDirectory = path.join(modulesDirectory, id); const migrationDirectory = migrations ? path.join(moduleDirectory, "drizzle") : undefined; await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); if (fixture.schemaSource !== undefined) { await mkdir(path.join(moduleDirectory, "dist/schema"), { recursive: true }); } if (migrationDirectory) { await mkdir(path.join(migrationDirectory, "meta"), { recursive: true }); } await Promise.all([ writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ name: `@test/module-${id}`, version: "0.1.0", type: "module", ekklesia: { layer: "module" }, }), ), writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify( fixture.manifest ?? manifestFor(id, { ...(fixture.dependencies ? { dependencies: fixture.dependencies } : {}), migrations: migrations !== undefined, schema: fixture.schemaSource !== undefined, }), ), ), writeFile( path.join(moduleDirectory, "dist/server/index.js"), fixture.serverSource ?? `export default { id: ${JSON.stringify(id)}, version: "0.1.0" };`, ), ...(fixture.schemaSource === undefined ? [] : [writeFile(path.join(moduleDirectory, "dist/schema/index.js"), fixture.schemaSource)]), ]); if (!migrationDirectory || !migrations) { return { moduleDirectory }; } const journalEntries = migrations.map((migration, index) => ({ idx: index, version: "7", when: migration.timestamp ?? 1_700_000_000_000 + index, tag: migration.tag, breakpoints: migration.breakpoints ?? true, })); const policyEntries = migrations.map((migration) => ({ tag: migration.tag, destructive: migration.destructive ?? { mode: "none" }, })); await Promise.all([ writeFile( path.join(migrationDirectory, "meta/_journal.json"), JSON.stringify({ version: "7", dialect: "postgresql", entries: journalEntries }), ), writeFile( path.join(migrationDirectory, "meta/_ekklesia.json"), JSON.stringify({ version: 1, moduleId: id, entries: policyEntries }), ), ...migrations.flatMap((migration, index) => { const idForSnapshot = migration.snapshotId ?? snapshotId(index); const previousId = index === 0 ? originSnapshotId : (migrations[index - 1]?.snapshotId ?? snapshotId(index - 1)); return [ writeFile(path.join(migrationDirectory, `${migration.tag}.sql`), migration.sql), writeFile( path.join(migrationDirectory, `meta/${index.toString().padStart(4, "0")}_snapshot.json`), JSON.stringify({ version: "7", dialect: "postgresql", id: idForSnapshot, prevId: previousId, }), ), ]; }), ]); return { migrationDirectory, moduleDirectory }; }