import { CreateRoleSchema, PermissionKey, UpdateRoleSchema } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { RESOURCE_BRIEF_SELECT } from "../db/selects.js"; import { emitRoleCreated, emitRoleDeleted, emitRoleUpdated } from "../sse/event-bus.js"; import { createTRPCRouter, managerProcedure, planningReadProcedure, protectedProcedure, requirePermission, } from "../trpc.js"; import { appendZeroAllocationCount, assertRoleNameAvailable, attachRolePlanningEntryCounts, attachSingleRolePlanningEntryCount, buildRoleCreateData, buildRoleListWhere, buildRoleUpdateData, findRoleByIdentifier, } from "./role-support.js"; export const roleRouter = createTRPCRouter({ list: planningReadProcedure .input( z.object({ isActive: z.boolean().optional(), search: z.string().optional(), }), ) .query(async ({ ctx, input }) => { const roles = await ctx.db.role.findMany({ where: buildRoleListWhere(input), include: { _count: { select: { resourceRoles: true }, }, }, orderBy: { name: "asc" }, }); return attachRolePlanningEntryCounts(ctx.db, roles); }), resolveByIdentifier: protectedProcedure .input(z.object({ identifier: z.string().trim().min(1) })) .query(async ({ ctx, input }) => { const select = { id: true, name: true, color: true, isActive: true, } as const; return findRoleByIdentifier<{ id: string; name: string; color: string | null; isActive: boolean; }>(ctx.db, input.identifier, select); }), getByIdentifier: planningReadProcedure .input(z.object({ identifier: z.string() })) .query(async ({ ctx, input }) => { const select = { id: true, name: true, description: true, color: true, isActive: true, _count: { select: { resourceRoles: true } }, } as const; const role = await findRoleByIdentifier<{ id: string; name: string; description: string | null; color: string | null; isActive: boolean; _count: { resourceRoles: number }; }>(ctx.db, input.identifier, select); return attachSingleRolePlanningEntryCount(ctx.db, role); }), getById: planningReadProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const role = await findUniqueOrThrow( ctx.db.role.findUnique({ where: { id: input.id }, include: { _count: { select: { resourceRoles: true } }, resourceRoles: { include: { resource: { select: RESOURCE_BRIEF_SELECT }, }, }, }, }), "Role", ); return attachSingleRolePlanningEntryCount(ctx.db, role); }), create: managerProcedure .input(CreateRoleSchema) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ROLES); await assertRoleNameAvailable(ctx.db, input.name); const role = await ctx.db.role.create({ data: buildRoleCreateData(input), include: { _count: { select: { resourceRoles: true } } }, }); await ctx.db.auditLog.create({ data: { entityType: "Role", entityId: role.id, action: "CREATE", changes: { after: role }, }, }); emitRoleCreated({ id: role.id, name: role.name }); return appendZeroAllocationCount(role); }), update: managerProcedure .input(z.object({ id: z.string(), data: UpdateRoleSchema })) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ROLES); const existing = await findUniqueOrThrow( ctx.db.role.findUnique({ where: { id: input.id } }), "Role", ); if (input.data.name && input.data.name !== existing.name) { await assertRoleNameAvailable(ctx.db, input.data.name, input.id); } const updated = await ctx.db.role.update({ where: { id: input.id }, data: buildRoleUpdateData(input.data), include: { _count: { select: { resourceRoles: true } } }, }); await ctx.db.auditLog.create({ data: { entityType: "Role", entityId: input.id, action: "UPDATE", changes: { before: existing, after: updated }, }, }); emitRoleUpdated({ id: updated.id, name: updated.name }); return attachSingleRolePlanningEntryCount(ctx.db, updated); }), delete: managerProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ROLES); const role = await findUniqueOrThrow( ctx.db.role.findUnique({ where: { id: input.id }, include: { _count: { select: { resourceRoles: true } } }, }), "Role", ); const roleWithCounts = await attachSingleRolePlanningEntryCount(ctx.db, role); if ( roleWithCounts._count.resourceRoles > 0 || roleWithCounts._count.allocations > 0 ) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: `Cannot delete role assigned to ${roleWithCounts._count.resourceRoles} resource(s) and ${roleWithCounts._count.allocations} allocation(s). Deactivate it instead.`, }); } await ctx.db.role.delete({ where: { id: input.id } }); await ctx.db.auditLog.create({ data: { entityType: "Role", entityId: input.id, action: "DELETE", changes: { before: role }, }, }); emitRoleDeleted(input.id); return { success: true }; }), deactivate: managerProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ROLES); const role = await ctx.db.role.update({ where: { id: input.id }, data: { isActive: false }, include: { _count: { select: { resourceRoles: true } } }, }); await ctx.db.auditLog.create({ data: { entityType: "Role", entityId: input.id, action: "UPDATE", changes: { after: { isActive: false } }, }, }); emitRoleUpdated({ id: role.id, isActive: false }); return attachSingleRolePlanningEntryCount(ctx.db, role); }), });