refactor(api): extract project lifecycle procedures

This commit is contained in:
2026-03-31 09:03:00 +02:00
parent 3a15f72f70
commit a23ef2c8b5
2 changed files with 161 additions and 125 deletions
+6 -125
View File
@@ -11,8 +11,9 @@ import { buildDynamicFieldWhereClauses } from "./custom-field-filters.js";
import { projectCostReadProcedures } from "./project-cost-read.js";
import { projectCoverProcedures } from "./project-cover.js";
import { projectIdentifierReadProcedures } from "./project-identifier-read.js";
import { createProjectLifecycleProcedures } from "./project-lifecycle.js";
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
import { adminProcedure, controllerProcedure, createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js";
import { controllerProcedure, createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js";
import { invalidateDashboardCache } from "../lib/cache.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
@@ -57,6 +58,10 @@ export const projectRouter = createTRPCRouter({
...projectCostReadProcedures,
...projectCoverProcedures,
...projectIdentifierReadProcedures,
...createProjectLifecycleProcedures({
invalidateDashboardCacheInBackground,
dispatchProjectWebhookInBackground,
}),
list: controllerProcedure
.input(
@@ -338,128 +343,4 @@ export const projectRouter = createTRPCRouter({
return updated;
}),
updateStatus: managerProcedure
.input(z.object({ id: z.string(), status: z.nativeEnum(ProjectStatus) }))
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
const result = await ctx.db.project.update({
where: { id: input.id },
data: { status: input.status },
});
invalidateDashboardCacheInBackground();
dispatchProjectWebhookInBackground(ctx.db, "project.status_changed", {
id: result.id,
shortCode: result.shortCode,
name: result.name,
status: result.status,
});
return result;
}),
batchUpdateStatus: managerProcedure
.input(
z.object({
ids: z.array(z.string()).min(1).max(100),
status: z.nativeEnum(ProjectStatus),
}),
)
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
const updated = await ctx.db.$transaction(
input.ids.map((id) =>
ctx.db.project.update({ where: { id }, data: { status: input.status } }),
),
);
await ctx.db.auditLog.create({
data: {
entityType: "Project",
entityId: input.ids.join(","),
action: "UPDATE",
changes: { after: { status: input.status, ids: input.ids } },
},
});
invalidateDashboardCacheInBackground();
return { count: updated.length };
}),
delete: adminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const project = await ctx.db.project.findUnique({
where: { id: input.id },
select: { id: true, name: true, shortCode: true },
});
if (!project) throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
// Delete all related records in a transaction
await ctx.db.$transaction(async (tx) => {
// Delete assignments (which reference demandRequirements)
await tx.assignment.deleteMany({ where: { projectId: input.id } });
// Delete demand requirements
await tx.demandRequirement.deleteMany({ where: { projectId: input.id } });
// Unlink calculation rules
await tx.calculationRule.updateMany({
where: { projectId: input.id },
data: { projectId: null },
});
// Delete the project
await tx.project.delete({ where: { id: input.id } });
// Audit log
await tx.auditLog.create({
data: {
entityType: "Project",
entityId: input.id,
action: "DELETE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: { before: { id: project.id, name: project.name, shortCode: project.shortCode } } as never,
},
});
});
invalidateDashboardCacheInBackground();
return { id: input.id, name: project.name };
}),
batchDelete: adminProcedure
.input(
z.object({
ids: z.array(z.string()).min(1).max(50),
}),
)
.mutation(async ({ ctx, input }) => {
const projects = await ctx.db.project.findMany({
where: { id: { in: input.ids } },
select: { id: true, name: true, shortCode: true },
});
if (projects.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "No projects found" });
}
await ctx.db.$transaction(async (tx) => {
const ids = projects.map((p) => p.id);
await tx.assignment.deleteMany({ where: { projectId: { in: ids } } });
await tx.demandRequirement.deleteMany({ where: { projectId: { in: ids } } });
await tx.calculationRule.updateMany({
where: { projectId: { in: ids } },
data: { projectId: null },
});
await tx.project.deleteMany({ where: { id: { in: ids } } });
await tx.auditLog.create({
data: {
entityType: "Project",
entityId: ids.join(","),
action: "DELETE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: { before: projects } as never,
},
});
});
invalidateDashboardCacheInBackground();
return { count: projects.length };
}),
});