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:
@@ -14,7 +14,10 @@ import {
|
||||
type CatalogDatabaseMetadataDeleteTarget,
|
||||
type CatalogMetadataDeleteCounts,
|
||||
type CatalogMetrics,
|
||||
type CatalogRelationship,
|
||||
type CatalogLogicalRelationship,
|
||||
type CatalogLogicalRelationshipCandidate,
|
||||
type CatalogLogicalRelationshipContext,
|
||||
type CatalogPhysicalRelationship,
|
||||
type CatalogSchemaDiff,
|
||||
type CatalogSyncCounts,
|
||||
type CatalogSyncEvent,
|
||||
@@ -49,7 +52,8 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
private readonly records = new Map<string, WorkspaceDatabase>();
|
||||
private readonly tables = new Map<string, CatalogTable>();
|
||||
private readonly columns = new Map<string, CatalogColumn>();
|
||||
private readonly relationships = new Map<string, CatalogRelationship>();
|
||||
private readonly relationships = new Map<string, CatalogPhysicalRelationship>();
|
||||
private readonly logicalRelationships = new Map<string, CatalogLogicalRelationship>();
|
||||
private readonly descriptionGenerationRuns = new Map<string, DescriptionGenerationRun>();
|
||||
private readonly descriptionGenerationEvents = new Map<string, DescriptionGenerationEvent[]>();
|
||||
private readonly sensitiveDataSuggestionRuns = new Map<string, SensitiveDataSuggestionRun>();
|
||||
@@ -171,6 +175,9 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
for (const [relationshipId, relationship] of this.relationships) {
|
||||
if (relationship.databaseId === id) this.relationships.delete(relationshipId);
|
||||
}
|
||||
for (const [relationshipId, relationship] of this.logicalRelationships) {
|
||||
if (relationship.databaseId === id) this.logicalRelationships.delete(relationshipId);
|
||||
}
|
||||
for (const [runId, run] of this.descriptionGenerationRuns) {
|
||||
if (run.databaseId !== id) continue;
|
||||
this.descriptionGenerationRuns.delete(runId);
|
||||
@@ -527,12 +534,138 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
.map((event) => structuredClone(event));
|
||||
}
|
||||
|
||||
async listRelationships(databaseId: string): Promise<CatalogRelationship[]> {
|
||||
async listRelationships(databaseId: string): Promise<CatalogPhysicalRelationship[]> {
|
||||
return [...this.relationships.values()].filter((relationship) => relationship.databaseId === databaseId)
|
||||
.sort((a, b) => `${a.sourceTableName}.${a.constraintName}`.localeCompare(`${b.sourceTableName}.${b.constraintName}`))
|
||||
.map((relationship) => structuredClone(relationship));
|
||||
}
|
||||
|
||||
async listLogicalRelationships(databaseId: string): Promise<CatalogLogicalRelationship[]> {
|
||||
return [...this.logicalRelationships.values()]
|
||||
.filter((relationship) => relationship.databaseId === databaseId)
|
||||
.sort((a, b) => {
|
||||
const left = `${a.sourceTableName}.${a.columns[0].sourceColumnName}.${a.targetTableName}.${a.columns[0].targetColumnName}`;
|
||||
const right = `${b.sourceTableName}.${b.columns[0].sourceColumnName}.${b.targetTableName}.${b.columns[0].targetColumnName}`;
|
||||
return left.localeCompare(right);
|
||||
})
|
||||
.map((relationship) => structuredClone(relationship));
|
||||
}
|
||||
|
||||
async getLogicalRelationshipContext(
|
||||
databaseId: string,
|
||||
): Promise<CatalogLogicalRelationshipContext | undefined> {
|
||||
if (!this.records.has(databaseId)) return undefined;
|
||||
const tables = [...this.tables.values()].filter((table) => table.databaseId === databaseId);
|
||||
const tableById = new Map(tables.map((table) => [table.id, table]));
|
||||
const columns = [...this.columns.values()].filter((column) => tableById.has(column.tableId));
|
||||
const primaryKeyCounts = new Map<string, number>();
|
||||
for (const column of columns) {
|
||||
if (column.primaryKeyPosition !== null) {
|
||||
primaryKeyCounts.set(column.tableId, (primaryKeyCounts.get(column.tableId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
endpoints: columns.map((column) => ({
|
||||
columnId: column.id,
|
||||
columnName: column.name,
|
||||
tableId: column.tableId,
|
||||
tableName: tableById.get(column.tableId)!.name,
|
||||
dataType: column.dataType,
|
||||
primaryKeyPosition: column.primaryKeyPosition,
|
||||
tablePrimaryKeyColumnCount: primaryKeyCounts.get(column.tableId) ?? 0,
|
||||
})),
|
||||
physicalPairs: [...this.relationships.values()]
|
||||
.filter((relationship) => relationship.databaseId === databaseId)
|
||||
.flatMap((relationship) => relationship.columns.map((column) => ({
|
||||
sourceColumnId: column.sourceColumnId,
|
||||
targetColumnId: column.targetColumnId,
|
||||
}))),
|
||||
logicalRelationships: await this.listLogicalRelationships(databaseId),
|
||||
};
|
||||
}
|
||||
|
||||
async insertLogicalRelationship(
|
||||
databaseId: string,
|
||||
sourceColumnId: string,
|
||||
targetColumnId: string,
|
||||
generated: boolean,
|
||||
): Promise<CatalogLogicalRelationship | undefined> {
|
||||
if ([...this.logicalRelationships.values()].some((relationship) => (
|
||||
relationship.databaseId === databaseId
|
||||
&& relationship.columns[0].sourceColumnId === sourceColumnId
|
||||
&& relationship.columns[0].targetColumnId === targetColumnId
|
||||
))) return undefined;
|
||||
const sourceColumn = this.columns.get(sourceColumnId);
|
||||
const targetColumn = this.columns.get(targetColumnId);
|
||||
const sourceTable = sourceColumn ? this.tables.get(sourceColumn.tableId) : undefined;
|
||||
const targetTable = targetColumn ? this.tables.get(targetColumn.tableId) : undefined;
|
||||
if (!sourceColumn || !targetColumn || !sourceTable || !targetTable
|
||||
|| sourceTable.databaseId !== databaseId || targetTable.databaseId !== databaseId
|
||||
|| sourceColumnId === targetColumnId) return undefined;
|
||||
const now = new Date().toISOString();
|
||||
const relationship: CatalogLogicalRelationship = {
|
||||
id: randomUUID(),
|
||||
databaseId,
|
||||
constraintName: null,
|
||||
sourceTableId: sourceTable.id,
|
||||
sourceTableName: sourceTable.name,
|
||||
targetTableId: targetTable.id,
|
||||
targetTableName: targetTable.name,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
deferrable: false,
|
||||
initiallyDeferred: false,
|
||||
columns: [{
|
||||
position: 1,
|
||||
sourceColumnId,
|
||||
sourceColumnName: sourceColumn.name,
|
||||
targetColumnId,
|
||||
targetColumnName: targetColumn.name,
|
||||
}],
|
||||
lastSyncedDatabaseVersion: null,
|
||||
lastSyncedAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
origin: generated ? "generated" : "manual",
|
||||
status: "active",
|
||||
};
|
||||
this.logicalRelationships.set(relationship.id, relationship);
|
||||
return structuredClone(relationship);
|
||||
}
|
||||
|
||||
async insertGeneratedLogicalRelationships(
|
||||
databaseId: string,
|
||||
candidates: readonly CatalogLogicalRelationshipCandidate[],
|
||||
): Promise<number> {
|
||||
let added = 0;
|
||||
for (const candidate of candidates) {
|
||||
if (await this.insertLogicalRelationship(
|
||||
databaseId,
|
||||
candidate.sourceColumnId,
|
||||
candidate.targetColumnId,
|
||||
true,
|
||||
)) added += 1;
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
async setLogicalRelationshipStatus(
|
||||
databaseId: string,
|
||||
relationshipId: string,
|
||||
status: CatalogLogicalRelationship["status"],
|
||||
): Promise<CatalogLogicalRelationship | undefined> {
|
||||
const current = this.logicalRelationships.get(relationshipId);
|
||||
if (!current || current.databaseId !== databaseId) return undefined;
|
||||
const updated = { ...current, status, updatedAt: new Date().toISOString() };
|
||||
this.logicalRelationships.set(relationshipId, updated);
|
||||
return structuredClone(updated);
|
||||
}
|
||||
|
||||
async deleteLogicalRelationship(databaseId: string, relationshipId: string): Promise<boolean> {
|
||||
const current = this.logicalRelationships.get(relationshipId);
|
||||
return Boolean(current?.databaseId === databaseId && this.logicalRelationships.delete(relationshipId));
|
||||
}
|
||||
|
||||
async deleteDatabaseMetadata(
|
||||
databaseIds: readonly string[],
|
||||
target: CatalogDatabaseMetadataDeleteTarget,
|
||||
@@ -574,6 +707,12 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
const columns = [...this.columns.values()].filter((column) => selected.has(column.tableId));
|
||||
const deletedColumnIds = new Set(columns.map((column) => column.id));
|
||||
for (const column of columns) this.columns.delete(column.id);
|
||||
for (const relationship of [...this.logicalRelationships.values()]) {
|
||||
const pair = relationship.columns[0];
|
||||
if (deletedColumnIds.has(pair.sourceColumnId) || deletedColumnIds.has(pair.targetColumnId)) {
|
||||
this.logicalRelationships.delete(relationship.id);
|
||||
}
|
||||
}
|
||||
for (const [relationshipId, relationship] of this.relationships) {
|
||||
if (relationship.databaseId !== databaseId) continue;
|
||||
this.relationships.set(relationshipId, {
|
||||
@@ -857,6 +996,8 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
lastSyncedAt: now,
|
||||
createdAt: current?.createdAt ?? now,
|
||||
updatedAt: comparable === nextComparable ? (current?.updatedAt ?? now) : now,
|
||||
origin: "physical",
|
||||
status: "active",
|
||||
});
|
||||
if (!current) created += 1;
|
||||
else if (comparable !== nextComparable) updated += 1;
|
||||
@@ -1073,6 +1214,12 @@ export class MemoryCatalogRepository implements CatalogRepository {
|
||||
this.relationships.delete(relationship.id);
|
||||
}
|
||||
}
|
||||
for (const relationship of [...this.logicalRelationships.values()]) {
|
||||
const pair = relationship.columns[0];
|
||||
if (pair.sourceColumnId === columnId || pair.targetColumnId === columnId) {
|
||||
this.logicalRelationships.delete(relationship.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private markCatalogIncomplete(databaseIds: readonly string[]): void {
|
||||
|
||||
Reference in New Issue
Block a user