refactor(api): extract project procedures

This commit is contained in:
2026-03-31 21:28:56 +02:00
parent b1799e4f54
commit e34c22f3b0
4 changed files with 334 additions and 93 deletions
@@ -0,0 +1,115 @@
import { countPlanningEntries } from "@capakraken/application";
import { FieldType, ProjectStatus } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { paginate, PaginationInputSchema } from "../db/pagination.js";
import type { TRPCContext } from "../trpc.js";
import { buildDynamicFieldWhereClauses } from "./custom-field-filters.js";
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
import { getProjectShoringRatio } from "./project-shoring-ratio.js";
export const ProjectListInputSchema = PaginationInputSchema.extend({
status: z.nativeEnum(ProjectStatus).optional(),
search: z.string().optional(),
customFieldFilters: z.array(z.object({
key: z.string(),
value: z.string(),
type: z.nativeEnum(FieldType),
})).optional(),
});
export const ProjectIdInputSchema = z.object({
id: z.string(),
});
export const ProjectShoringRatioInputSchema = z.object({
projectId: z.string(),
});
type ProjectProcedureContext = Pick<TRPCContext, "db">;
export async function listProjects(
ctx: ProjectProcedureContext,
input: z.infer<typeof ProjectListInputSchema>,
) {
const { status, search, cursor, customFieldFilters } = input;
const cfConditions = buildDynamicFieldWhereClauses(customFieldFilters)
.map((dynamicFields) => ({ dynamicFields }));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const where: any = {
...(status ? { status } : {}),
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ shortCode: { contains: search, mode: "insensitive" as const } },
],
}
: {}),
...(cfConditions.length > 0 ? { AND: cfConditions } : {}),
};
const whereWithCursor = cursor ? { ...where, id: { gt: cursor } } : where;
const result = await paginate(
({ skip, take }) =>
ctx.db.project.findMany({
where: whereWithCursor,
skip,
take,
orderBy: [{ startDate: "asc" }, { id: "asc" }],
}),
() => ctx.db.project.count({ where }),
input,
);
const { countsByProjectId } = await countPlanningEntries(ctx.db, {
projectIds: result.items.map((project) => project.id),
});
return {
projects: result.items.map((project) => ({
...project,
_count: {
allocations: countsByProjectId.get(project.id) ?? 0,
},
})),
total: result.total,
page: result.page,
limit: result.limit,
nextCursor: result.nextCursor,
};
}
export async function getProjectById(
ctx: ProjectProcedureContext,
input: z.infer<typeof ProjectIdInputSchema>,
) {
const [project, planningRead] = await Promise.all([
ctx.db.project.findUnique({
where: { id: input.id },
include: { blueprint: true },
}),
loadProjectPlanningReadModel(ctx.db, { projectId: input.id }),
]);
if (!project) {
throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
}
return {
...project,
allocations: planningRead.readModel.assignments,
demands: planningRead.readModel.demands,
assignments: planningRead.readModel.assignments,
};
}
export async function getProjectShoringRatioData(
ctx: ProjectProcedureContext,
input: z.infer<typeof ProjectShoringRatioInputSchema>,
) {
return getProjectShoringRatio(ctx.db, input.projectId);
}
+14 -93
View File
@@ -1,17 +1,17 @@
import { countPlanningEntries } from "@capakraken/application";
import { FieldType, ProjectStatus } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { paginate, PaginationInputSchema } from "../db/pagination.js";
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 { createProjectMutationProcedures } from "./project-mutations.js";
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
import { createProjectBackgroundEffects } from "./project-background-effects.js";
import { getProjectShoringRatio } from "./project-shoring-ratio.js";
import {
getProjectById,
getProjectShoringRatioData,
listProjects,
ProjectIdInputSchema,
ProjectListInputSchema,
ProjectShoringRatioInputSchema,
} from "./project-procedure-support.js";
import { controllerProcedure, createTRPCRouter } from "../trpc.js";
const projectBackgroundEffects = createProjectBackgroundEffects();
@@ -27,94 +27,15 @@ export const projectRouter = createTRPCRouter({
...createProjectMutationProcedures(projectBackgroundEffects),
list: controllerProcedure
.input(
PaginationInputSchema.extend({
status: z.nativeEnum(ProjectStatus).optional(),
search: z.string().optional(),
// Custom field JSONB filters
customFieldFilters: z.array(z.object({
key: z.string(),
value: z.string(),
type: z.nativeEnum(FieldType),
})).optional(),
}),
)
.query(async ({ ctx, input }) => {
const { status, search, cursor, customFieldFilters } = input;
const cfConditions = buildDynamicFieldWhereClauses(customFieldFilters).map((dynamicFields) => ({ dynamicFields }));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const where: any = {
...(status ? { status } : {}),
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ shortCode: { contains: search, mode: "insensitive" as const } },
],
}
: {}),
...(cfConditions.length > 0 ? { AND: cfConditions } : {}),
};
const whereWithCursor = cursor ? { ...where, id: { gt: cursor } } : where;
const result = await paginate(
({ skip, take }) =>
ctx.db.project.findMany({
where: whereWithCursor,
skip,
take,
orderBy: [{ startDate: "asc" }, { id: "asc" }],
}),
() => ctx.db.project.count({ where }),
input,
);
const { countsByProjectId } = await countPlanningEntries(ctx.db, {
projectIds: result.items.map((project) => project.id),
});
return {
projects: result.items.map((project) => ({
...project,
_count: {
allocations: countsByProjectId.get(project.id) ?? 0,
},
})),
total: result.total,
page: result.page,
limit: result.limit,
nextCursor: result.nextCursor,
};
}),
.input(ProjectListInputSchema)
.query(({ ctx, input }) => listProjects(ctx, input)),
getById: controllerProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
const [project, planningRead] = await Promise.all([
ctx.db.project.findUnique({
where: { id: input.id },
include: { blueprint: true },
}),
loadProjectPlanningReadModel(ctx.db, { projectId: input.id }),
]);
if (!project) {
throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
}
return {
...project,
allocations: planningRead.readModel.assignments,
demands: planningRead.readModel.demands,
assignments: planningRead.readModel.assignments,
};
}),
.input(ProjectIdInputSchema)
.query(({ ctx, input }) => getProjectById(ctx, input)),
getShoringRatio: controllerProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ ctx, input }) => getProjectShoringRatio(ctx.db, input.projectId)),
.input(ProjectShoringRatioInputSchema)
.query(({ ctx, input }) => getProjectShoringRatioData(ctx, input)),
});