import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { and, asc, eq } from "drizzle-orm"; import { objects, propertyDefinitions, propertyValues, } from "@tasks/database/schema"; import { router, workspaceProcedure } from "@/server/trpc"; export const propertiesRouter = router({ listDefinitions: workspaceProcedure.query(async ({ ctx }) => { const definitions = await ctx.db .select() .from(propertyDefinitions) .where(eq(propertyDefinitions.workspaceId, ctx.workspace.id)) .orderBy(asc(propertyDefinitions.sortOrder), asc(propertyDefinitions.id)); return { definitions }; }), createDefinition: workspaceProcedure .input( z.object({ name: z.string().min(1).max(255), fieldType: z.string().min(1).max(50), config: z.any().optional(), }), ) .mutation(async ({ ctx, input }) => { const [created] = await ctx.db .insert(propertyDefinitions) .values({ workspaceId: ctx.workspace.id, name: input.name, fieldType: input.fieldType, config: input.config ?? null, }) .returning(); if (!created) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to create property definition", }); } return created; }), getValues: workspaceProcedure .input(z.object({ objectId: z.string().uuid() })) .query(async ({ ctx, input }) => { // Confirm the target object lives in this workspace. const [obj] = await ctx.db .select({ id: objects.id }) .from(objects) .where(and(eq(objects.id, input.objectId), eq(objects.workspaceId, ctx.workspace.id))) .limit(1); if (!obj) { throw new TRPCError({ code: "NOT_FOUND", message: "Object not found" }); } const rows = await ctx.db .select({ valueRow: propertyValues, definition: propertyDefinitions, }) .from(propertyValues) .innerJoin( propertyDefinitions, eq(propertyValues.propertyDefId, propertyDefinitions.id), ) .where(eq(propertyValues.objectId, input.objectId)) .orderBy(asc(propertyDefinitions.sortOrder), asc(propertyDefinitions.id)); return { values: rows.map((r) => ({ ...r.valueRow, definition: r.definition, })), }; }), setValue: workspaceProcedure .input( z.object({ objectId: z.string().uuid(), propertyDefId: z.string().uuid(), value: z.any(), }), ) .mutation(async ({ ctx, input }) => { // Verify both the target object and the property definition belong to // the resolved workspace before writing. const [obj] = await ctx.db .select({ id: objects.id }) .from(objects) .where(and(eq(objects.id, input.objectId), eq(objects.workspaceId, ctx.workspace.id))) .limit(1); if (!obj) { throw new TRPCError({ code: "NOT_FOUND", message: "Object not found" }); } const [def] = await ctx.db .select({ id: propertyDefinitions.id }) .from(propertyDefinitions) .where( and( eq(propertyDefinitions.id, input.propertyDefId), eq(propertyDefinitions.workspaceId, ctx.workspace.id), ), ) .limit(1); if (!def) { throw new TRPCError({ code: "NOT_FOUND", message: "Property definition not found" }); } const now = new Date(); const [row] = await ctx.db .insert(propertyValues) .values({ objectId: input.objectId, propertyDefId: input.propertyDefId, value: input.value, updatedAt: now, }) .onConflictDoUpdate({ target: [propertyValues.objectId, propertyValues.propertyDefId], set: { value: input.value, updatedAt: now, }, }) .returning(); if (!row) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to set property value", }); } return row; }), });