refactor(api): extract role router support

This commit is contained in:
2026-03-31 13:40:55 +02:00
parent b57f7e6d2e
commit 5e74d61902
3 changed files with 288 additions and 139 deletions
+36 -139
View File
@@ -1,4 +1,3 @@
import { countPlanningEntries } from "@capakraken/application";
import { CreateRoleSchema, PermissionKey, UpdateRoleSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
@@ -12,52 +11,16 @@ import {
protectedProcedure,
requirePermission,
} from "../trpc.js";
async function loadRolePlanningEntryCounts(
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
roleIds: string[],
) {
const { countsByRoleId } = await countPlanningEntries(db, {
roleIds,
});
return countsByRoleId;
}
async function attachPlanningEntryCounts<
TRole extends {
id: string;
_count: { resourceRoles: number };
},
>(
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
roles: TRole[],
): Promise<Array<TRole & { _count: { resourceRoles: number; allocations: number } }>> {
const countsByRoleId = await loadRolePlanningEntryCounts(
db,
roles.map((role) => role.id),
);
return roles.map((role) => ({
...role,
_count: {
...role._count,
allocations: countsByRoleId.get(role.id) ?? 0,
},
}));
}
async function attachSinglePlanningEntryCount<
TRole extends {
id: string;
_count: { resourceRoles: number };
},
>(
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
role: TRole,
): Promise<TRole & { _count: { resourceRoles: number; allocations: number } }> {
return (await attachPlanningEntryCounts(db, [role]))[0]!;
}
import {
appendZeroAllocationCount,
assertRoleNameAvailable,
attachRolePlanningEntryCounts,
attachSingleRolePlanningEntryCount,
buildRoleCreateData,
buildRoleListWhere,
buildRoleUpdateData,
findRoleByIdentifier,
} from "./role-support.js";
export const roleRouter = createTRPCRouter({
list: planningReadProcedure
@@ -69,12 +32,7 @@ export const roleRouter = createTRPCRouter({
)
.query(async ({ ctx, input }) => {
const roles = await ctx.db.role.findMany({
where: {
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
...(input.search
? { name: { contains: input.search, mode: "insensitive" as const } }
: {}),
},
where: buildRoleListWhere(input),
include: {
_count: {
select: { resourceRoles: true },
@@ -83,13 +41,12 @@ export const roleRouter = createTRPCRouter({
orderBy: { name: "asc" },
});
return attachPlanningEntryCounts(ctx.db, roles);
return attachRolePlanningEntryCounts(ctx.db, roles);
}),
resolveByIdentifier: protectedProcedure
.input(z.object({ identifier: z.string().trim().min(1) }))
.query(async ({ ctx, input }) => {
const identifier = input.identifier.trim();
const select = {
id: true,
name: true,
@@ -97,33 +54,12 @@ export const roleRouter = createTRPCRouter({
isActive: true,
} as const;
let role = await ctx.db.role.findUnique({
where: { id: identifier },
select,
});
if (!role) {
role = await ctx.db.role.findUnique({
where: { name: identifier },
select,
});
}
if (!role) {
role = await ctx.db.role.findFirst({
where: { name: { equals: identifier, mode: "insensitive" } },
select,
});
}
if (!role) {
role = await ctx.db.role.findFirst({
where: { name: { contains: identifier, mode: "insensitive" } },
select,
});
}
if (!role) {
throw new TRPCError({ code: "NOT_FOUND", message: "Role not found" });
}
return role;
return findRoleByIdentifier<{
id: string;
name: string;
color: string | null;
isActive: boolean;
}>(ctx.db, input.identifier, select);
}),
getByIdentifier: planningReadProcedure
@@ -138,33 +74,15 @@ export const roleRouter = createTRPCRouter({
_count: { select: { resourceRoles: true } },
} as const;
let role = await ctx.db.role.findUnique({
where: { id: input.identifier },
select,
});
if (!role) {
role = await ctx.db.role.findUnique({
where: { name: input.identifier },
select,
});
}
if (!role) {
role = await ctx.db.role.findFirst({
where: { name: { equals: input.identifier, mode: "insensitive" } },
select,
});
}
if (!role) {
role = await ctx.db.role.findFirst({
where: { name: { contains: input.identifier, mode: "insensitive" } },
select,
});
}
if (!role) {
throw new TRPCError({ code: "NOT_FOUND", message: "Role not found" });
}
return attachSinglePlanningEntryCount(ctx.db, role);
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
@@ -185,24 +103,17 @@ export const roleRouter = createTRPCRouter({
"Role",
);
return attachSinglePlanningEntryCount(ctx.db, role);
return attachSingleRolePlanningEntryCount(ctx.db, role);
}),
create: managerProcedure
.input(CreateRoleSchema)
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
const existing = await ctx.db.role.findUnique({ where: { name: input.name } });
if (existing) {
throw new TRPCError({ code: "CONFLICT", message: `Role "${input.name}" already exists` });
}
await assertRoleNameAvailable(ctx.db, input.name);
const role = await ctx.db.role.create({
data: {
name: input.name,
description: input.description ?? null,
color: input.color ?? null,
},
data: buildRoleCreateData(input),
include: { _count: { select: { resourceRoles: true } } },
});
@@ -217,13 +128,7 @@ export const roleRouter = createTRPCRouter({
emitRoleCreated({ id: role.id, name: role.name });
return {
...role,
_count: {
...role._count,
allocations: 0,
},
};
return appendZeroAllocationCount(role);
}),
update: managerProcedure
@@ -236,20 +141,12 @@ export const roleRouter = createTRPCRouter({
);
if (input.data.name && input.data.name !== existing.name) {
const nameConflict = await ctx.db.role.findUnique({ where: { name: input.data.name } });
if (nameConflict) {
throw new TRPCError({ code: "CONFLICT", message: `Role "${input.data.name}" already exists` });
}
await assertRoleNameAvailable(ctx.db, input.data.name, input.id);
}
const updated = await ctx.db.role.update({
where: { id: input.id },
data: {
...(input.data.name !== undefined ? { name: input.data.name } : {}),
...(input.data.description !== undefined ? { description: input.data.description } : {}),
...(input.data.color !== undefined ? { color: input.data.color } : {}),
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
},
data: buildRoleUpdateData(input.data),
include: { _count: { select: { resourceRoles: true } } },
});
@@ -264,7 +161,7 @@ export const roleRouter = createTRPCRouter({
emitRoleUpdated({ id: updated.id, name: updated.name });
return attachSinglePlanningEntryCount(ctx.db, updated);
return attachSingleRolePlanningEntryCount(ctx.db, updated);
}),
delete: managerProcedure
@@ -279,7 +176,7 @@ export const roleRouter = createTRPCRouter({
"Role",
);
const roleWithCounts = await attachSinglePlanningEntryCount(ctx.db, role);
const roleWithCounts = await attachSingleRolePlanningEntryCount(ctx.db, role);
if (
roleWithCounts._count.resourceRoles > 0 ||
@@ -328,6 +225,6 @@ export const roleRouter = createTRPCRouter({
emitRoleUpdated({ id: role.id, isActive: false });
return attachSinglePlanningEntryCount(ctx.db, role);
return attachSingleRolePlanningEntryCount(ctx.db, role);
}),
});