perf(api,web,db): refactor and optimize for enterprise readiness
- Add missing @@index([userId]) on Account and Session models (auth query perf) - Batch holiday-auto-import to eliminate N+1 query pattern (O(n) → O(1)) - Reduce SessionProvider refetchInterval from 5min to 15min - Fix Cache-Control catch-all to stop blocking static asset caching - Decompose assistant-tools.ts (2,562 → 809 lines) into callers, helpers, access-control modules - Add @next/bundle-analyzer for data-driven bundle optimization - Add @react-pdf/renderer to optimizePackageImports - Add safety caps (take limits) on unbounded findMany queries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ describe("assistant broadcast send tool", () => {
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith({
|
||||
select: { id: true },
|
||||
take: 10_000,
|
||||
});
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
@@ -80,6 +81,7 @@ describe("assistant broadcast send tool", () => {
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith({
|
||||
select: { id: true },
|
||||
take: 10_000,
|
||||
});
|
||||
expect(create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function autoImportPublicHolidays(
|
||||
country: { select: { code: true } },
|
||||
metroCity: { select: { name: true } },
|
||||
},
|
||||
take: 50_000,
|
||||
});
|
||||
|
||||
if (resources.length === 0) {
|
||||
@@ -103,43 +104,68 @@ export async function autoImportPublicHolidays(
|
||||
if (holidays.length === 0) continue;
|
||||
const resourceIds = groupedResources.map((resource: { id: string }) => resource.id);
|
||||
|
||||
// Batch: collect all holiday dates, fetch existing records in one query
|
||||
const holidayDates = holidays.map((h) => new Date(h.date));
|
||||
|
||||
const allExisting: MinimalVacation[] = await db.vacation.findMany({
|
||||
where: {
|
||||
resourceId: { in: resourceIds },
|
||||
type: "PUBLIC_HOLIDAY",
|
||||
startDate: { in: holidayDates },
|
||||
endDate: { in: holidayDates },
|
||||
},
|
||||
select: { resourceId: true, startDate: true, endDate: true },
|
||||
});
|
||||
|
||||
// Map: date ISO string → set of resourceIds that already have the holiday
|
||||
const existingByDate = new Map<string, Set<string>>();
|
||||
for (const v of allExisting) {
|
||||
const key = v.startDate.toISOString();
|
||||
const set = existingByDate.get(key) ?? new Set();
|
||||
set.add(v.resourceId);
|
||||
existingByDate.set(key, set);
|
||||
}
|
||||
|
||||
// Build all new records for a single createMany call
|
||||
const allNewRecords: Array<{
|
||||
resourceId: string;
|
||||
type: string;
|
||||
status: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
note: string;
|
||||
isHalfDay: boolean;
|
||||
approvedAt: Date;
|
||||
}> = [];
|
||||
const approvedAt = new Date();
|
||||
|
||||
for (const holiday of holidays) {
|
||||
const holidayDate = new Date(holiday.date);
|
||||
|
||||
// Find existing records for this date + type to skip duplicates
|
||||
const existing: MinimalVacation[] = await db.vacation.findMany({
|
||||
where: {
|
||||
resourceId: { in: resourceIds },
|
||||
type: "PUBLIC_HOLIDAY",
|
||||
startDate: holidayDate,
|
||||
endDate: holidayDate,
|
||||
},
|
||||
select: { resourceId: true, startDate: true, endDate: true },
|
||||
});
|
||||
|
||||
const existingResourceIds = new Set(existing.map((v: MinimalVacation) => v.resourceId));
|
||||
const newResourceIds = resourceIds.filter((id: string) => !existingResourceIds.has(id));
|
||||
const dateKey = holidayDate.toISOString();
|
||||
const existingResourceIds = existingByDate.get(dateKey) ?? new Set();
|
||||
|
||||
totalSkipped += existingResourceIds.size;
|
||||
|
||||
if (newResourceIds.length === 0) continue;
|
||||
|
||||
const records = newResourceIds.map((resourceId: string) => ({
|
||||
resourceId,
|
||||
type: "PUBLIC_HOLIDAY",
|
||||
status: "APPROVED",
|
||||
startDate: holidayDate,
|
||||
endDate: holidayDate,
|
||||
note: holiday.name,
|
||||
isHalfDay: false,
|
||||
approvedAt: new Date(),
|
||||
}));
|
||||
for (const resourceId of resourceIds) {
|
||||
if (existingResourceIds.has(resourceId)) continue;
|
||||
allNewRecords.push({
|
||||
resourceId,
|
||||
type: "PUBLIC_HOLIDAY",
|
||||
status: "APPROVED",
|
||||
startDate: holidayDate,
|
||||
endDate: holidayDate,
|
||||
note: holiday.name,
|
||||
isHalfDay: false,
|
||||
approvedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (allNewRecords.length > 0) {
|
||||
const result = await db.vacation.createMany({
|
||||
data: records,
|
||||
data: allNewRecords,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
totalCreated += result.count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ export async function resolveRecipients(
|
||||
case "role": {
|
||||
// Find all users with the given systemRole
|
||||
const roleUsers = await db.user.findMany({
|
||||
where: { systemRole: targetValue as "ADMIN" | "MANAGER" | "CONTROLLER" | "USER" | "VIEWER" },
|
||||
where: {
|
||||
systemRole: targetValue as "ADMIN" | "MANAGER" | "CONTROLLER" | "USER" | "VIEWER",
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
userIds = roleUsers.map((u) => u.id);
|
||||
@@ -36,9 +38,7 @@ export async function resolveRecipients(
|
||||
where: { projectId: targetValue, status: { not: "CANCELLED" } },
|
||||
select: { resource: { select: { userId: true } } },
|
||||
});
|
||||
userIds = assignments
|
||||
.map((a) => a.resource.userId)
|
||||
.filter((id): id is string => !!id);
|
||||
userIds = assignments.map((a) => a.resource.userId).filter((id): id is string => !!id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -49,9 +49,7 @@ export async function resolveRecipients(
|
||||
where: { orgUnitId: targetValue, isActive: true },
|
||||
select: { userId: true },
|
||||
});
|
||||
userIds = resources
|
||||
.map((r) => r.userId)
|
||||
.filter((id): id is string => !!id);
|
||||
userIds = resources.map((r) => r.userId).filter((id): id is string => !!id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -59,6 +57,7 @@ export async function resolveRecipients(
|
||||
// User model has no isActive — get all users
|
||||
const allUsers = await db.user.findMany({
|
||||
select: { id: true },
|
||||
take: 10_000,
|
||||
});
|
||||
userIds = allUsers.map((u) => u.id);
|
||||
break;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import type { ToolAccessRequirements, ToolContext, ToolDef } from "./shared.js";
|
||||
import { AssistantVisibleError } from "./helpers.js";
|
||||
|
||||
export const CONTROLLER_ASSISTANT_ROLES = [
|
||||
SystemRole.ADMIN,
|
||||
SystemRole.MANAGER,
|
||||
SystemRole.CONTROLLER,
|
||||
] as const;
|
||||
|
||||
export const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequirements>> = {
|
||||
search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||
get_project: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||
update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
};
|
||||
|
||||
export type AssistantToolAccessEvaluationContext = Pick<ToolContext, "permissions" | "userRole">;
|
||||
|
||||
export type AssistantToolAccessFailure =
|
||||
| { type: "role" }
|
||||
| {
|
||||
type: "permission";
|
||||
permission?: PermissionKey;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function hasAssistantResourceOverviewAccess(permissions: Set<PermissionKey>): boolean {
|
||||
return (
|
||||
permissions.has(PermissionKey.VIEW_ALL_RESOURCES) ||
|
||||
permissions.has(PermissionKey.MANAGE_RESOURCES)
|
||||
);
|
||||
}
|
||||
|
||||
export function getAssistantToolAccessRequirements(
|
||||
tool: ToolDef,
|
||||
): ToolAccessRequirements | undefined {
|
||||
return tool.access ?? LEGACY_MONOLITHIC_TOOL_ACCESS[tool.function.name];
|
||||
}
|
||||
|
||||
export function getAssistantToolAccessFailure(
|
||||
tool: ToolDef,
|
||||
ctx: AssistantToolAccessEvaluationContext,
|
||||
): AssistantToolAccessFailure | null {
|
||||
const access = getAssistantToolAccessRequirements(tool);
|
||||
if (!access) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
access.allowedSystemRoles &&
|
||||
!access.allowedSystemRoles.includes(ctx.userRole as SystemRole)
|
||||
) {
|
||||
return { type: "role" };
|
||||
}
|
||||
|
||||
const missingRequiredPermission = access.requiredPermissions?.find(
|
||||
(permission) => !ctx.permissions.has(permission),
|
||||
);
|
||||
if (missingRequiredPermission) {
|
||||
return {
|
||||
type: "permission",
|
||||
permission: missingRequiredPermission,
|
||||
};
|
||||
}
|
||||
|
||||
if (access.requiresPlanningRead && !ctx.permissions.has(PermissionKey.VIEW_PLANNING)) {
|
||||
return {
|
||||
type: "permission",
|
||||
permission: PermissionKey.VIEW_PLANNING,
|
||||
};
|
||||
}
|
||||
|
||||
if (access.requiresCostView && !ctx.permissions.has(PermissionKey.VIEW_COSTS)) {
|
||||
return {
|
||||
type: "permission",
|
||||
permission: PermissionKey.VIEW_COSTS,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
access.requiresAdvancedAssistant &&
|
||||
!ctx.permissions.has(PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS)
|
||||
) {
|
||||
return {
|
||||
type: "permission",
|
||||
permission: PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS,
|
||||
};
|
||||
}
|
||||
|
||||
if (access.requiresResourceOverview && !hasAssistantResourceOverviewAccess(ctx.permissions)) {
|
||||
return {
|
||||
type: "permission",
|
||||
message: "Permission denied: you need resource overview access to perform this action.",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function toAssistantToolAccessError(
|
||||
failure: AssistantToolAccessFailure,
|
||||
): AssistantVisibleError {
|
||||
if (failure.type === "role") {
|
||||
return new AssistantVisibleError("You do not have permission to perform this action.");
|
||||
}
|
||||
|
||||
if (failure.permission) {
|
||||
return new AssistantVisibleError(
|
||||
`Permission denied: you need the "${failure.permission}" permission to perform this action.`,
|
||||
);
|
||||
}
|
||||
|
||||
return new AssistantVisibleError(
|
||||
failure.message ?? "You do not have permission to perform this action.",
|
||||
);
|
||||
}
|
||||
|
||||
export function canAccessAssistantTool(
|
||||
tool: ToolDef,
|
||||
ctx: AssistantToolAccessEvaluationContext,
|
||||
): boolean {
|
||||
return getAssistantToolAccessFailure(tool, ctx) === null;
|
||||
}
|
||||
|
||||
export function getAvailableAssistantToolsForContext(
|
||||
allTools: ToolDef[],
|
||||
permissions: Set<PermissionKey>,
|
||||
userRole: string,
|
||||
): ToolDef[] {
|
||||
return allTools.filter((tool) => canAccessAssistantTool(tool, { permissions, userRole }));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createCallerFactory } from "../../trpc.js";
|
||||
import { chargeabilityReportRouter } from "../chargeability-report.js";
|
||||
import { computationGraphRouter } from "../computation-graph.js";
|
||||
import { timelineRouter } from "../timeline.js";
|
||||
import { auditLogRouter } from "../audit-log.js";
|
||||
import { importExportRouter } from "../import-export.js";
|
||||
import { dispoRouter } from "../dispo.js";
|
||||
import { resourceRouter } from "../resource.js";
|
||||
import { settingsRouter } from "../settings.js";
|
||||
import { systemRoleConfigRouter } from "../system-role-config.js";
|
||||
import { userRouter } from "../user.js";
|
||||
import { notificationRouter } from "../notification.js";
|
||||
import { estimateRouter } from "../estimate.js";
|
||||
import { webhookRouter } from "../webhook.js";
|
||||
import { countryRouter } from "../country.js";
|
||||
import { holidayCalendarRouter } from "../holiday-calendar.js";
|
||||
import { blueprintRouter } from "../blueprint.js";
|
||||
import { roleRouter } from "../role.js";
|
||||
import { clientRouter } from "../client.js";
|
||||
import { orgUnitRouter } from "../org-unit.js";
|
||||
import { projectRouter } from "../project.js";
|
||||
import { rateCardRouter } from "../rate-card.js";
|
||||
import { reportRouter } from "../report.js";
|
||||
import { vacationRouter } from "../vacation.js";
|
||||
import { entitlementRouter } from "../entitlement.js";
|
||||
import { commentRouter } from "../comment.js";
|
||||
import { managementLevelRouter } from "../management-level.js";
|
||||
import { utilizationCategoryRouter } from "../utilization-category.js";
|
||||
import { calculationRuleRouter } from "../calculation-rules.js";
|
||||
import { effortRuleRouter } from "../effort-rule.js";
|
||||
import { experienceMultiplierRouter } from "../experience-multiplier.js";
|
||||
import { dashboardRouter } from "../dashboard.js";
|
||||
import { insightsRouter } from "../insights.js";
|
||||
import { scenarioRouter } from "../scenario.js";
|
||||
import { allocationRouter } from "../allocation/index.js";
|
||||
import { staffingRouter } from "../staffing.js";
|
||||
|
||||
export const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter);
|
||||
export const createComputationGraphCaller = createCallerFactory(computationGraphRouter);
|
||||
export const createTimelineCaller = createCallerFactory(timelineRouter);
|
||||
export const createAuditLogCaller = createCallerFactory(auditLogRouter);
|
||||
export const createImportExportCaller = createCallerFactory(importExportRouter);
|
||||
export const createDispoCaller = createCallerFactory(dispoRouter);
|
||||
export const createResourceCaller = createCallerFactory(resourceRouter);
|
||||
export const createSettingsCaller = createCallerFactory(settingsRouter);
|
||||
export const createSystemRoleConfigCaller = createCallerFactory(systemRoleConfigRouter);
|
||||
export const createUserCaller = createCallerFactory(userRouter);
|
||||
export const createNotificationCaller = createCallerFactory(notificationRouter);
|
||||
export const createEstimateCaller = createCallerFactory(estimateRouter);
|
||||
export const createWebhookCaller = createCallerFactory(webhookRouter);
|
||||
export const createCountryCaller = createCallerFactory(countryRouter);
|
||||
export const createHolidayCalendarCaller = createCallerFactory(holidayCalendarRouter);
|
||||
export const createBlueprintCaller = createCallerFactory(blueprintRouter);
|
||||
export const createRoleCaller = createCallerFactory(roleRouter);
|
||||
export const createClientCaller = createCallerFactory(clientRouter);
|
||||
export const createOrgUnitCaller = createCallerFactory(orgUnitRouter);
|
||||
export const createProjectCaller = createCallerFactory(projectRouter);
|
||||
export const createRateCardCaller = createCallerFactory(rateCardRouter);
|
||||
export const createReportCaller = createCallerFactory(reportRouter);
|
||||
export const createVacationCaller = createCallerFactory(vacationRouter);
|
||||
export const createEntitlementCaller = createCallerFactory(entitlementRouter);
|
||||
export const createCommentCaller = createCallerFactory(commentRouter);
|
||||
export const createManagementLevelCaller = createCallerFactory(managementLevelRouter);
|
||||
export const createUtilizationCategoryCaller = createCallerFactory(utilizationCategoryRouter);
|
||||
export const createCalculationRuleCaller = createCallerFactory(calculationRuleRouter);
|
||||
export const createEffortRuleCaller = createCallerFactory(effortRuleRouter);
|
||||
export const createExperienceMultiplierCaller = createCallerFactory(experienceMultiplierRouter);
|
||||
export const createDashboardCaller = createCallerFactory(dashboardRouter);
|
||||
export const createInsightsCaller = createCallerFactory(insightsRouter);
|
||||
export const createScenarioCaller = createCallerFactory(scenarioRouter);
|
||||
export const createAllocationCaller = createCallerFactory(allocationRouter);
|
||||
export const createStaffingCaller = createCallerFactory(staffingRouter);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -282,6 +282,7 @@ model Account {
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@unique([provider, providerAccountId])
|
||||
@@map("accounts")
|
||||
}
|
||||
@@ -293,6 +294,7 @@ model Session {
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
@@ -938,6 +940,7 @@ model Project {
|
||||
color String? // Hex color for timeline display, e.g. "#3b82f6"
|
||||
coverImageUrl String? @db.Text // Base64 data-URL for project cover art
|
||||
coverFocusY Int @default(50) // Vertical focus point 0-100 (% from top)
|
||||
coverAiGenerated Boolean @default(false) // EGAI 4.3.1.3 — true when cover was AI-generated
|
||||
|
||||
// staffingReqs: StaffingRequirement[]
|
||||
staffingReqs Json @db.JsonB @default("[]")
|
||||
|
||||
Reference in New Issue
Block a user