import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { discoverModuleSchemas, ModuleDiscoveryError, ModuleSchemaDiscoveryError, type ModuleSchemaDiscoveryIssueCode, } from "../src/modules/index.js"; interface SchemaModuleFixture { readonly manifest?: Record; readonly packageJson?: Record; readonly schemaSource?: string; readonly serverSource?: string; } const drizzlePgCoreUrl = import.meta.resolve("drizzle-orm/pg-core"); const temporaryDirectories: string[] = []; const eventSymbols: symbol[] = []; afterEach(async () => { const globalValues = globalThis as unknown as Record; for (const symbol of eventSymbols.splice(0)) { delete globalValues[symbol]; } await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })), ); }); async function createModulesDirectory(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-schema-")); temporaryDirectories.push(directory); return directory; } function manifestFor( id: string, options: { dependencies?: Record; schema?: string | false } = {}, ): Record { return { manifestVersion: 6, 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 === false ? {} : { schema: options.schema ?? "./dist/schema/index.js" }), }, }; } async function createSchemaModule( modulesDirectory: string, id: string, fixture: SchemaModuleFixture = {}, ): Promise { const moduleDirectory = path.join(modulesDirectory, id); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); if (fixture.schemaSource !== undefined) { await mkdir(path.join(moduleDirectory, "dist/schema"), { recursive: true }); } await Promise.all([ writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify( fixture.packageJson ?? { 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)), ), 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)]), ]); } function createGlobalEventRecorder(): { events: string[]; expression: string } { const key = `ekklesia.schema.test.${eventSymbols.length}.${Date.now()}.${Math.random()}`; const symbol = Symbol.for(key); const events: string[] = []; (globalThis as unknown as Record)[symbol] = events; eventSymbols.push(symbol); return { events, expression: `globalThis[Symbol.for(${JSON.stringify(key)})]` }; } function discoveryOptions(modulesDirectory: string) { return { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }; } async function captureSchemaError(modulesDirectory: string): Promise { try { await discoverModuleSchemas(discoveryOptions(modulesDirectory)); } catch (error) { if (error instanceof ModuleSchemaDiscoveryError) { return error; } throw error; } throw new Error("Expected module schema discovery to fail."); } function issueCodes(error: ModuleSchemaDiscoveryError): ModuleSchemaDiscoveryIssueCode[] { return error.issues.map((issue) => issue.code); } describe("module schema discovery", () => { it("returns a frozen empty aggregate without importing module servers", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createGlobalEventRecorder(); await createSchemaModule(modulesDirectory, "server-only", { manifest: manifestFor("server-only", { schema: false }), serverSource: `${recorder.expression}.push("server-imported"); export default { id: "server-only", version: "0.1.0" };`, }); const aggregate = await discoverModuleSchemas(discoveryOptions(modulesDirectory)); expect(aggregate.discoveredModules).toHaveLength(1); expect(aggregate.modules).toEqual([]); expect(aggregate.tables).toEqual([]); expect(aggregate.drizzleSchema).toEqual({}); expect(recorder.events).toEqual([]); expect(Object.isFrozen(aggregate)).toBe(true); expect(Object.isFrozen(aggregate.tables)).toBe(true); }); it("completes all static module discovery before importing any schema code", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createGlobalEventRecorder(); await createSchemaModule(modulesDirectory, "valid", { schemaSource: `${recorder.expression}.push("schema-imported"); export default {};`, }); await createSchemaModule(modulesDirectory, "missing-schema"); let discoveryError: ModuleDiscoveryError | undefined; try { await discoverModuleSchemas(discoveryOptions(modulesDirectory)); } catch (error) { if (error instanceof ModuleDiscoveryError) { discoveryError = error; } else { throw error; } } expect(discoveryError?.issues).toContainEqual( expect.objectContaining({ code: "MODULE_ENTRYPOINT_MISSING", moduleId: "missing-schema", path: "entrypoints.schema", }), ); expect(recorder.events).toEqual([]); }); it("loads and aggregates module tables deterministically", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createGlobalEventRecorder(); await createSchemaModule(modulesDirectory, "calendar", { manifest: manifestFor("calendar", { dependencies: { modules: { required: { people: "^0.1.0" } } }, }), schemaSource: ` import { integer, pgTable, text } from ${JSON.stringify(drizzlePgCoreUrl)}; ${recorder.expression}.push("calendar:schema"); const events = pgTable("calendar_events", { id: integer("id").primaryKey() }); const locations = pgTable("calendar_locations", { id: integer("id").primaryKey(), eventId: integer("event_id").references(() => events.id) }); export default { moduleId: "calendar", tables: { locations, events } }; `, serverSource: 'throw new Error("server entrypoint must remain unloaded");', }); await createSchemaModule(modulesDirectory, "people", { schemaSource: ` import { integer, pgTable, text } from ${JSON.stringify(drizzlePgCoreUrl)}; ${recorder.expression}.push("people:schema"); const records = pgTable("people_records", { id: integer("id").primaryKey() }); const notes = pgTable("people_notes", { id: integer("id").primaryKey(), recordId: integer("record_id").references(() => records.id), body: text("body") }); export default { moduleId: "people", tables: { records, notes } }; `, }); const aggregate = await discoverModuleSchemas(discoveryOptions(modulesDirectory)); expect(recorder.events).toEqual(["people:schema", "calendar:schema"]); expect(aggregate.modules.map((module) => module.moduleId)).toEqual(["people", "calendar"]); expect(aggregate.modules[0]?.tables.map((table) => table.exportName)).toEqual([ "notes", "records", ]); expect(aggregate.modules[1]?.tables.map((table) => table.exportName)).toEqual([ "events", "locations", ]); expect(aggregate.tables.map((table) => table.name)).toEqual([ "calendar_events", "calendar_locations", "people_notes", "people_records", ]); expect(Object.keys(aggregate.drizzleSchema)).toEqual( aggregate.tables.map((table) => table.name), ); expect(Object.isFrozen(aggregate.modules)).toBe(true); expect(Object.isFrozen(aggregate.drizzleSchema)).toBe(true); expect(Object.isFrozen(aggregate.modules[0])).toBe(true); expect(Object.isFrozen(aggregate.modules[0]?.tables)).toBe(true); expect(Object.isFrozen(aggregate.tables[0])).toBe(true); }); it("aggregates import, export, and ownership-marker failures without leaking details", async () => { const modulesDirectory = await createModulesDirectory(); await createSchemaModule(modulesDirectory, "alpha", { schemaSource: 'throw new Error("database-password at /machine/private/path");', }); await createSchemaModule(modulesDirectory, "beta", { schemaSource: 'export default { moduleId: "beta", tables: { rows: {} } };', }); await createSchemaModule(modulesDirectory, "gamma", { schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; const rows = pgTable("gamma_rows", { id: integer("id") }); export default { moduleId: "wrong", tables: { rows } }; `, }); const error = await captureSchemaError(modulesDirectory); expect(issueCodes(error)).toEqual([ "MODULE_SCHEMA_IMPORT_FAILED", "MODULE_SCHEMA_EXPORT_INVALID", "MODULE_SCHEMA_MODULE_ID_MISMATCH", ]); const report = `${error.message}\n${error.stack}\n${JSON.stringify(error.issues)}`; expect(report).not.toContain("database-password"); expect(report).not.toContain("/machine/private/path"); expect(report).not.toContain(modulesDirectory); }); it("rejects malformed, unnamespaced, duplicate, and custom-schema tables", async () => { const modulesDirectory = await createModulesDirectory(); await createSchemaModule(modulesDirectory, "example", { schemaSource: ` import { integer, pgSchema, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; const duplicateA = pgTable("example_records", { id: integer("id") }); const duplicateB = pgTable("example_records", { id: integer("id") }); const invalid = pgTable("secret at /machine/path", { id: integer("id") }); const invalidLocal = pgTable("example_1records", { id: integer("id") }); const other = pgTable("other_records", { id: integer("id") }); const privateTable = pgSchema("private").table("example_private", { id: integer("id") }); export default { moduleId: "example", tables: { repeated: duplicateA, privateTable, other, invalid, invalidLocal, duplicateB, duplicateA } }; `, }); const error = await captureSchemaError(modulesDirectory); expect(issueCodes(error)).toEqual( expect.arrayContaining([ "MODULE_SCHEMA_POSTGRES_SCHEMA_UNSUPPORTED", "MODULE_SCHEMA_TABLE_DUPLICATE", "MODULE_SCHEMA_TABLE_NAME_INVALID", "MODULE_SCHEMA_TABLE_NAMESPACE_INVALID", ]), ); expect( issueCodes(error).filter((code) => code === "MODULE_SCHEMA_TABLE_DUPLICATE"), ).toHaveLength(2); expect( issueCodes(error).filter((code) => code === "MODULE_SCHEMA_TABLE_NAME_INVALID"), ).toHaveLength(2); expect(JSON.stringify(error.issues)).not.toContain("secret at /machine/path"); }); it("rejects duplicate physical names across module namespaces", async () => { const modulesDirectory = await createModulesDirectory(); for (const [moduleId, exportName] of [ ["foo", "barRows"], ["foo-bar", "rows"], ] as const) { await createSchemaModule(modulesDirectory, moduleId, { schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; const table = pgTable("foo_bar_rows", { id: integer("id") }); export default { moduleId: ${JSON.stringify(moduleId)}, tables: { ${exportName}: table } }; `, }); } const error = await captureSchemaError(modulesDirectory); expect(error.issues).toEqual([ expect.objectContaining({ code: "MODULE_SCHEMA_TABLE_DUPLICATE", moduleId: "foo-bar", tableName: "foo_bar_rows", }), ]); }); it("rejects cross-module and omitted local foreign-key targets", async () => { const modulesDirectory = await createModulesDirectory(); await createSchemaModule(modulesDirectory, "people", { schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; export const records = pgTable("people_records", { id: integer("id").primaryKey() }); export default { moduleId: "people", tables: { records } }; `, }); await createSchemaModule(modulesDirectory, "calendar", { manifest: manifestFor("calendar", { dependencies: { modules: { required: { people: "^0.1.0" } } }, }), schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; import { records } from "../../../people/dist/schema/index.js"; const privateTable = pgTable("calendar_private", { id: integer("id").primaryKey() }); const events = pgTable("calendar_events", { id: integer("id"), personId: integer("person_id").references(() => records.id) }); const attendees = pgTable("calendar_attendees", { id: integer("id"), privateId: integer("private_id").references(() => privateTable.id) }); export default { moduleId: "calendar", tables: { events, attendees } }; `, }); const error = await captureSchemaError(modulesDirectory); expect(issueCodes(error)).toEqual([ "MODULE_SCHEMA_FOREIGN_KEY_TARGET_MISSING", "MODULE_SCHEMA_CROSS_MODULE_FOREIGN_KEY", ]); }); it("rejects compound foreign keys that mix target-table ownership", async () => { const modulesDirectory = await createModulesDirectory(); await createSchemaModule(modulesDirectory, "people", { schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; export const records = pgTable("people_records", { id: integer("id").notNull(), alternateId: integer("alternate_id").notNull() }); export default { moduleId: "people", tables: { records } }; `, }); await createSchemaModule(modulesDirectory, "calendar", { manifest: manifestFor("calendar", { dependencies: { modules: { required: { people: "^0.1.0" } } }, }), schemaSource: ` import { foreignKey, integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; import { records } from "../../../people/dist/schema/index.js"; const targets = pgTable("calendar_targets", { id: integer("id").notNull(), alternateId: integer("alternate_id").notNull() }); const links = pgTable("calendar_links", { targetId: integer("target_id").notNull(), alternateId: integer("alternate_id").notNull() }, (table) => [ foreignKey({ columns: [table.targetId, table.alternateId], foreignColumns: [targets.id, records.alternateId] }) ]); export default { moduleId: "calendar", tables: { links, targets } }; `, }); const error = await captureSchemaError(modulesDirectory); expect(issueCodes(error)).toEqual(["MODULE_SCHEMA_FOREIGN_KEY_INVALID"]); }); it("rejects table aliases and sanitizes hostile metadata proxies", async () => { const modulesDirectory = await createModulesDirectory(); await createSchemaModule(modulesDirectory, "alias-table", { schemaSource: ` import { alias, integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; const base = pgTable("private_records", { id: integer("id") }); const records = alias(base, "alias_table_records"); export default { moduleId: "alias-table", tables: { records } }; `, }); await createSchemaModule(modulesDirectory, "proxy-table", { schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; const base = pgTable("proxy_table_records", { id: integer("id") }); const aliasSymbol = Symbol.for("drizzle:IsAlias"); let aliasReads = 0; const records = new Proxy(base, { get(target, key, receiver) { if (key === aliasSymbol && ++aliasReads > 1) { throw new Error("proxy-secret at /machine/private/schema"); } return Reflect.get(target, key, receiver); } }); export default { moduleId: "proxy-table", tables: { records } }; `, }); const error = await captureSchemaError(modulesDirectory); expect(issueCodes(error)).toEqual([ "MODULE_SCHEMA_EXPORT_INVALID", "MODULE_SCHEMA_TABLE_INVALID", ]); const report = `${error.stack}\n${JSON.stringify(error.issues)}`; expect(report).not.toContain("proxy-secret"); expect(report).not.toContain("/machine/private/schema"); }); it("sanitizes errors thrown while resolving lazy foreign-key metadata", async () => { const modulesDirectory = await createModulesDirectory(); await createSchemaModule(modulesDirectory, "unsafe", { schemaSource: ` import { integer, pgTable } from ${JSON.stringify(drizzlePgCoreUrl)}; const rows = pgTable("unsafe_rows", { id: integer("id"), ownerId: integer("owner_id").references(() => { throw new Error("foreign-key-secret at /machine/schema/path"); }) }); export default { moduleId: "unsafe", tables: { rows } }; `, }); const error = await captureSchemaError(modulesDirectory); expect(issueCodes(error)).toEqual(["MODULE_SCHEMA_FOREIGN_KEY_INVALID"]); expect(JSON.stringify(error.issues)).not.toContain("foreign-key-secret"); expect(JSON.stringify(error.issues)).not.toContain("/machine/schema/path"); }); it("produces schema diagnostics independent of the absolute modules path", async () => { const firstModulesDirectory = await createModulesDirectory(); const secondModulesDirectory = await createModulesDirectory(); for (const modulesDirectory of [firstModulesDirectory, secondModulesDirectory]) { await createSchemaModule(modulesDirectory, "broken", { schemaSource: 'throw new Error("secret at /machine/schema/path");', }); } const [firstError, secondError] = await Promise.all([ captureSchemaError(firstModulesDirectory), captureSchemaError(secondModulesDirectory), ]); expect(firstError.issues).toEqual(secondError.issues); expect(firstError.stack).toBe(secondError.stack); }); });