feat: add metadata catalog cleanup commands
This commit is contained in:
@@ -51,6 +51,41 @@ function snapshot(): ObservedSchemaSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
function expandedSnapshot(): ObservedSchemaSnapshot {
|
||||
const expanded = snapshot();
|
||||
expanded.tables.push(
|
||||
{ name: "encounters", sourceComment: null },
|
||||
{ name: "departments", sourceComment: null },
|
||||
);
|
||||
expanded.columns.push(
|
||||
{ tableName: "encounters", name: "visit_id", ordinalPosition: 1, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: null, sourceComment: null },
|
||||
{ tableName: "departments", name: "patient_id", ordinalPosition: 1, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: null, sourceComment: null },
|
||||
);
|
||||
expanded.relationships.push(
|
||||
{
|
||||
constraintName: "encounters_visit_id_fkey",
|
||||
sourceTableName: "encounters",
|
||||
targetTableName: "visits",
|
||||
updateRule: "NO ACTION",
|
||||
deleteRule: "CASCADE",
|
||||
deferrable: false,
|
||||
initiallyDeferred: false,
|
||||
columns: [{ position: 1, sourceColumnName: "visit_id", targetColumnName: "id" }],
|
||||
},
|
||||
{
|
||||
constraintName: "departments_patient_id_fkey",
|
||||
sourceTableName: "departments",
|
||||
targetTableName: "patients",
|
||||
updateRule: "NO ACTION",
|
||||
deleteRule: "CASCADE",
|
||||
deferrable: false,
|
||||
initiallyDeferred: false,
|
||||
columns: [{ position: 1, sourceColumnName: "patient_id", targetColumnName: "id" }],
|
||||
},
|
||||
);
|
||||
return expanded;
|
||||
}
|
||||
|
||||
async function waitFor(repository: MemoryCatalogRepository, runId: string, state: CatalogSyncRun["state"]): Promise<CatalogSyncRun> {
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
const run = await repository.getSyncRun(runId);
|
||||
@@ -100,6 +135,14 @@ async function setup() {
|
||||
};
|
||||
}
|
||||
|
||||
async function seedCatalog(
|
||||
repository: MemoryCatalogRepository,
|
||||
database: { id: string; version: number },
|
||||
observed: ObservedSchemaSnapshot = snapshot(),
|
||||
): Promise<void> {
|
||||
await repository.applySchemaSync(database.id, database.version, "all", [], observed);
|
||||
}
|
||||
|
||||
test("synchronizes a full physical schema and derives primary and foreign key flags", async () => {
|
||||
const { app, repository, database } = await setup();
|
||||
const started = await app.inject({
|
||||
@@ -222,3 +265,110 @@ test("waits for confirmation and rescans before applying destructive changes", a
|
||||
expect((await repository.listTables(database.id)).map((table) => table.name)).toEqual(["patients"]);
|
||||
expect(scan).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("deletes every catalog table for multiple selected databases and cascades dependent metadata", async () => {
|
||||
const { app, repository, database } = await setup();
|
||||
const second = await repository.create({
|
||||
workspaceId: "radiology", engine: "postgres", databaseName: "radiology", schema: "public",
|
||||
binding: { transport: "postgres_direct", host: "db.internal", port: 5432, username: "reader" },
|
||||
});
|
||||
await seedCatalog(repository, database);
|
||||
await seedCatalog(repository, second);
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/catalog/databases/metadata-cleanup",
|
||||
payload: { databaseIds: [database.id, second.id], target: "tables" },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ tables: 4, columns: 6, relationships: 2 });
|
||||
expect(await repository.get(database.id)).toBeDefined();
|
||||
expect(await repository.get(second.id)).toBeDefined();
|
||||
expect(await repository.listTables(database.id)).toEqual([]);
|
||||
expect(await repository.listTables(second.id)).toEqual([]);
|
||||
expect(await repository.listRelationships(database.id)).toEqual([]);
|
||||
expect(await repository.listRelationships(second.id)).toEqual([]);
|
||||
});
|
||||
|
||||
test("deletes relationships for selected databases without deleting their tables or columns", async () => {
|
||||
const { app, repository, database } = await setup();
|
||||
await seedCatalog(repository, database);
|
||||
const tables = await repository.listTables(database.id);
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/catalog/databases/metadata-cleanup",
|
||||
payload: { databaseIds: [database.id], target: "relationships" },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ tables: 0, columns: 0, relationships: 1 });
|
||||
expect(await repository.listTables(database.id)).toHaveLength(2);
|
||||
expect(await repository.listColumns(database.id, tables[0]!.id)).not.toEqual([]);
|
||||
expect(await repository.listRelationships(database.id)).toEqual([]);
|
||||
expect((await repository.get(database.id))?.schemaSyncedVersion).toBeUndefined();
|
||||
});
|
||||
|
||||
test("deletes columns from multiple selected catalog tables only", async () => {
|
||||
const { app, repository, database } = await setup();
|
||||
await seedCatalog(repository, database, expandedSnapshot());
|
||||
const tables = await repository.listTables(database.id);
|
||||
const patients = tables.find((table) => table.name === "patients")!;
|
||||
const visits = tables.find((table) => table.name === "visits")!;
|
||||
const encounters = tables.find((table) => table.name === "encounters")!;
|
||||
const departments = tables.find((table) => table.name === "departments")!;
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/tables/metadata-cleanup`,
|
||||
payload: { tableIds: [patients.id, visits.id], target: "columns" },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ tables: 0, columns: 3, relationships: 0 });
|
||||
expect(await repository.listColumns(database.id, patients.id)).toEqual([]);
|
||||
expect(await repository.listColumns(database.id, visits.id)).toEqual([]);
|
||||
expect((await repository.listColumns(database.id, encounters.id)).map((column) => column.name))
|
||||
.toEqual(["visit_id"]);
|
||||
expect((await repository.listColumns(database.id, departments.id)).map((column) => column.name))
|
||||
.toEqual(["patient_id"]);
|
||||
expect((await repository.get(database.id))?.schemaSyncedVersion).toBeUndefined();
|
||||
});
|
||||
|
||||
test("deletes incoming and outgoing relationships for multiple selected catalog tables only", async () => {
|
||||
const { app, repository, database } = await setup();
|
||||
await seedCatalog(repository, database, expandedSnapshot());
|
||||
const tables = await repository.listTables(database.id);
|
||||
const visits = tables.find((table) => table.name === "visits")!;
|
||||
const encounters = tables.find((table) => table.name === "encounters")!;
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/tables/metadata-cleanup`,
|
||||
payload: { tableIds: [visits.id, encounters.id], target: "relationships" },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ tables: 0, columns: 0, relationships: 2 });
|
||||
expect((await repository.listRelationships(database.id)).map((relationship) => relationship.constraintName))
|
||||
.toEqual(["departments_patient_id_fkey"]);
|
||||
expect(await repository.listTables(database.id)).toHaveLength(4);
|
||||
});
|
||||
|
||||
test("rejects a missing database without partially cleaning valid selections", async () => {
|
||||
const { app, repository, database } = await setup();
|
||||
await seedCatalog(repository, database);
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/catalog/databases/metadata-cleanup",
|
||||
payload: {
|
||||
databaseIds: [database.id, "99999999-9999-4999-8999-999999999999"],
|
||||
target: "tables",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(await repository.listTables(database.id)).toHaveLength(2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user