feat: add module schema discovery and validation
CI / verify (pull_request) Successful in 24s
CI / infrastructure-smoke (pull_request) Successful in 20s

This commit is contained in:
ekklesia
2026-07-13 21:35:21 -05:00
parent 2465d9c1f7
commit 80b56b72b1
31 changed files with 1584 additions and 34 deletions
+2 -1
View File
@@ -23,7 +23,8 @@
"typecheck": "tsc --noEmit --project tsconfig.json"
},
"dependencies": {
"@ekklesia/contracts": "workspace:*"
"@ekklesia/contracts": "workspace:*",
"drizzle-orm": "0.45.2"
},
"devDependencies": {
"@biomejs/biome": "2.5.3",
+16 -3
View File
@@ -1,6 +1,10 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { discoverModules, ModuleDiscoveryError } from "../modules/index.js";
import {
discoverModuleSchemas,
ModuleDiscoveryError,
ModuleSchemaDiscoveryError,
} from "../modules/index.js";
interface PackageVersion {
version?: unknown;
@@ -20,18 +24,21 @@ async function main(): Promise<void> {
readPackageVersion(path.join(repositoryRoot, "package.json")),
readPackageVersion(path.join(repositoryRoot, "packages/module-sdk/package.json")),
]);
const modules = await discoverModules({
const schemaAggregate = await discoverModuleSchemas({
moduleSdkVersion,
modulesDirectory: path.join(repositoryRoot, "modules"),
platformVersion,
});
const modules = schemaAggregate.discoveredModules;
if (modules.length === 0) {
console.log("Module validation passed (0 modules installed).");
return;
}
console.log(`Module validation passed (${modules.length} modules):`);
console.log(
`Module validation passed (${modules.length} modules, ${schemaAggregate.tables.length} tables):`,
);
for (const discoveredModule of modules) {
console.log(`- ${discoveredModule.manifest.id}@${discoveredModule.manifest.version}`);
}
@@ -44,6 +51,12 @@ main().catch((error: unknown) => {
const issuePath = issue.path ? ` (${issue.path})` : "";
console.error(`[${issue.code}] ${issue.moduleDirectory}${issuePath}: ${issue.message}`);
}
} else if (error instanceof ModuleSchemaDiscoveryError) {
console.error(error.message);
for (const issue of error.issues) {
const issuePath = issue.path ? ` (${issue.path})` : "";
console.error(`[${issue.code}] ${issue.moduleId}${issuePath}: ${issue.message}`);
}
} else {
console.error(error instanceof Error ? error.message : String(error));
}
+3 -3
View File
@@ -238,15 +238,15 @@ async function resolveEntrypoints(
continue;
}
if (
entrypointName === "server" &&
["schema", "server"].includes(entrypointName) &&
![".js", ".mjs"].includes(path.extname(relativeEntrypoint).toLowerCase())
) {
issues.push({
code: "MODULE_ENTRYPOINT_INVALID",
message: "server must reference compiled ESM JavaScript with a .js or .mjs extension.",
message: `${entrypointName} must reference compiled ESM JavaScript with a .js or .mjs extension.`,
moduleDirectory: directoryName,
moduleId: manifest.id,
path: "entrypoints.server",
path: `entrypoints.${entrypointName}`,
});
continue;
}
+1
View File
@@ -1,2 +1,3 @@
export * from "./discovery.js";
export * from "./runtime.js";
export * from "./schema-discovery.js";
+480
View File
@@ -0,0 +1,480 @@
import { pathToFileURL } from "node:url";
import {
type ModuleId,
type ModuleSchemaDefinition,
moduleTableLocalNameSchema,
moduleTablePrefix,
parseModuleSchemaDefinition,
physicalModuleTableNameSchema,
} from "@ekklesia/contracts";
import { is } from "drizzle-orm";
import { type AnyPgTable, getTableConfig, PgColumn, PgTable } from "drizzle-orm/pg-core";
import {
type DiscoveredModule,
discoverModules,
type ModuleDiscoveryOptions,
} from "./discovery.js";
export type ModuleSchemaDiscoveryIssueCode =
| "MODULE_SCHEMA_CROSS_MODULE_FOREIGN_KEY"
| "MODULE_SCHEMA_EXPORT_INVALID"
| "MODULE_SCHEMA_FOREIGN_KEY_INVALID"
| "MODULE_SCHEMA_FOREIGN_KEY_TARGET_MISSING"
| "MODULE_SCHEMA_IMPORT_FAILED"
| "MODULE_SCHEMA_MODULE_ID_MISMATCH"
| "MODULE_SCHEMA_POSTGRES_SCHEMA_UNSUPPORTED"
| "MODULE_SCHEMA_TABLE_DUPLICATE"
| "MODULE_SCHEMA_TABLE_INVALID"
| "MODULE_SCHEMA_TABLE_NAME_INVALID"
| "MODULE_SCHEMA_TABLE_NAMESPACE_INVALID";
export interface ModuleSchemaDiscoveryIssue {
readonly code: ModuleSchemaDiscoveryIssueCode;
readonly message: string;
readonly moduleId: ModuleId;
readonly path?: string;
readonly tableName?: string;
}
export interface DiscoveredModuleTable {
readonly exportName: string;
readonly moduleId: ModuleId;
readonly name: string;
readonly table: AnyPgTable;
}
export interface DiscoveredModuleSchema {
readonly moduleId: ModuleId;
readonly tables: readonly DiscoveredModuleTable[];
}
export interface ModuleSchemaAggregate {
readonly discoveredModules: readonly DiscoveredModule[];
readonly drizzleSchema: Readonly<Record<string, AnyPgTable>>;
readonly modules: readonly DiscoveredModuleSchema[];
readonly tables: readonly DiscoveredModuleTable[];
}
interface LoadedModuleSchema {
readonly definition: ModuleSchemaDefinition;
readonly discovered: DiscoveredModule;
}
interface InspectedModuleSchema {
readonly discovered: DiscoveredModule;
readonly tables: ModuleTableCandidate[];
}
interface ModuleTableCandidate extends DiscoveredModuleTable {
readonly foreignKeys: ReturnType<typeof getTableConfig>["foreignKeys"];
readonly path: string;
}
const drizzleTableIsAlias = Symbol.for("drizzle:IsAlias");
export class ModuleSchemaDiscoveryError extends Error {
readonly issues: readonly ModuleSchemaDiscoveryIssue[];
constructor(issues: readonly ModuleSchemaDiscoveryIssue[]) {
const sortedIssues = [...issues].sort(compareIssues);
super(`Module schema discovery failed with ${sortedIssues.length} issue(s).`);
this.name = "ModuleSchemaDiscoveryError";
this.stack = `${this.name}: ${this.message}`;
this.issues = Object.freeze(sortedIssues.map((issue) => Object.freeze({ ...issue })));
}
}
function compareText(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function compareIssues(
left: ModuleSchemaDiscoveryIssue,
right: ModuleSchemaDiscoveryIssue,
): number {
return (
compareText(left.moduleId, right.moduleId) ||
compareText(left.tableName ?? "", right.tableName ?? "") ||
compareText(left.path ?? "", right.path ?? "") ||
compareText(left.code, right.code) ||
compareText(left.message, right.message)
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isUnaliasedPgTable(value: unknown): value is AnyPgTable {
try {
return (
is(value, PgTable) &&
(value as unknown as Record<PropertyKey, unknown>)[drizzleTableIsAlias] === false
);
} catch {
return false;
}
}
function defaultExport(moduleNamespace: unknown): unknown {
return isRecord(moduleNamespace) ? moduleNamespace.default : undefined;
}
function issue(
code: ModuleSchemaDiscoveryIssueCode,
moduleId: ModuleId,
message: string,
details: Pick<ModuleSchemaDiscoveryIssue, "path" | "tableName"> = {},
): ModuleSchemaDiscoveryIssue {
return { code, message, moduleId, ...details };
}
async function loadSchemaDefinitions(discoveredModules: readonly DiscoveredModule[]): Promise<{
readonly issues: readonly ModuleSchemaDiscoveryIssue[];
readonly loaded: readonly LoadedModuleSchema[];
}> {
const issues: ModuleSchemaDiscoveryIssue[] = [];
const loaded: LoadedModuleSchema[] = [];
for (const discovered of discoveredModules) {
const schemaEntrypoint = discovered.entrypoints.schema;
if (!schemaEntrypoint) {
continue;
}
let moduleNamespace: unknown;
try {
moduleNamespace = (await import(pathToFileURL(schemaEntrypoint).href)) as unknown;
} catch {
issues.push(
issue(
"MODULE_SCHEMA_IMPORT_FAILED",
discovered.manifest.id,
`Module ${discovered.manifest.id} schema entrypoint could not be imported.`,
),
);
continue;
}
let definition: ModuleSchemaDefinition;
try {
definition = parseModuleSchemaDefinition(defaultExport(moduleNamespace));
} catch {
issues.push(
issue(
"MODULE_SCHEMA_EXPORT_INVALID",
discovered.manifest.id,
`Module ${discovered.manifest.id} schema entrypoint has an invalid default export.`,
),
);
continue;
}
if (definition.moduleId !== discovered.manifest.id) {
issues.push(
issue(
"MODULE_SCHEMA_MODULE_ID_MISMATCH",
discovered.manifest.id,
`Module ${discovered.manifest.id} schema definition does not match its manifest ID.`,
{ path: "moduleId" },
),
);
continue;
}
loaded.push({ definition, discovered });
}
return { issues, loaded };
}
function inspectTables(
loadedSchemas: readonly LoadedModuleSchema[],
issues: ModuleSchemaDiscoveryIssue[],
): readonly InspectedModuleSchema[] {
const inspectedModules: InspectedModuleSchema[] = [];
const firstTableByName = new Map<string, ModuleTableCandidate>();
for (const loaded of loadedSchemas) {
const moduleId = loaded.discovered.manifest.id;
const seenTables = new Set<AnyPgTable>();
const candidates: ModuleTableCandidate[] = [];
const entries = Object.entries(loaded.definition.tables).sort(([left], [right]) =>
compareText(left, right),
);
entries.forEach(([exportName, table], index) => {
const path = `tables.${index}`;
if (!isUnaliasedPgTable(table)) {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_INVALID",
moduleId,
`Module ${moduleId} schema contains a non-PostgreSQL table.`,
{ path },
),
);
return;
}
if (seenTables.has(table)) {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_DUPLICATE",
moduleId,
`Module ${moduleId} schema exports the same table more than once.`,
{ path },
),
);
return;
}
seenTables.add(table);
let config: ReturnType<typeof getTableConfig>;
try {
config = getTableConfig(table);
} catch {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_INVALID",
moduleId,
`Module ${moduleId} table metadata could not be inspected.`,
{ path },
),
);
return;
}
if (config.schema !== undefined && config.schema !== "public") {
issues.push(
issue(
"MODULE_SCHEMA_POSTGRES_SCHEMA_UNSUPPORTED",
moduleId,
`Module ${moduleId} tables must use the platform-managed public PostgreSQL schema.`,
{ path },
),
);
return;
}
if (!physicalModuleTableNameSchema.safeParse(config.name).success) {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_NAME_INVALID",
moduleId,
`Module ${moduleId} has an invalid PostgreSQL table name.`,
{ path },
),
);
return;
}
const tablePrefix = moduleTablePrefix(moduleId);
if (!config.name.startsWith(tablePrefix)) {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_NAMESPACE_INVALID",
moduleId,
`Table ${config.name} is not namespaced for module ${moduleId}.`,
{ path, tableName: config.name },
),
);
return;
}
if (!moduleTableLocalNameSchema.safeParse(config.name.slice(tablePrefix.length)).success) {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_NAME_INVALID",
moduleId,
`Module ${moduleId} has an invalid local table name.`,
{ path },
),
);
return;
}
const candidate: ModuleTableCandidate = {
exportName,
foreignKeys: config.foreignKeys,
moduleId,
name: config.name,
path,
table,
};
const existing = firstTableByName.get(config.name);
if (existing) {
issues.push(
issue(
"MODULE_SCHEMA_TABLE_DUPLICATE",
moduleId,
`Table ${config.name} is declared more than once.`,
{ path, tableName: config.name },
),
);
} else {
firstTableByName.set(config.name, candidate);
}
candidates.push(candidate);
});
inspectedModules.push({ discovered: loaded.discovered, tables: candidates });
}
return inspectedModules;
}
function validForeignKeyTarget(
foreignKey: ModuleTableCandidate["foreignKeys"][number],
candidate: ModuleTableCandidate,
): AnyPgTable | undefined {
try {
const reference = foreignKey.reference();
const foreignTable = reference.foreignTable;
if (
foreignKey.table !== candidate.table ||
!Array.isArray(reference.columns) ||
!Array.isArray(reference.foreignColumns) ||
reference.columns.length === 0 ||
reference.columns.length !== reference.foreignColumns.length ||
!isUnaliasedPgTable(foreignTable) ||
!reference.columns.every(
(column) => is(column, PgColumn) && column.table === candidate.table,
) ||
!reference.foreignColumns.every(
(column) => is(column, PgColumn) && column.table === foreignTable,
)
) {
return undefined;
}
return foreignTable;
} catch {
return undefined;
}
}
function inspectForeignKeys(
inspectedModules: readonly InspectedModuleSchema[],
issues: ModuleSchemaDiscoveryIssue[],
): void {
const ownerByTable = new Map<AnyPgTable, ModuleId>();
for (const inspected of inspectedModules) {
for (const candidate of inspected.tables) {
ownerByTable.set(candidate.table, candidate.moduleId);
}
}
for (const inspected of inspectedModules) {
const moduleId = inspected.discovered.manifest.id;
const ownedTables = new Set(inspected.tables.map((candidate) => candidate.table));
for (const candidate of inspected.tables) {
candidate.foreignKeys.forEach((foreignKey, foreignKeyIndex) => {
const path = `${candidate.path}.foreignKeys.${foreignKeyIndex}`;
const foreignTable = validForeignKeyTarget(foreignKey, candidate);
if (!foreignTable) {
issues.push(
issue(
"MODULE_SCHEMA_FOREIGN_KEY_INVALID",
moduleId,
`Table ${candidate.name} has a foreign key that could not be inspected.`,
{ path, tableName: candidate.name },
),
);
return;
}
if (ownedTables.has(foreignTable)) {
return;
}
const knownOwner = ownerByTable.get(foreignTable);
if (knownOwner && knownOwner !== moduleId) {
issues.push(
issue(
"MODULE_SCHEMA_CROSS_MODULE_FOREIGN_KEY",
moduleId,
`Table ${candidate.name} has a foreign key outside module ${moduleId}.`,
{ path, tableName: candidate.name },
),
);
return;
}
try {
const targetName = getTableConfig(foreignTable).name;
if (
physicalModuleTableNameSchema.safeParse(targetName).success &&
!targetName.startsWith(moduleTablePrefix(moduleId))
) {
issues.push(
issue(
"MODULE_SCHEMA_CROSS_MODULE_FOREIGN_KEY",
moduleId,
`Table ${candidate.name} has a foreign key outside module ${moduleId}.`,
{ path, tableName: candidate.name },
),
);
return;
}
} catch {
issues.push(
issue(
"MODULE_SCHEMA_FOREIGN_KEY_INVALID",
moduleId,
`Table ${candidate.name} has a foreign key that could not be inspected.`,
{ path, tableName: candidate.name },
),
);
return;
}
issues.push(
issue(
"MODULE_SCHEMA_FOREIGN_KEY_TARGET_MISSING",
moduleId,
`Table ${candidate.name} references a table not exported by module ${moduleId}.`,
{ path, tableName: candidate.name },
),
);
});
}
}
}
function publishAggregate(
discoveredModules: readonly DiscoveredModule[],
inspectedModules: readonly InspectedModuleSchema[],
): ModuleSchemaAggregate {
const modules = Object.freeze(
inspectedModules.map((inspected) =>
Object.freeze({
moduleId: inspected.discovered.manifest.id,
tables: Object.freeze(
inspected.tables
.map(({ foreignKeys: _foreignKeys, path: _path, ...table }) => Object.freeze(table))
.sort((left, right) => compareText(left.exportName, right.exportName)),
),
}),
),
);
const tables = Object.freeze(
modules
.flatMap((module) => module.tables)
.sort((left, right) => compareText(left.name, right.name)),
);
const drizzleSchema = Object.freeze(
Object.fromEntries(tables.map((table) => [table.name, table.table] as const)),
);
return Object.freeze({ discoveredModules, drizzleSchema, modules, tables });
}
export async function discoverModuleSchemas(
options: ModuleDiscoveryOptions,
): Promise<ModuleSchemaAggregate> {
const discoveredModules = await discoverModules(options);
const loaded = await loadSchemaDefinitions(discoveredModules);
const issues = [...loaded.issues];
const inspectedModules = inspectTables(loaded.loaded, issues);
inspectForeignKeys(inspectedModules, issues);
if (issues.length > 0) {
throw new ModuleSchemaDiscoveryError(issues);
}
return publishAggregate(discoveredModules, inspectedModules);
}
+88 -4
View File
@@ -397,12 +397,17 @@ describe("module discovery", () => {
await createModule(modulesDirectory, "missing-server", {
manifest: {
...manifestFor("missing-server"),
entrypoints: { server: "./dist/server/missing.js" },
entrypoints: {
server: "./dist/server/missing.js",
schema: "./dist/schema/missing.js",
},
},
});
let error = await discoveryError(modulesDirectory);
expect(issueCodes(error)).toContain("MODULE_ENTRYPOINT_MISSING");
expect(error.issues.filter((issue) => issue.code === "MODULE_ENTRYPOINT_MISSING")).toHaveLength(
2,
);
error = await discoveryError(modulesDirectory, { platformVersion: "invalid" });
expect(issueCodes(error)).toContain("DISCOVERY_CONFIGURATION_INVALID");
@@ -427,21 +432,100 @@ describe("module discovery", () => {
);
});
it.each([
"./src/schema/index.ts",
"./dist/schema/index.cjs",
])("rejects non-ESM schema entrypoint %s", async (schemaEntrypoint) => {
const modulesDirectory = await createModulesDirectory();
await createModule(modulesDirectory, "schema-source", {
manifest: {
...manifestFor("schema-source"),
entrypoints: {
server: "./dist/server/index.js",
schema: schemaEntrypoint,
},
},
});
const error = await discoveryError(modulesDirectory);
expect(error.issues).toContainEqual(
expect.objectContaining({
code: "MODULE_ENTRYPOINT_INVALID",
message: "schema must reference compiled ESM JavaScript with a .js or .mjs extension.",
path: "entrypoints.schema",
}),
);
});
it("accepts an optional compiled ESM schema entrypoint", async () => {
const modulesDirectory = await createModulesDirectory();
await createModule(modulesDirectory, "compiled-schema", {
manifest: {
...manifestFor("compiled-schema"),
entrypoints: {
server: "./dist/server/index.js",
schema: "./dist/schema/index.mjs",
},
},
});
const schemaDirectory = path.join(modulesDirectory, "compiled-schema", "dist/schema");
await mkdir(schemaDirectory, { recursive: true });
await writeFile(path.join(schemaDirectory, "index.mjs"), "export default {};\n");
const modules = await discoverModules({
moduleSdkVersion: "0.1.0",
modulesDirectory,
platformVersion: "0.1.0",
});
expect(modules[0]?.entrypoints.schema).toBe(path.join(schemaDirectory, "index.mjs"));
});
it("requires executable entrypoints to be files and migrations to be a directory", async () => {
const modulesDirectory = await createModulesDirectory();
await createModule(modulesDirectory, "wrong-entrypoint-kind", {
manifest: {
...manifestFor("wrong-entrypoint-kind"),
entrypoints: {
server: "./dist/server",
server: "./dist/server.js",
schema: "./dist/schema.js",
migrations: "./dist/server/index.js",
},
},
});
await Promise.all([
mkdir(path.join(modulesDirectory, "wrong-entrypoint-kind", "dist/server.js")),
mkdir(path.join(modulesDirectory, "wrong-entrypoint-kind", "dist/schema.js")),
]);
const error = await discoveryError(modulesDirectory);
expect(error.issues.filter((issue) => issue.code === "MODULE_ENTRYPOINT_INVALID")).toHaveLength(
2,
3,
);
});
it("rejects schema entrypoint links that escape the module directory", async () => {
const modulesDirectory = await createModulesDirectory();
await createModule(modulesDirectory, "linked-schema", {
manifest: {
...manifestFor("linked-schema"),
entrypoints: {
server: "./dist/server/index.js",
schema: "./dist/schema/index.js",
},
},
});
const schemaDirectory = path.join(modulesDirectory, "linked-schema", "dist/schema");
const externalSchema = path.join(modulesDirectory, "external-schema.js");
await mkdir(schemaDirectory, { recursive: true });
await writeFile(externalSchema, "export default {};\n");
await symlink("../../../external-schema.js", path.join(schemaDirectory, "index.js"));
const error = await discoveryError(modulesDirectory);
expect(error.issues).toContainEqual(
expect.objectContaining({
code: "MODULE_ENTRYPOINT_INVALID",
path: "entrypoints.schema",
}),
);
});
+489
View File
@@ -0,0 +1,489 @@
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<string, unknown>;
readonly packageJson?: Record<string, unknown>;
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<PropertyKey, unknown>;
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<string> {
const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-schema-"));
temporaryDirectories.push(directory);
return directory;
}
function manifestFor(
id: string,
options: { dependencies?: Record<string, unknown>; schema?: string | false } = {},
): Record<string, unknown> {
return {
manifestVersion: 2,
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<void> {
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<PropertyKey, unknown>)[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<ModuleSchemaDiscoveryError> {
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);
});
});