From 6f3bdd81e877ee4c0b85b531f86c518ce6053753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Thu, 9 Apr 2026 19:24:55 +0200 Subject: [PATCH] perf(api): add explicit Prisma selects on hot read paths Replaces full model includes with field-scoped selects on the resource list (listStaff) query. Avoids fetching large JSONB columns (availability, valueScoreBreakdown) and unused scalar fields (aiSummary, portfolioUrl, fte, resourceType, postalCode, etc.) when only identity/rate fields are needed. Adds RESOURCE_LIST_SELECT constant to packages/api/src/db/selects.ts covering all fields actually consumed by ResourcesClient, FillOpenDemandModal, EstimateWizard, EstimateWorkspaceDraftEditor, and ScenarioPlanner. Co-Authored-By: Claude Sonnet 4.6 --- packages/api/src/db/selects.ts | 31 +++++++++++++++++++ .../api/src/router/resource-read-shared.ts | 14 ++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/api/src/db/selects.ts b/packages/api/src/db/selects.ts index 3bfce25..d555b14 100644 --- a/packages/api/src/db/selects.ts +++ b/packages/api/src/db/selects.ts @@ -1,3 +1,34 @@ export const ROLE_BRIEF_SELECT = { id: true, name: true, color: true } as const; export const PROJECT_BRIEF_SELECT = { id: true, name: true, shortCode: true, status: true, endDate: true } as const; export const RESOURCE_BRIEF_SELECT = { id: true, displayName: true, eid: true, lcrCents: true, chapter: true } as const; + +/** + * Explicit select for the resource list endpoint (listStaff). + * Omits large JSONB columns that are unused in all list consumers: + * - availability (~several KB per row, only needed for scheduling calculations) + * - valueScoreBreakdown (JSON, only needed in detail/dashboard views) + * - aiSummary / aiSummaryUpdatedAt / skillMatrixUpdatedAt / portfolioUrl + * This keeps skills and dynamicFields because they are rendered in the resource table. + */ +export const RESOURCE_LIST_SELECT = { + id: true, + eid: true, + displayName: true, + email: true, + chapter: true, + lcrCents: true, + ucrCents: true, + currency: true, + chargeabilityTarget: true, + skills: true, + dynamicFields: true, + blueprintId: true, + isActive: true, + roleId: true, + federalState: true, + valueScore: true, + rolledOff: true, + departed: true, + createdAt: true, + updatedAt: true, +} as const; diff --git a/packages/api/src/router/resource-read-shared.ts b/packages/api/src/router/resource-read-shared.ts index c9346e5..3e8aafc 100644 --- a/packages/api/src/router/resource-read-shared.ts +++ b/packages/api/src/router/resource-read-shared.ts @@ -1,7 +1,7 @@ import { FieldType, ResourceType } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { ROLE_BRIEF_SELECT } from "../db/selects.js"; +import { RESOURCE_LIST_SELECT, ROLE_BRIEF_SELECT } from "../db/selects.js"; import { buildDynamicFieldWhereClauses } from "./custom-field-filters.js"; import { anonymizeResources, @@ -167,7 +167,8 @@ export async function listStaffResources( const rawResources = await (includeRoles ? ctx.db.resource.findMany({ where, - include: { + select: { + ...RESOURCE_LIST_SELECT, resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } }, }, @@ -176,6 +177,7 @@ export async function listStaffResources( }) : ctx.db.resource.findMany({ where, + select: RESOURCE_LIST_SELECT, orderBy: [{ displayName: "asc" }, { id: "asc" }], })); @@ -264,13 +266,17 @@ export async function listStaffResources( includeRoles ? ctx.db.resource.findMany({ ...baseQuery, - include: { + select: { + ...RESOURCE_LIST_SELECT, resourceRoles: { include: { role: { select: ROLE_BRIEF_SELECT } }, }, }, }) - : ctx.db.resource.findMany(baseQuery), + : ctx.db.resource.findMany({ + ...baseQuery, + select: RESOURCE_LIST_SELECT, + }), ctx.db.resource.count({ where }), ]);