feat: consolidate database management work
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.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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`;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user