perf(db): add missing indexes, fix N+1 batch delete, add pagination limits

- Add indexes on Resource(blueprintId, roleId), DemandRequirement(roleId),
  Assignment(roleId) — commonly filtered FK columns that were missing indexes
- Replace N+1 batch delete pattern (2N queries) with findAllocationEntries()
  that does 2 total queries via findMany({ id: { in: ids } })
- Add take/skip pagination with default limit of 500 to listDemands and
  listAssignments to prevent unbounded result sets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:09:39 +02:00
parent 110e4ff1aa
commit c098cedf06
7 changed files with 264 additions and 158 deletions
@@ -1,7 +1,11 @@
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { allocationRouter } from "../router/allocation/index.js";
import { emitAllocationCreated, emitAllocationDeleted, emitNotificationCreated } from "../sse/event-bus.js";
import {
emitAllocationCreated,
emitAllocationDeleted,
emitNotificationCreated,
} from "../sse/event-bus.js";
import { checkBudgetThresholds } from "../lib/budget-alerts.js";
import { generateAutoSuggestions } from "../lib/auto-staffing.js";
import { invalidateDashboardCache } from "../lib/cache.js";
@@ -155,6 +159,8 @@ describe("allocation router authorization", () => {
where: {},
include: expect.any(Object),
orderBy: { startDate: "asc" },
take: 500,
skip: 0,
});
});
@@ -196,9 +202,12 @@ describe("allocation router authorization", () => {
});
it("does not treat viewCosts as a substitute for viewPlanning on planning reads", async () => {
const caller = createProtectedCallerWithOverrides({}, {
granted: [PermissionKey.VIEW_COSTS],
});
const caller = createProtectedCallerWithOverrides(
{},
{
granted: [PermissionKey.VIEW_COSTS],
},
);
await expect(caller.listAssignments({})).rejects.toMatchObject({
code: "FORBIDDEN",
@@ -208,32 +217,47 @@ describe("allocation router authorization", () => {
it.each([
{ name: "list", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.list({}) },
{ name: "listView", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listView({}) },
{ name: "listDemands", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listDemands({}) },
{ name: "listAssignments", invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listAssignments({}) },
{
name: "listView",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listView({}),
},
{
name: "listDemands",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listDemands({}),
},
{
name: "listAssignments",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.listAssignments({}),
},
{
name: "getAssignmentById",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.getAssignmentById({ id: "assignment_1" }),
invoke: (caller: ReturnType<typeof createProtectedCaller>) =>
caller.getAssignmentById({ id: "assignment_1" }),
},
{
name: "resolveAssignment",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.resolveAssignment({ assignmentId: "assignment_1" }),
invoke: (caller: ReturnType<typeof createProtectedCaller>) =>
caller.resolveAssignment({ assignmentId: "assignment_1" }),
},
{
name: "getDemandRequirementById",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.getDemandRequirementById({ id: "demand_1" }),
invoke: (caller: ReturnType<typeof createProtectedCaller>) =>
caller.getDemandRequirementById({ id: "demand_1" }),
},
{
name: "checkResourceAvailability",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.checkResourceAvailability(planningWindow),
invoke: (caller: ReturnType<typeof createProtectedCaller>) =>
caller.checkResourceAvailability(planningWindow),
},
{
name: "getResourceAvailabilityView",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.getResourceAvailabilityView(planningWindow),
invoke: (caller: ReturnType<typeof createProtectedCaller>) =>
caller.getResourceAvailabilityView(planningWindow),
},
{
name: "getResourceAvailabilitySummary",
invoke: (caller: ReturnType<typeof createProtectedCaller>) => caller.getResourceAvailabilitySummary(planningWindow),
invoke: (caller: ReturnType<typeof createProtectedCaller>) =>
caller.getResourceAvailabilitySummary(planningWindow),
},
])("requires planning read access for $name", async ({ invoke }) => {
const caller = createProtectedCaller({});
@@ -708,7 +732,9 @@ describe("allocation entry resolution router", () => {
it("logs and swallows background side-effect failures during demand creation", async () => {
vi.mocked(invalidateDashboardCache).mockRejectedValueOnce(new Error("redis unavailable"));
vi.mocked(checkBudgetThresholds).mockRejectedValueOnce(new Error("budget alerts unavailable"));
vi.mocked(generateAutoSuggestions).mockRejectedValueOnce(new Error("auto suggestions unavailable"));
vi.mocked(generateAutoSuggestions).mockRejectedValueOnce(
new Error("auto suggestions unavailable"),
);
const createdDemandRequirement = {
id: "demand_safe_1",
@@ -761,7 +787,10 @@ describe("allocation entry resolution router", () => {
"Allocation background side effect failed",
);
expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
expect.objectContaining({ effectName: "generateAutoSuggestions", demandRequirementId: "demand_safe_1" }),
expect.objectContaining({
effectName: "generateAutoSuggestions",
demandRequirementId: "demand_safe_1",
}),
"Allocation background side effect failed",
);
});
@@ -1017,7 +1046,8 @@ describe("allocation entry resolution router", () => {
}),
},
demandRequirement: {
findUnique: vi.fn()
findUnique: vi
.fn()
.mockResolvedValueOnce({
id: "demand_1",
projectId: "project_1",
@@ -1182,7 +1212,11 @@ describe("allocation entry resolution router", () => {
expect(db.assignment.delete).toHaveBeenCalledWith({
where: { id: "assignment_explicit_1" },
});
expect(emitAllocationDeleted).toHaveBeenCalledWith("assignment_explicit_1", "project_1", "resource_1");
expect(emitAllocationDeleted).toHaveBeenCalledWith(
"assignment_explicit_1",
"project_1",
"resource_1",
);
});
it("updates an explicit demand row through allocation.update", async () => {
@@ -1276,15 +1310,13 @@ describe("allocation entry resolution router", () => {
const db = {
demandRequirement: {
findUnique: vi.fn().mockImplementation(
({ where }: { where: { id?: string } }) => {
if (where.id === "demand_stale") {
return existingDemand;
}
findUnique: vi.fn().mockImplementation(({ where }: { where: { id?: string } }) => {
if (where.id === "demand_stale") {
return existingDemand;
}
return null;
},
),
return null;
}),
update: vi.fn().mockResolvedValue(updatedDemand),
},
assignment: {
@@ -1360,15 +1392,29 @@ describe("allocation entry resolution router", () => {
findUnique: vi.fn().mockResolvedValue(null),
},
demandRequirement: {
findUnique: vi.fn().mockImplementation(({ where }: { where: { id: string } }) =>
where.id === "demand_1" ? explicitDemand : null,
),
findUnique: vi
.fn()
.mockImplementation(({ where }: { where: { id: string } }) =>
where.id === "demand_1" ? explicitDemand : null,
),
findMany: vi
.fn()
.mockImplementation(({ where }: { where: { id: { in: string[] } } }) =>
[explicitDemand].filter((d) => where.id.in.includes(d.id)),
),
delete: vi.fn().mockResolvedValue({}),
},
assignment: {
findUnique: vi.fn().mockImplementation(({ where }: { where: { id: string } }) =>
where.id === "assignment_1" ? explicitAssignment : null,
),
findUnique: vi
.fn()
.mockImplementation(({ where }: { where: { id: string } }) =>
where.id === "assignment_1" ? explicitAssignment : null,
),
findMany: vi
.fn()
.mockImplementation(({ where }: { where: { id: { in: string[] } } }) =>
[explicitAssignment].filter((a) => where.id.in.includes(a.id)),
),
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
delete: vi.fn().mockResolvedValue({}),
},
@@ -1424,15 +1470,13 @@ describe("allocation entry resolution router", () => {
findUnique: vi.fn().mockResolvedValue(null),
},
assignment: {
findUnique: vi.fn().mockImplementation(
({ where }: { where: { id?: string } }) => {
if (where.id === "assignment_stale") {
return existingAssignment;
}
findUnique: vi.fn().mockImplementation(({ where }: { where: { id?: string } }) => {
if (where.id === "assignment_stale") {
return existingAssignment;
}
return null;
},
),
return null;
}),
delete: vi.fn().mockResolvedValue({}),
},
auditLog: {