perf(api): eliminate 3 N+1 query patterns

- timeline-holiday-load-support: deduplicate getResolvedCalendarHolidays
  by location key so resources sharing the same country/state/city resolve
  holidays once instead of per-resource
- rate-card-lookup: add lookupRatesBatch that loads rate card lines once
  and scores locally per demand line, replacing per-line DB round-trips
  in estimate-demand-lines autoFillDemandLineRates
- config-readmodels: include _count in utilization-category list query
  instead of calling getById per category for project counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 22:59:45 +02:00
parent dd2c9c0f88
commit 5a4836d292
8 changed files with 727 additions and 571 deletions
@@ -4,138 +4,149 @@ import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } fro
type ConfigReadmodelsDeps = {
createManagementLevelCaller: (ctx: TRPCContext) => {
listGroups: () => Promise<Array<{
id: string;
name: string;
targetPercentage: number | null;
levels: Array<{ id: string; name: string }>;
}>>;
};
createUtilizationCategoryCaller: (ctx: TRPCContext) => {
list: () => Promise<Array<{
id: string;
code: string;
name: string;
description: string | null;
}>>;
getById: (params: { id: string }) => Promise<{
_count: { projects: number };
}>;
};
createCalculationRuleCaller: (ctx: TRPCContext) => {
list: () => Promise<Array<{
id: string;
name: string;
description: string | null;
isActive: boolean;
triggerType: string;
orderType: string | null;
costEffect: string;
costReductionPercent: number | null;
chargeabilityEffect: string;
priority: number;
project: {
listGroups: () => Promise<
Array<{
id: string;
name: string;
shortCode: string;
} | null;
}>>;
targetPercentage: number | null;
levels: Array<{ id: string; name: string }>;
}>
>;
};
createUtilizationCategoryCaller: (ctx: TRPCContext) => {
list: () => Promise<
Array<{
id: string;
code: string;
name: string;
description: string | null;
_count: { projects: number };
}>
>;
};
createCalculationRuleCaller: (ctx: TRPCContext) => {
list: () => Promise<
Array<{
id: string;
name: string;
description: string | null;
isActive: boolean;
triggerType: string;
orderType: string | null;
costEffect: string;
costReductionPercent: number | null;
chargeabilityEffect: string;
priority: number;
project: {
id: string;
name: string;
shortCode: string;
} | null;
}>
>;
};
createEffortRuleCaller: (ctx: TRPCContext) => {
list: () => Promise<Array<{
name: string;
isDefault: boolean;
rules: Array<{
id: string;
description: string | null;
scopeType: string;
discipline: string;
chapter: string | null;
unitMode: string;
hoursPerUnit: number;
sortOrder: number;
}>;
}>>;
list: () => Promise<
Array<{
name: string;
isDefault: boolean;
rules: Array<{
id: string;
description: string | null;
scopeType: string;
discipline: string;
chapter: string | null;
unitMode: string;
hoursPerUnit: number;
sortOrder: number;
}>;
}>
>;
};
createExperienceMultiplierCaller: (ctx: TRPCContext) => {
list: () => Promise<Array<{
name: string;
isDefault: boolean;
rules: Array<{
id: string;
description: string | null;
chapter: string | null;
location: string | null;
level: string | null;
costMultiplier: number;
billMultiplier: number;
shoringRatio: number | null;
additionalEffortRatio: number | null;
sortOrder: number;
}>;
}>>;
list: () => Promise<
Array<{
name: string;
isDefault: boolean;
rules: Array<{
id: string;
description: string | null;
chapter: string | null;
location: string | null;
level: string | null;
costMultiplier: number;
billMultiplier: number;
shoringRatio: number | null;
additionalEffortRatio: number | null;
sortOrder: number;
}>;
}>
>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
};
export const configReadmodelToolDefinitions: ToolDef[] = withToolAccess([
export const configReadmodelToolDefinitions: ToolDef[] = withToolAccess(
[
{
type: "function",
function: {
name: "list_management_levels",
description: "List management level groups and their levels with target percentages.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_utilization_categories",
description: "List utilization categories (cost classification for projects).",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_calculation_rules",
description: "List calculation rules for cost attribution and chargeability.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_effort_rules",
description: "List effort estimation rules with their formulas and conditions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_experience_multipliers",
description: "List experience multipliers that adjust effort estimates based on seniority.",
parameters: { type: "object", properties: {} },
},
},
],
{
type: "function",
function: {
name: "list_management_levels",
description: "List management level groups and their levels with target percentages.",
parameters: { type: "object", properties: {} },
list_management_levels: {
requiresPlanningRead: true,
},
list_utilization_categories: {
requiresPlanningRead: true,
},
list_calculation_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_effort_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_experience_multipliers: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
},
{
type: "function",
function: {
name: "list_utilization_categories",
description: "List utilization categories (cost classification for projects).",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_calculation_rules",
description: "List calculation rules for cost attribution and chargeability.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_effort_rules",
description: "List effort estimation rules with their formulas and conditions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_experience_multipliers",
description: "List experience multipliers that adjust effort estimates based on seniority.",
parameters: { type: "object", properties: {} },
},
},
], {
list_management_levels: {
requiresPlanningRead: true,
},
list_utilization_categories: {
requiresPlanningRead: true,
},
list_calculation_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_effort_rules: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
list_experience_multipliers: {
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
},
});
);
export function createConfigReadmodelExecutors(
deps: ConfigReadmodelsDeps,
@@ -155,19 +166,13 @@ export function createConfigReadmodelExecutors(
async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUtilizationCategoryCaller(deps.createScopedCallerContext(ctx));
const categories = await caller.list();
const categoriesWithCounts = await Promise.all(
categories.map(async (category) => ({
category,
projectCount: (await caller.getById({ id: category.id }))._count.projects,
})),
);
return categoriesWithCounts.map(({ category, projectCount }) => ({
return categories.map((category) => ({
id: category.id,
code: category.code,
name: category.name,
description: category.description,
projectCount,
projectCount: category._count.projects,
}));
},
@@ -198,41 +203,45 @@ export function createConfigReadmodelExecutors(
async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createEffortRuleCaller(deps.createScopedCallerContext(ctx));
const ruleSets = await caller.list();
return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
scopeType: rule.scopeType,
discipline: rule.discipline,
chapter: rule.chapter,
unitMode: rule.unitMode,
hoursPerUnit: rule.hoursPerUnit,
sortOrder: rule.sortOrder,
ruleSet: {
name: ruleSet.name,
isDefault: ruleSet.isDefault,
},
})));
return ruleSets.flatMap((ruleSet) =>
ruleSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
scopeType: rule.scopeType,
discipline: rule.discipline,
chapter: rule.chapter,
unitMode: rule.unitMode,
hoursPerUnit: rule.hoursPerUnit,
sortOrder: rule.sortOrder,
ruleSet: {
name: ruleSet.name,
isDefault: ruleSet.isDefault,
},
})),
);
},
async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createExperienceMultiplierCaller(deps.createScopedCallerContext(ctx));
const multiplierSets = await caller.list();
return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
chapter: rule.chapter,
location: rule.location,
level: rule.level,
costMultiplier: rule.costMultiplier,
billMultiplier: rule.billMultiplier,
shoringRatio: rule.shoringRatio,
additionalEffortRatio: rule.additionalEffortRatio,
sortOrder: rule.sortOrder,
multiplierSet: {
name: multiplierSet.name,
isDefault: multiplierSet.isDefault,
},
})));
return multiplierSets.flatMap((multiplierSet) =>
multiplierSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
chapter: rule.chapter,
location: rule.location,
level: rule.level,
costMultiplier: rule.costMultiplier,
billMultiplier: rule.billMultiplier,
shoringRatio: rule.shoringRatio,
additionalEffortRatio: rule.additionalEffortRatio,
sortOrder: rule.sortOrder,
multiplierSet: {
name: multiplierSet.name,
isDefault: multiplierSet.isDefault,
},
})),
);
},
};
}