CI / verify (pull_request) Successful in 1m2s
CI / infrastructure-smoke (pull_request) Successful in 28s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / standalone-module (pull_request) Successful in 26s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m42s
Makes the public module-development package set usable from a module repository outside the monorepo. Nothing is published. - Curated @ekklesia/module-sdk: an explicit author API, no contracts barrel, no identity/authorization internals, no Drizzle and no legacy ModuleDatabase type. defineModuleRuntime returns the SDK's own named interfaces so a module's emitted declarations never reach into @ekklesia/contracts. defineModuleSchema is gone; createModuleTable and defineModuleSchemaMetadata live in @ekklesia/module-database. - Drizzle: no longer a dependency of module-sdk or contracts. Core keeps the legacy Drizzle schema validator (moved to core/src/modules) and its legacy runtime support; drizzle-orm is an optional peer only. - React is a peer (plus devDependency) of @ekklesia/ui; the boundary checker allows exactly that consumer-selected peer. - @ekklesia/typescript-config is publishable; module-ui-host is private. Every public package carries "ekklesia.distribution": "module-development" and no monorepo-root prepack. - module-pack reads the Node.js range from the module's own engines.node and recognizes Ekklesia packages by name plus that marker, so registry-installed packages are incorporated, SDK-compatibility checked, and never credited as third-party. - scripts/pack-public-packages.ts (pnpm packages:pack) rebuilds, packs and inspects the nine public tarballs; scripts/test-standalone-module.sh (pnpm test:standalone-module, new CI job) installs them into a fixture module outside the repository and proves it typechecks, builds, packages, and verifies. - Public-package boundary tests and module-development docs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
601 lines
19 KiB
TypeScript
601 lines
19 KiB
TypeScript
import { pathToFileURL } from "node:url";
|
|
import {
|
|
type ModuleId,
|
|
type ModuleSchemaMetadataDefinition,
|
|
moduleTableLocalNameSchema,
|
|
moduleTablePrefix,
|
|
parseModuleSchemaMetadataDefinition,
|
|
physicalModuleTableNameSchema,
|
|
} from "@ekklesia/contracts";
|
|
import {
|
|
type ModuleSchemaDefinition,
|
|
parseModuleSchemaDefinition,
|
|
} from "./legacy-schema-definition.js";
|
|
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;
|
|
/** Present only for a temporary legacy-facade module. ABI-v1 schema discovery publishes no ORM
|
|
* object and contributes nothing to the legacy aggregate below. */
|
|
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 LoadedLegacyModuleSchema {
|
|
readonly definition: ModuleSchemaDefinition;
|
|
readonly discovered: DiscoveredModule;
|
|
readonly mode: "legacy";
|
|
}
|
|
|
|
interface LoadedAbiModuleSchema {
|
|
readonly definition: ModuleSchemaMetadataDefinition;
|
|
readonly discovered: DiscoveredModule;
|
|
readonly mode: "abi-v1";
|
|
}
|
|
|
|
type LoadedModuleSchema = LoadedLegacyModuleSchema | LoadedAbiModuleSchema;
|
|
|
|
interface InspectedModuleSchema {
|
|
readonly discovered: DiscoveredModule;
|
|
readonly tables: ModuleTableCandidate[];
|
|
}
|
|
|
|
interface LegacyModuleTableCandidate extends DiscoveredModuleTable {
|
|
readonly mode: "legacy";
|
|
readonly foreignKeys: ReturnType<typeof getTableConfig>["foreignKeys"];
|
|
readonly path: string;
|
|
readonly table: AnyPgTable;
|
|
}
|
|
|
|
interface AbiModuleTableCandidate extends DiscoveredModuleTable {
|
|
readonly mode: "abi-v1";
|
|
readonly foreignKeys: ModuleSchemaMetadataDefinition["tables"][number]["foreignKeys"];
|
|
readonly path: string;
|
|
}
|
|
|
|
type ModuleTableCandidate = LegacyModuleTableCandidate | AbiModuleTableCandidate;
|
|
|
|
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 | ModuleSchemaMetadataDefinition;
|
|
const mode = discovered.manifest.database?.abi === 1 ? "abi-v1" : "legacy";
|
|
try {
|
|
definition =
|
|
mode === "abi-v1"
|
|
? parseModuleSchemaMetadataDefinition(defaultExport(moduleNamespace))
|
|
: 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(
|
|
mode === "abi-v1"
|
|
? { definition: definition as ModuleSchemaMetadataDefinition, discovered, mode }
|
|
: { definition: definition as ModuleSchemaDefinition, discovered, mode },
|
|
);
|
|
}
|
|
|
|
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;
|
|
if (loaded.mode === "abi-v1") {
|
|
const candidates: AbiModuleTableCandidate[] = [];
|
|
loaded.definition.tables.forEach((table, index) => {
|
|
const path = `tables.${index}`;
|
|
const candidate: AbiModuleTableCandidate = {
|
|
exportName: table.name,
|
|
foreignKeys: table.foreignKeys,
|
|
mode: "abi-v1",
|
|
moduleId,
|
|
name: table.name,
|
|
path,
|
|
};
|
|
const tablePrefix = moduleTablePrefix(moduleId);
|
|
if (!table.name.startsWith(tablePrefix)) {
|
|
issues.push(
|
|
issue(
|
|
"MODULE_SCHEMA_TABLE_NAMESPACE_INVALID",
|
|
moduleId,
|
|
`Table ${table.name} is not namespaced for module ${moduleId}.`,
|
|
{ path, tableName: table.name },
|
|
),
|
|
);
|
|
} else if (
|
|
!moduleTableLocalNameSchema.safeParse(table.name.slice(tablePrefix.length)).success
|
|
) {
|
|
issues.push(
|
|
issue(
|
|
"MODULE_SCHEMA_TABLE_NAME_INVALID",
|
|
moduleId,
|
|
`Module ${moduleId} has an invalid local table name.`,
|
|
{ path },
|
|
),
|
|
);
|
|
}
|
|
const existing = firstTableByName.get(table.name);
|
|
if (existing) {
|
|
issues.push(
|
|
issue(
|
|
"MODULE_SCHEMA_TABLE_DUPLICATE",
|
|
moduleId,
|
|
`Table ${table.name} is declared more than once.`,
|
|
{ path, tableName: table.name },
|
|
),
|
|
);
|
|
} else {
|
|
firstTableByName.set(table.name, candidate);
|
|
}
|
|
candidates.push(candidate);
|
|
});
|
|
inspectedModules.push({ discovered: loaded.discovered, tables: candidates });
|
|
continue;
|
|
}
|
|
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,
|
|
mode: "legacy",
|
|
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: LegacyModuleTableCandidate["foreignKeys"][number],
|
|
candidate: LegacyModuleTableCandidate,
|
|
): 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) {
|
|
if (candidate.mode === "legacy") {
|
|
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) {
|
|
if (candidate.mode === "abi-v1") {
|
|
const ownedNames = new Set(inspected.tables.map((table) => table.name));
|
|
candidate.foreignKeys.forEach((foreignKey, foreignKeyIndex) => {
|
|
const path = `${candidate.path}.foreignKeys.${foreignKeyIndex}`;
|
|
if (!foreignKey.table.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 },
|
|
),
|
|
);
|
|
} else if (!ownedNames.has(foreignKey.table)) {
|
|
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 },
|
|
),
|
|
);
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
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, mode: _mode, 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.flatMap((table) =>
|
|
table.table === undefined ? [] : [[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);
|
|
}
|