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,154 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { isPrincipalContext, requirePermission } from "../auth/authorization.js";
|
||||
import {
|
||||
CatalogLogicalRelationshipService,
|
||||
LogicalRelationshipColumnNotFoundError,
|
||||
LogicalRelationshipDatabaseNotFoundError,
|
||||
LogicalRelationshipDuplicateError,
|
||||
LogicalRelationshipNotFoundError,
|
||||
LogicalRelationshipReadOnlyError,
|
||||
LogicalRelationshipSchemaStaleError,
|
||||
LogicalRelationshipTargetNotUniqueError,
|
||||
LogicalRelationshipTypeIncompatibleError,
|
||||
} from "../catalog/logical-relationship-service.js";
|
||||
import type { CatalogOperationCoordinator } from "../catalog/operation-coordinator.js";
|
||||
import { CatalogOperationInProgressError, CatalogUnavailableError } from "../catalog/types.js";
|
||||
|
||||
const idSchema = z.uuid();
|
||||
const createSchema = z.object({
|
||||
sourceColumnId: idSchema,
|
||||
targetColumnId: idSchema,
|
||||
}).strict();
|
||||
const statusSchema = z.object({ status: z.enum(["active", "excluded"]) }).strict();
|
||||
const emptySchema = z.object({}).strict();
|
||||
|
||||
function manage(request: FastifyRequest, reply: FastifyReply) {
|
||||
return isPrincipalContext(requirePermission(request, reply, "database.manage"));
|
||||
}
|
||||
|
||||
function safeError(reply: FastifyReply, error: unknown) {
|
||||
if (error instanceof CatalogUnavailableError) {
|
||||
return reply.code(503).send({ code: "catalog_unavailable", message: "Database catalog is unavailable." });
|
||||
}
|
||||
if (error instanceof CatalogOperationInProgressError) {
|
||||
return reply.code(409).send({
|
||||
code: "database_operation_in_progress",
|
||||
message: "A database operation is already in progress.",
|
||||
});
|
||||
}
|
||||
if (error instanceof LogicalRelationshipDatabaseNotFoundError) {
|
||||
return reply.code(404).send({ code: "database_not_found", message: "Database configuration was not found." });
|
||||
}
|
||||
if (error instanceof LogicalRelationshipColumnNotFoundError) {
|
||||
return reply.code(404).send({
|
||||
code: "column_not_found",
|
||||
message: "Catalog column was not found.",
|
||||
field: error.field,
|
||||
});
|
||||
}
|
||||
if (error instanceof LogicalRelationshipNotFoundError) {
|
||||
return reply.code(404).send({ code: "relationship_not_found", message: "Relationship was not found." });
|
||||
}
|
||||
if (error instanceof LogicalRelationshipDuplicateError) {
|
||||
return reply.code(409).send({ code: "relationship_duplicate", message: "Relationship already exists." });
|
||||
}
|
||||
if (error instanceof LogicalRelationshipReadOnlyError) {
|
||||
return reply.code(409).send({ code: "relationship_read_only", message: "Physical relationships are read-only." });
|
||||
}
|
||||
if (error instanceof LogicalRelationshipSchemaStaleError) {
|
||||
return reply.code(409).send({
|
||||
code: "relationship_schema_stale",
|
||||
message: "Synchronize the current database schema before managing logical relationships.",
|
||||
});
|
||||
}
|
||||
if (error instanceof LogicalRelationshipTargetNotUniqueError) {
|
||||
return reply.code(422).send({
|
||||
code: "relationship_target_not_unique",
|
||||
message: "Target column must be the only primary-key column of its table.",
|
||||
field: "targetColumnId",
|
||||
});
|
||||
}
|
||||
if (error instanceof LogicalRelationshipTypeIncompatibleError) {
|
||||
return reply.code(422).send({
|
||||
code: "relationship_type_incompatible",
|
||||
message: "Source and target column types are incompatible.",
|
||||
field: "targetColumnId",
|
||||
});
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return reply.code(400).send({
|
||||
code: "relationship_request_invalid",
|
||||
message: "Relationship request is invalid.",
|
||||
});
|
||||
}
|
||||
return reply.code(500).send({
|
||||
code: "relationship_operation_failed",
|
||||
message: "Relationship operation failed.",
|
||||
});
|
||||
}
|
||||
|
||||
export function catalogLogicalRelationshipRoutes(
|
||||
app: FastifyInstance,
|
||||
deps: {
|
||||
service: CatalogLogicalRelationshipService;
|
||||
operations: CatalogOperationCoordinator;
|
||||
},
|
||||
): void {
|
||||
app.get("/catalog/databases/:databaseId/relationships", async (request, reply) => {
|
||||
if (!manage(request, reply)) return reply;
|
||||
try {
|
||||
const databaseId = idSchema.parse((request.params as { databaseId?: unknown }).databaseId);
|
||||
return await deps.service.list(databaseId);
|
||||
} catch (error) { return safeError(reply, error); }
|
||||
});
|
||||
|
||||
app.post("/catalog/databases/:databaseId/relationships", async (request, reply) => {
|
||||
if (!manage(request, reply)) return reply;
|
||||
try {
|
||||
const databaseId = idSchema.parse((request.params as { databaseId?: unknown }).databaseId);
|
||||
const input = createSchema.parse(request.body);
|
||||
const relationship = await deps.operations.run(databaseId, async () => (
|
||||
await deps.service.addManual(databaseId, input.sourceColumnId, input.targetColumnId)
|
||||
));
|
||||
return reply.code(201).send(relationship);
|
||||
} catch (error) { return safeError(reply, error); }
|
||||
});
|
||||
|
||||
app.post("/catalog/databases/:databaseId/relationships/rebuild-generated", async (request, reply) => {
|
||||
if (!manage(request, reply)) return reply;
|
||||
try {
|
||||
const databaseId = idSchema.parse((request.params as { databaseId?: unknown }).databaseId);
|
||||
emptySchema.parse(request.body ?? {});
|
||||
return await deps.operations.run(databaseId, async () => (
|
||||
await deps.service.rebuildGenerated(databaseId)
|
||||
));
|
||||
} catch (error) { return safeError(reply, error); }
|
||||
});
|
||||
|
||||
app.patch("/catalog/databases/:databaseId/relationships/:relationshipId", async (request, reply) => {
|
||||
if (!manage(request, reply)) return reply;
|
||||
try {
|
||||
const params = request.params as { databaseId?: unknown; relationshipId?: unknown };
|
||||
const databaseId = idSchema.parse(params.databaseId);
|
||||
const relationshipId = idSchema.parse(params.relationshipId);
|
||||
const input = statusSchema.parse(request.body);
|
||||
return await deps.operations.run(databaseId, async () => (
|
||||
await deps.service.setStatus(databaseId, relationshipId, input.status)
|
||||
));
|
||||
} catch (error) { return safeError(reply, error); }
|
||||
});
|
||||
|
||||
app.delete("/catalog/databases/:databaseId/relationships/:relationshipId", async (request, reply) => {
|
||||
if (!manage(request, reply)) return reply;
|
||||
try {
|
||||
const params = request.params as { databaseId?: unknown; relationshipId?: unknown };
|
||||
const databaseId = idSchema.parse(params.databaseId);
|
||||
const relationshipId = idSchema.parse(params.relationshipId);
|
||||
await deps.operations.run(databaseId, async () => {
|
||||
await deps.service.deletePermanently(databaseId, relationshipId);
|
||||
});
|
||||
return reply.code(204).send();
|
||||
} catch (error) { return safeError(reply, error); }
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user