ubiquitous-invention/apps/web/server/routers/relations.ts

149 lines
4.2 KiB
TypeScript
Raw Normal View History

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