import { CreateRoleSchema, PermissionKey, UpdateRoleSchema } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { emitRoleCreated, emitRoleDeleted, emitRoleUpdated } from "../sse/event-bus.js"; import type { TRPCContext } from "../trpc.js"; import { requirePermission } from "../trpc.js"; import { appendZeroAllocationCount, assertRoleNameAvailable, attachSingleRolePlanningEntryCount, buildRoleCreateData, buildRoleUpdateData, } from "./role-support.js"; export const RoleIdInputSchema = z.object({ id: z.string(), }); export const UpdateRoleProcedureInputSchema = z.object({ id: z.string(), data: UpdateRoleSchema, }); type RoleMutationContext = Pick & { permissions: Set; }; export async function createRole( ctx: RoleMutationContext, input: z.infer, ) { 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); } export async function updateRole( ctx: RoleMutationContext, input: z.infer, ) { 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); } export async function deleteRole( ctx: RoleMutationContext, input: z.infer, ) { 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 }; } export async function deactivateRole( ctx: RoleMutationContext, input: z.infer, ) { 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); }