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); } }); }