Add catalog-owned logical relationships and runtime snapshots, extend the database-management UI and validation coverage, and document the updated operational workflow. Keep active sensitive-generation status in a tooltip and indicator, and update the layout E2E to follow the history action in its new database-scoped location.
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import type { CatalogRelationship, CatalogRepository } from "./types.js";
|
|
|
|
export interface EffectiveRelationshipSnapshotReader {
|
|
list(databaseId: string): Promise<CatalogRelationship[]>;
|
|
}
|
|
|
|
export interface EffectiveRelationshipSnapshotCoordinator {
|
|
run<T>(databaseId: string, operation: () => Promise<T>): Promise<T>;
|
|
}
|
|
|
|
export class EffectiveRelationshipSnapshotStaleError extends Error {}
|
|
|
|
interface EffectiveRelationship {
|
|
sourceTable: string;
|
|
sourceColumns: string[];
|
|
targetTable: string;
|
|
targetColumns: string[];
|
|
origin: CatalogRelationship["origin"];
|
|
}
|
|
|
|
interface EffectiveRelationshipSnapshot {
|
|
schemaVersion: 1;
|
|
workspaceId: string;
|
|
relationships: EffectiveRelationship[];
|
|
}
|
|
|
|
const originRank: Record<CatalogRelationship["origin"], number> = {
|
|
physical: 0,
|
|
manual: 1,
|
|
generated: 2,
|
|
};
|
|
|
|
function endpointKey(relationship: EffectiveRelationship): string {
|
|
return [
|
|
relationship.sourceTable,
|
|
relationship.sourceColumns.join("\u0000"),
|
|
relationship.targetTable,
|
|
relationship.targetColumns.join("\u0000"),
|
|
].join("\u0001");
|
|
}
|
|
|
|
function compareRelationships(left: EffectiveRelationship, right: EffectiveRelationship): number {
|
|
return endpointKey(left).localeCompare(endpointKey(right))
|
|
|| originRank[left.origin] - originRank[right.origin];
|
|
}
|
|
|
|
/**
|
|
* Adapter from the mutable Catalog model to the immutable relationship contract consumed by the
|
|
* harness. The returned JSON is a deterministic projection, never an authored second store.
|
|
*/
|
|
export class EffectiveRelationshipSnapshotProvider {
|
|
constructor(
|
|
private readonly repository: Pick<CatalogRepository, "get" | "getByWorkspace">,
|
|
private readonly relationships: EffectiveRelationshipSnapshotReader,
|
|
private readonly operations: EffectiveRelationshipSnapshotCoordinator,
|
|
) {}
|
|
|
|
async render(workspaceId: string): Promise<string | undefined> {
|
|
const database = await this.repository.getByWorkspace(workspaceId);
|
|
if (!database) return undefined;
|
|
return await this.operations.run(database.id, async () => {
|
|
const current = await this.repository.get(database.id);
|
|
if (!current || current.schemaSyncedVersion !== current.version) {
|
|
throw new EffectiveRelationshipSnapshotStaleError(
|
|
"effective relationship snapshot requires a current full schema synchronization",
|
|
);
|
|
}
|
|
const projected = (await this.relationships.list(database.id))
|
|
.filter((relationship) => relationship.status === "active")
|
|
.map((relationship): EffectiveRelationship => ({
|
|
sourceTable: relationship.sourceTableName,
|
|
sourceColumns: relationship.columns.map((column) => column.sourceColumnName),
|
|
targetTable: relationship.targetTableName,
|
|
targetColumns: relationship.columns.map((column) => column.targetColumnName),
|
|
origin: relationship.origin,
|
|
}))
|
|
.sort(compareRelationships);
|
|
|
|
const seen = new Set<string>();
|
|
const snapshot: EffectiveRelationshipSnapshot = {
|
|
schemaVersion: 1,
|
|
workspaceId,
|
|
relationships: projected.filter((relationship) => {
|
|
const key = endpointKey(relationship);
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
}),
|
|
};
|
|
return `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
});
|
|
}
|
|
}
|