import { BlueprintTarget, CreateBlueprintSchema, UpdateBlueprintSchema, type BlueprintFieldDefinition } from "@planarchy/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc.js"; import { createAuditEntry } from "../lib/audit.js"; export const blueprintRouter = createTRPCRouter({ list: protectedProcedure .input( z.object({ target: z.nativeEnum(BlueprintTarget).optional(), isActive: z.boolean().optional().default(true), }), ) .query(async ({ ctx, input }) => { return ctx.db.blueprint.findMany({ where: { ...(input.target ? { target: input.target } : {}), isActive: input.isActive, }, orderBy: { name: "asc" }, }); }), getById: protectedProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const blueprint = await findUniqueOrThrow( ctx.db.blueprint.findUnique({ where: { id: input.id } }), "Blueprint", ); return blueprint; }), create: adminProcedure .input(CreateBlueprintSchema) .mutation(async ({ ctx, input }) => { const blueprint = await ctx.db.blueprint.create({ data: { name: input.name, target: input.target, description: input.description, fieldDefs: input.fieldDefs as unknown as import("@planarchy/db").Prisma.InputJsonValue, defaults: input.defaults as unknown as import("@planarchy/db").Prisma.InputJsonValue, validationRules: input.validationRules as unknown as import("@planarchy/db").Prisma.InputJsonValue, } as unknown as Parameters[0]["data"], }); void createAuditEntry({ db: ctx.db, entityType: "Blueprint", entityId: blueprint.id, entityName: blueprint.name, action: "CREATE", userId: ctx.dbUser?.id, after: { name: input.name, target: input.target, description: input.description }, source: "ui", }); return blueprint; }), update: adminProcedure .input(z.object({ id: z.string(), data: UpdateBlueprintSchema })) .mutation(async ({ ctx, input }) => { const before = await findUniqueOrThrow( ctx.db.blueprint.findUnique({ where: { id: input.id } }), "Blueprint", ); const updated = await ctx.db.blueprint.update({ where: { id: input.id }, data: { ...(input.data.name !== undefined ? { name: input.data.name } : {}), ...(input.data.description !== undefined ? { description: input.data.description } : {}), ...(input.data.fieldDefs !== undefined ? { fieldDefs: input.data.fieldDefs as unknown as import("@planarchy/db").Prisma.InputJsonValue } : {}), ...(input.data.defaults !== undefined ? { defaults: input.data.defaults as unknown as import("@planarchy/db").Prisma.InputJsonValue } : {}), ...(input.data.validationRules !== undefined ? { validationRules: input.data.validationRules as unknown as import("@planarchy/db").Prisma.InputJsonValue } : {}), } as unknown as Parameters[0]["data"], }); void createAuditEntry({ db: ctx.db, entityType: "Blueprint", entityId: input.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, before: before as unknown as Record, after: updated as unknown as Record, source: "ui", }); return updated; }), /** Dedicated mutation for saving role presets — separate from field defs to avoid Zod depth issues */ updateRolePresets: adminProcedure .input(z.object({ id: z.string(), rolePresets: z.array(z.unknown()) })) .mutation(async ({ ctx, input }) => { const before = await findUniqueOrThrow( ctx.db.blueprint.findUnique({ where: { id: input.id } }), "Blueprint", ); const updated = await ctx.db.blueprint.update({ where: { id: input.id }, data: { rolePresets: input.rolePresets as unknown as import("@planarchy/db").Prisma.InputJsonValue }, }); void createAuditEntry({ db: ctx.db, entityType: "Blueprint", entityId: input.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, before: { rolePresets: before.rolePresets }, after: { rolePresets: input.rolePresets }, source: "ui", summary: "Updated role presets", }); return updated; }), delete: adminProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { // Soft delete — mark as inactive const deleted = await ctx.db.blueprint.update({ where: { id: input.id }, data: { isActive: false }, }); void createAuditEntry({ db: ctx.db, entityType: "Blueprint", entityId: input.id, entityName: deleted.name, action: "DELETE", userId: ctx.dbUser?.id, source: "ui", }); return deleted; }), batchDelete: adminProcedure .input(z.object({ ids: z.array(z.string()).min(1).max(100) })) .mutation(async ({ ctx, input }) => { // Soft delete const updated = await ctx.db.$transaction( input.ids.map((id) => ctx.db.blueprint.update({ where: { id }, data: { isActive: false } }), ), ); for (const bp of updated) { void createAuditEntry({ db: ctx.db, entityType: "Blueprint", entityId: bp.id, entityName: bp.name, action: "DELETE", userId: ctx.dbUser?.id, source: "ui", }); } return { count: updated.length }; }), getGlobalFieldDefs: protectedProcedure .input(z.object({ target: z.nativeEnum(BlueprintTarget) })) .query(async ({ ctx, input }) => { const blueprints = await ctx.db.blueprint.findMany({ where: { target: input.target, isGlobal: true, isActive: true }, select: { id: true, name: true, fieldDefs: true }, }); return blueprints.flatMap((b) => (b.fieldDefs as unknown as BlueprintFieldDefinition[]).map((f) => ({ ...f, blueprintId: b.id, blueprintName: b.name, })), ); }), setGlobal: adminProcedure .input(z.object({ id: z.string(), isGlobal: z.boolean() })) .mutation(async ({ ctx, input }) => { const updated = await ctx.db.blueprint.update({ where: { id: input.id }, data: { isGlobal: input.isGlobal }, }); void createAuditEntry({ db: ctx.db, entityType: "Blueprint", entityId: input.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, after: { isGlobal: input.isGlobal }, source: "ui", summary: input.isGlobal ? "Set blueprint as global" : "Removed global flag from blueprint", }); return updated; }), });