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:
@@ -1,11 +1,9 @@
|
||||
import { normalizeEstimateDemandLine, summarizeEstimateDemandLines } from "@capakraken/engine";
|
||||
import { CreateEstimateSchema } from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import { lookupRate } from "../lib/rate-card-lookup.js";
|
||||
import { lookupRatesBatch } from "../lib/rate-card-lookup.js";
|
||||
|
||||
function buildComputedMetrics(
|
||||
demandLines: z.infer<typeof CreateEstimateSchema>["demandLines"],
|
||||
) {
|
||||
function buildComputedMetrics(demandLines: z.infer<typeof CreateEstimateSchema>["demandLines"]) {
|
||||
const summary = summarizeEstimateDemandLines(demandLines);
|
||||
|
||||
return [
|
||||
@@ -62,7 +60,9 @@ function normalizeDemandLines<
|
||||
const snapshotsByResourceId = new Map(
|
||||
input.resourceSnapshots
|
||||
.filter(
|
||||
(snapshot): snapshot is (typeof input.resourceSnapshots)[number] & {
|
||||
(
|
||||
snapshot,
|
||||
): snapshot is (typeof input.resourceSnapshots)[number] & {
|
||||
resourceId: string;
|
||||
} => typeof snapshot.resourceId === "string" && snapshot.resourceId.length > 0,
|
||||
)
|
||||
@@ -71,8 +71,7 @@ function normalizeDemandLines<
|
||||
|
||||
return input.demandLines.map((line) =>
|
||||
normalizeEstimateDemandLine(line, {
|
||||
resourceSnapshot:
|
||||
line.resourceId != null ? snapshotsByResourceId.get(line.resourceId) : null,
|
||||
resourceSnapshot: line.resourceId != null ? snapshotsByResourceId.get(line.resourceId) : null,
|
||||
defaultCurrency: baseCurrency,
|
||||
}),
|
||||
);
|
||||
@@ -117,44 +116,60 @@ export async function autoFillDemandLineRates(
|
||||
clientId = project?.clientId ?? null;
|
||||
}
|
||||
|
||||
const autoFilledIndices: number[] = [];
|
||||
const enriched = await Promise.all(
|
||||
demandLines.map(async (line, index) => {
|
||||
const isDefaultRate = line.costRateCents === 0 && line.billRateCents === 0;
|
||||
const hasExplicitSource = line.rateSource != null && line.rateSource.length > 0;
|
||||
if (!isDefaultRate || hasExplicitSource) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const result = await lookupRate(db, {
|
||||
clientId,
|
||||
chapter: line.chapter ?? null,
|
||||
roleId: line.roleId ?? null,
|
||||
// Identify which lines need auto-fill and collect their lookup params
|
||||
const needsLookup: {
|
||||
index: number;
|
||||
params: { chapter: string | null; roleId: string | null };
|
||||
}[] = [];
|
||||
for (let i = 0; i < demandLines.length; i++) {
|
||||
const line = demandLines[i]!;
|
||||
const isDefaultRate = line.costRateCents === 0 && line.billRateCents === 0;
|
||||
const hasExplicitSource = line.rateSource != null && line.rateSource.length > 0;
|
||||
if (isDefaultRate && !hasExplicitSource) {
|
||||
needsLookup.push({
|
||||
index: i,
|
||||
params: { chapter: line.chapter ?? null, roleId: line.roleId ?? null },
|
||||
});
|
||||
if (!result) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
autoFilledIndices.push(index);
|
||||
const existingMetadata = (line.metadata ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
...line,
|
||||
costRateCents: result.costRateCents,
|
||||
billRateCents: result.billRateCents,
|
||||
currency: result.currency,
|
||||
rateSource: `rate-card:${result.rateCardId}`,
|
||||
metadata: {
|
||||
...existingMetadata,
|
||||
autoAppliedRateCard: {
|
||||
rateCardId: result.rateCardId,
|
||||
rateCardLineId: result.rateCardLineId,
|
||||
rateCardName: result.rateCardName,
|
||||
appliedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
if (needsLookup.length === 0) {
|
||||
return { demandLines, autoFilledIndices: [] };
|
||||
}
|
||||
|
||||
// Single DB query for all rate card lines, scored locally per demand line
|
||||
const results = await lookupRatesBatch(
|
||||
db,
|
||||
clientId,
|
||||
needsLookup.map((entry) => entry.params),
|
||||
);
|
||||
|
||||
const autoFilledIndices: number[] = [];
|
||||
const enriched = [...demandLines];
|
||||
for (let i = 0; i < needsLookup.length; i++) {
|
||||
const result = results[i];
|
||||
if (!result) continue;
|
||||
const { index } = needsLookup[i]!;
|
||||
autoFilledIndices.push(index);
|
||||
const line = demandLines[index]!;
|
||||
const existingMetadata = (line.metadata ?? {}) as Record<string, unknown>;
|
||||
enriched[index] = {
|
||||
...line,
|
||||
costRateCents: result.costRateCents,
|
||||
billRateCents: result.billRateCents,
|
||||
currency: result.currency,
|
||||
rateSource: `rate-card:${result.rateCardId}`,
|
||||
metadata: {
|
||||
...existingMetadata,
|
||||
autoAppliedRateCard: {
|
||||
rateCardId: result.rateCardId,
|
||||
rateCardLineId: result.rateCardLineId,
|
||||
rateCardName: result.rateCardName,
|
||||
appliedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { demandLines: enriched, autoFilledIndices };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user