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.
165 lines
5.1 KiB
TypeScript
165 lines
5.1 KiB
TypeScript
import { http, HttpResponse } from "msw";
|
|
import { expect, test } from "vitest";
|
|
import { server } from "../test/msw";
|
|
import {
|
|
createCatalogRelationship,
|
|
deleteCatalogRelationship,
|
|
getCatalogMetrics,
|
|
rebuildGeneratedRelationships,
|
|
setCatalogRelationshipStatus,
|
|
type CatalogMetrics,
|
|
type CatalogRelationship,
|
|
} from "./catalog-databases";
|
|
|
|
const databaseId = "d4baf0f5-b9c3-4dc5-aad2-2f5676a36e64";
|
|
|
|
const globalMetrics: CatalogMetrics = {
|
|
scope: "global",
|
|
databaseId: null,
|
|
tables: 163,
|
|
columns: 2_275,
|
|
sensitiveColumns: 5,
|
|
relationships: 21,
|
|
descriptionTargets: 2_438,
|
|
describedTargets: 1_829,
|
|
descriptionCoverage: 75.02,
|
|
updatedAt: "2026-08-31T08:45:00.000Z",
|
|
};
|
|
|
|
test("loads global catalog metrics without a database filter", async () => {
|
|
let requestedUrl: URL | undefined;
|
|
server.use(http.get("/api/catalog/metrics", ({ request }) => {
|
|
requestedUrl = new URL(request.url);
|
|
return HttpResponse.json(globalMetrics);
|
|
}));
|
|
|
|
await expect(getCatalogMetrics()).resolves.toEqual(globalMetrics);
|
|
expect(requestedUrl?.pathname).toBe("/api/catalog/metrics");
|
|
expect(requestedUrl?.searchParams.has("databaseId")).toBe(false);
|
|
});
|
|
|
|
test("loads metrics scoped to the requested database", async () => {
|
|
const databaseMetrics: CatalogMetrics = {
|
|
...globalMetrics,
|
|
scope: "database",
|
|
databaseId,
|
|
tables: 42,
|
|
columns: 508,
|
|
sensitiveColumns: 3,
|
|
relationships: 8,
|
|
descriptionTargets: 550,
|
|
describedTargets: 413,
|
|
descriptionCoverage: 75.09,
|
|
};
|
|
let requestedDatabaseId: string | null = null;
|
|
server.use(http.get("/api/catalog/metrics", ({ request }) => {
|
|
requestedDatabaseId = new URL(request.url).searchParams.get("databaseId");
|
|
return HttpResponse.json(databaseMetrics);
|
|
}));
|
|
|
|
await expect(getCatalogMetrics(databaseId)).resolves.toEqual(databaseMetrics);
|
|
expect(requestedDatabaseId).toBe(databaseId);
|
|
});
|
|
|
|
test("propagates an unsuccessful catalog metrics response", async () => {
|
|
server.use(http.get("/api/catalog/metrics", () => new HttpResponse(null, { status: 503 })));
|
|
|
|
await expect(getCatalogMetrics()).rejects.toMatchObject({ status: 503 });
|
|
});
|
|
|
|
const logicalRelationship: CatalogRelationship = {
|
|
id: "11111111-1111-4111-8111-111111111111",
|
|
databaseId,
|
|
constraintName: null,
|
|
sourceTableId: "22222222-2222-4222-8222-222222222222",
|
|
sourceTableName: "orders",
|
|
targetTableId: "33333333-3333-4333-8333-333333333333",
|
|
targetTableName: "users",
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
deferrable: false,
|
|
initiallyDeferred: false,
|
|
columns: [{
|
|
position: 1,
|
|
sourceColumnId: "44444444-4444-4444-8444-444444444444",
|
|
sourceColumnName: "user_id",
|
|
targetColumnId: "55555555-5555-4555-8555-555555555555",
|
|
targetColumnName: "id",
|
|
}],
|
|
origin: "manual",
|
|
status: "active",
|
|
lastSyncedDatabaseVersion: null,
|
|
lastSyncedAt: null,
|
|
createdAt: "2026-08-31T12:00:00.000Z",
|
|
updatedAt: "2026-08-31T12:00:00.000Z",
|
|
};
|
|
|
|
test("creates a manual relationship from the selected source and target columns", async () => {
|
|
let requestBody: unknown;
|
|
server.use(http.post("/api/catalog/databases/:databaseId/relationships", async ({ request }) => {
|
|
requestBody = await request.json();
|
|
return HttpResponse.json(logicalRelationship, { status: 201 });
|
|
}));
|
|
|
|
await expect(createCatalogRelationship(
|
|
databaseId,
|
|
logicalRelationship.columns[0].sourceColumnId,
|
|
logicalRelationship.columns[0].targetColumnId,
|
|
)).resolves.toEqual(logicalRelationship);
|
|
expect(requestBody).toEqual({
|
|
sourceColumnId: logicalRelationship.columns[0].sourceColumnId,
|
|
targetColumnId: logicalRelationship.columns[0].targetColumnId,
|
|
});
|
|
});
|
|
|
|
test("rebuilds generated relationships for one database", async () => {
|
|
let calls = 0;
|
|
server.use(http.post(
|
|
"/api/catalog/databases/:databaseId/relationships/rebuild-generated",
|
|
() => {
|
|
calls += 1;
|
|
return HttpResponse.json({ added: 12, alreadyPresent: 7, excluded: 3, ambiguous: 2 });
|
|
},
|
|
));
|
|
|
|
await expect(rebuildGeneratedRelationships(databaseId)).resolves.toEqual({
|
|
added: 12,
|
|
alreadyPresent: 7,
|
|
excluded: 3,
|
|
ambiguous: 2,
|
|
});
|
|
expect(calls).toBe(1);
|
|
});
|
|
|
|
test("changes whether a logical relationship is active or excluded", async () => {
|
|
let requestBody: unknown;
|
|
server.use(http.patch(
|
|
"/api/catalog/databases/:databaseId/relationships/:relationshipId",
|
|
async ({ request }) => {
|
|
requestBody = await request.json();
|
|
return HttpResponse.json({ ...logicalRelationship, status: "excluded" });
|
|
},
|
|
));
|
|
|
|
await expect(setCatalogRelationshipStatus(
|
|
databaseId,
|
|
logicalRelationship.id,
|
|
"excluded",
|
|
)).resolves.toMatchObject({ status: "excluded" });
|
|
expect(requestBody).toEqual({ status: "excluded" });
|
|
});
|
|
|
|
test("permanently deletes a logical relationship", async () => {
|
|
let calls = 0;
|
|
server.use(http.delete(
|
|
"/api/catalog/databases/:databaseId/relationships/:relationshipId",
|
|
() => {
|
|
calls += 1;
|
|
return new HttpResponse(null, { status: 204 });
|
|
},
|
|
));
|
|
|
|
await expect(deleteCatalogRelationship(databaseId, logicalRelationship.id)).resolves.toBeUndefined();
|
|
expect(calls).toBe(1);
|
|
});
|