import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { eq } from "drizzle-orm"; import { objectRelations, objects } from "@tasks/database/schema"; import { router, protectedProcedure } from "@/server/trpc"; export const relationsRouter = router({ list: protectedProcedure .input( z.object({ objectId: z.string().uuid(), direction: z.enum(["outgoing", "incoming", "both"]).optional(), }), ) .query(async ({ ctx, input }) => { const dir = input.direction ?? "both"; const baseSelect = { id: objectRelations.id, relationType: objectRelations.relationType, sourceId: objectRelations.sourceId, targetId: objectRelations.targetId, createdAt: objectRelations.createdAt, relatedId: objects.id, relatedTitle: objects.title, relatedType: objects.type, }; const outgoing = dir === "incoming" ? [] : await ctx.db .select(baseSelect) .from(objectRelations) .innerJoin(objects, eq(objectRelations.targetId, objects.id)) .where(eq(objectRelations.sourceId, input.objectId)); const incoming = dir === "outgoing" ? [] : await ctx.db .select(baseSelect) .from(objectRelations) .innerJoin(objects, eq(objectRelations.sourceId, objects.id)) .where(eq(objectRelations.targetId, input.objectId)); const relations = [ ...outgoing.map((r) => ({ id: r.id, relationType: r.relationType, sourceId: r.sourceId, targetId: r.targetId, createdAt: r.createdAt, direction: "outgoing" as const, relatedObject: { id: r.relatedId, title: r.relatedTitle, type: r.relatedType, }, })), ...incoming.map((r) => ({ id: r.id, relationType: r.relationType, sourceId: r.sourceId, targetId: r.targetId, createdAt: r.createdAt, direction: "incoming" as const, relatedObject: { id: r.relatedId, title: r.relatedTitle, type: r.relatedType, }, })), ]; return { relations }; }), create: protectedProcedure .input( z.object({ sourceId: z.string().uuid(), targetId: z.string().uuid(), relationType: z.string().min(1).max(50), }), ) .mutation(async ({ ctx, input }) => { if (input.sourceId === input.targetId) { throw new TRPCError({ code: "BAD_REQUEST", message: "Cannot relate an object to itself", }); } try { const [created] = await ctx.db .insert(objectRelations) .values({ sourceId: input.sourceId, targetId: input.targetId, relationType: input.relationType, }) .returning(); if (!created) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create relation", }); } return created; } catch (cause) { const msg = cause instanceof Error ? cause.message : String(cause); if ( msg.includes("unique") || msg.includes("duplicate") || msg.includes("object_relations_source_target_type_unique") ) { throw new TRPCError({ code: "CONFLICT", message: "This relation already exists", cause, }); } throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create relation", cause, }); } }), delete: protectedProcedure .input(z.object({ id: z.string().uuid() })) .mutation(async ({ ctx, input }) => { const deleted = await ctx.db .delete(objectRelations) .where(eq(objectRelations.id, input.id)) .returning({ id: objectRelations.id }); if (deleted.length === 0) { throw new TRPCError({ code: "NOT_FOUND", message: "Relation not found" }); } return { ok: true as const, id: deleted[0]!.id }; }), });