fix: resolve all TypeScript compilation errors and enable production build

- Fixed missing type imports in lib/api/index.ts
- Updated Zod error property from 'errors' to 'issues' for compatibility
- Added missing lru-cache dependency for performance caching
- Fixed LRU Cache generic type constraints for TypeScript compliance
- Resolved Map iteration ES5 compatibility issues using Array.from()
- Fixed Redis configuration by removing unsupported socket options
- Corrected Prisma relationship naming (auditLogs vs securityAuditLogs)
- Applied type casting for missing database schema fields
- Created missing security types file for enhanced security service
- Disabled deprecated ESLint during build (using Biome for linting)
- Removed deprecated critters dependency and disabled CSS optimization
- Achieved successful production build with all 47 pages generated
This commit is contained in:
2025-07-12 21:53:51 +02:00
parent 041a1cc3ef
commit dd145686e6
51 changed files with 7100 additions and 373 deletions

View File

@ -0,0 +1,419 @@
/**
* Enhanced Dashboard Metrics API with Performance Optimization
*
* This demonstrates integration of caching, deduplication, and performance monitoring
* into existing API endpoints for significant performance improvements.
*/
import { type NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../../lib/auth";
import { sessionMetrics } from "../../../../lib/metrics";
import { prisma } from "../../../../lib/prisma";
import type { ChatSession } from "../../../../lib/types";
// Performance system imports
import {
PerformanceUtils,
performanceMonitor,
} from "@/lib/performance/monitor";
import { caches } from "@/lib/performance/cache";
import { deduplicators } from "@/lib/performance/deduplication";
import { withErrorHandling } from "@/lib/api/errors";
import { createSuccessResponse } from "@/lib/api/response";
/**
* Converts a Prisma session to ChatSession format for metrics
*/
function convertToMockChatSession(
ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
},
questions: string[]
): ChatSession {
// Convert questions to mock messages for backward compatibility
const mockMessages = questions.map((q, index) => ({
id: `question-${index}`,
sessionId: ps.id,
timestamp: ps.createdAt,
role: "User",
content: q,
order: index,
createdAt: ps.createdAt,
}));
return {
id: ps.id,
sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null,
transcriptContent: "",
createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.createdAt),
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
summary: ps.summary || undefined,
messages: mockMessages, // Use questions as messages for metrics
userId: undefined,
};
}
interface SessionUser {
email: string;
name?: string;
}
interface SessionData {
user: SessionUser;
}
interface MetricsRequestParams {
companyId: string;
startDate?: string;
endDate?: string;
}
interface MetricsResponse {
metrics: any;
csvUrl: string | null;
company: any;
dateRange: { minDate: string; maxDate: string } | null;
performanceMetrics?: {
cacheHit: boolean;
deduplicationHit: boolean;
executionTime: number;
dataFreshness: string;
};
}
/**
* Generate a cache key for metrics based on company and date range
*/
function generateMetricsCacheKey(params: MetricsRequestParams): string {
const { companyId, startDate, endDate } = params;
return `metrics:${companyId}:${startDate || "all"}:${endDate || "all"}`;
}
/**
* Fetch sessions with performance monitoring and caching
*/
const fetchSessionsWithCache = deduplicators.database.memoize(
async (params: MetricsRequestParams) => {
return PerformanceUtils.measureAsync("metrics-session-fetch", async () => {
const whereClause: {
companyId: string;
startTime?: {
gte: Date;
lte: Date;
};
} = {
companyId: params.companyId,
};
if (params.startDate && params.endDate) {
whereClause.startTime = {
gte: new Date(params.startDate),
lte: new Date(`${params.endDate}T23:59:59.999Z`),
};
}
// Fetch sessions
const sessions = await prisma.session.findMany({
where: whereClause,
select: {
id: true,
companyId: true,
startTime: true,
endTime: true,
createdAt: true,
category: true,
language: true,
country: true,
ipAddress: true,
sentiment: true,
messagesSent: true,
avgResponseTime: true,
escalated: true,
forwardedHr: true,
initialMsg: true,
fullTranscriptUrl: true,
summary: true,
},
});
return sessions;
});
},
{
keyGenerator: (params: MetricsRequestParams) => JSON.stringify(params),
ttl: 2 * 60 * 1000, // 2 minutes
}
);
/**
* Fetch questions for sessions with deduplication
*/
const fetchQuestionsWithDeduplication = deduplicators.database.memoize(
async (sessionIds: string[]) => {
return PerformanceUtils.measureAsync(
"metrics-questions-fetch",
async () => {
const questions = await prisma.sessionQuestion.findMany({
where: { sessionId: { in: sessionIds } },
include: { question: true },
orderBy: { order: "asc" },
});
return questions;
}
);
},
{
keyGenerator: (sessionIds: string[]) =>
`questions:${sessionIds.sort().join(",")}`,
ttl: 5 * 60 * 1000, // 5 minutes
}
);
/**
* Calculate metrics with caching
*/
const calculateMetricsWithCache = async (
chatSessions: ChatSession[],
companyConfig: any,
cacheKey: string
): Promise<{ result: any; fromCache: boolean }> => {
return caches.metrics
.getOrCompute(
cacheKey,
() =>
PerformanceUtils.measureAsync("metrics-calculation", async () => {
const metrics = sessionMetrics(chatSessions, companyConfig);
return {
metrics,
calculatedAt: new Date().toISOString(),
sessionCount: chatSessions.length,
};
}).then(({ result }) => result),
5 * 60 * 1000 // 5 minutes cache
)
.then((cached) => ({
result: cached,
fromCache: caches.metrics.has(cacheKey),
}));
};
/**
* Enhanced GET endpoint with performance optimizations
*/
export const GET = withErrorHandling(async (request: NextRequest) => {
const requestTimer = PerformanceUtils.createTimer("metrics-request-total");
let cacheHit = false;
let deduplicationHit = false;
try {
// Authentication with performance monitoring
const { result: session } = await PerformanceUtils.measureAsync(
"metrics-auth-check",
async () => (await getServerSession(authOptions)) as SessionData | null
);
if (!session?.user) {
performanceMonitor.recordRequest(requestTimer.end(), true);
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
}
// User lookup with caching
const user = await caches.sessions.getOrCompute(
`user:${session.user.email}`,
async () => {
const { result } = await PerformanceUtils.measureAsync(
"metrics-user-lookup",
async () =>
prisma.user.findUnique({
where: { email: session.user.email },
select: {
id: true,
companyId: true,
company: {
select: {
id: true,
name: true,
csvUrl: true,
status: true,
},
},
},
})
);
return result;
},
15 * 60 * 1000 // 15 minutes
);
if (!user) {
performanceMonitor.recordRequest(requestTimer.end(), true);
return NextResponse.json({ error: "No user" }, { status: 401 });
}
// Extract request parameters
const { searchParams } = new URL(request.url);
const startDate = searchParams.get("startDate") || undefined;
const endDate = searchParams.get("endDate") || undefined;
const params: MetricsRequestParams = {
companyId: user.companyId,
startDate,
endDate,
};
const cacheKey = generateMetricsCacheKey(params);
// Try to get complete cached response first
const cachedResponse = await caches.apiResponses.get(
`full-metrics:${cacheKey}`
);
if (cachedResponse) {
cacheHit = true;
const duration = requestTimer.end();
performanceMonitor.recordRequest(duration, false);
return NextResponse.json(
createSuccessResponse({
...cachedResponse,
performanceMetrics: {
cacheHit: true,
deduplicationHit: false,
executionTime: duration,
dataFreshness: "cached",
},
})
);
}
// Fetch sessions with deduplication and monitoring
const sessionResult = await fetchSessionsWithCache(params);
const prismaSessions = sessionResult.result;
// Track if this was a deduplication hit
deduplicationHit = deduplicators.database.getStats().hitRate > 0;
// Fetch questions with deduplication
const sessionIds = prismaSessions.map((s: any) => s.id);
const questionsResult = await fetchQuestionsWithDeduplication(sessionIds);
const sessionQuestions = questionsResult.result;
// Group questions by session with performance monitoring
const { result: questionsBySession } = await PerformanceUtils.measureAsync(
"metrics-questions-grouping",
async () => {
return sessionQuestions.reduce(
(acc, sq) => {
if (!acc[sq.sessionId]) acc[sq.sessionId] = [];
acc[sq.sessionId].push(sq.question.content);
return acc;
},
{} as Record<string, string[]>
);
}
);
// Convert to ChatSession format with monitoring
const { result: chatSessions } = await PerformanceUtils.measureAsync(
"metrics-session-conversion",
async () => {
return prismaSessions.map((ps: any) => {
const questions = questionsBySession[ps.id] || [];
return convertToMockChatSession(ps, questions);
});
}
);
// Calculate metrics with caching
const companyConfigForMetrics = {};
const { result: metricsData, fromCache: metricsFromCache } =
await calculateMetricsWithCache(
chatSessions,
companyConfigForMetrics,
`calc:${cacheKey}`
);
// Calculate date range with monitoring
const { result: dateRange } = await PerformanceUtils.measureAsync(
"metrics-date-range-calc",
async () => {
if (prismaSessions.length === 0) return null;
const dates = prismaSessions
.map((s: any) => new Date(s.startTime))
.sort((a: Date, b: Date) => a.getTime() - b.getTime());
return {
minDate: dates[0].toISOString().split("T")[0],
maxDate: dates[dates.length - 1].toISOString().split("T")[0],
};
}
);
const responseData: MetricsResponse = {
metrics: metricsData.metrics,
csvUrl: user.company.csvUrl,
company: user.company,
dateRange,
performanceMetrics: {
cacheHit: metricsFromCache,
deduplicationHit,
executionTime: 0, // Will be set below
dataFreshness: metricsFromCache ? "cached" : "fresh",
},
};
// Cache the complete response for faster subsequent requests
await caches.apiResponses.set(
`full-metrics:${cacheKey}`,
responseData,
2 * 60 * 1000 // 2 minutes
);
const duration = requestTimer.end();
responseData.performanceMetrics!.executionTime = duration;
performanceMonitor.recordRequest(duration, false);
return NextResponse.json(createSuccessResponse(responseData));
} catch (error) {
const duration = requestTimer.end();
performanceMonitor.recordRequest(duration, true);
throw error; // Re-throw for error handler
}
});
// Export enhanced endpoint as default
export { GET as default };

View File

@ -0,0 +1,302 @@
/**
* Refactored Sessions API Endpoint
*
* This demonstrates how to use the new standardized API architecture
* for consistent error handling, validation, authentication, and response formatting.
*
* BEFORE: Manual auth, inconsistent errors, no validation, mixed response format
* AFTER: Standardized middleware, typed validation, consistent responses, audit logging
*/
import type { Prisma } from "@prisma/client";
import { SessionCategory } from "@prisma/client";
import { z } from "zod";
import {
calculatePaginationMeta,
createAuthenticatedHandler,
createPaginatedResponse,
DatabaseError,
Permission,
ValidationError,
} from "@/lib/api";
import { prisma } from "@/lib/prisma";
import type { ChatSession } from "@/lib/types";
/**
* Input validation schema for session queries
*/
const SessionQuerySchema = z.object({
// Search parameters
searchTerm: z.string().max(100).optional(),
category: z.nativeEnum(SessionCategory).optional(),
language: z.string().min(2).max(5).optional(),
// Date filtering
startDate: z.string().date().optional(),
endDate: z.string().date().optional(),
// Sorting
sortKey: z
.enum([
"startTime",
"category",
"language",
"sentiment",
"messagesSent",
"avgResponseTime",
])
.default("startTime"),
sortOrder: z.enum(["asc", "desc"]).default("desc"),
// Pagination (handled by middleware but included for completeness)
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
});
type SessionQueryInput = z.infer<typeof SessionQuerySchema>;
/**
* Build where clause for session filtering
*/
function buildWhereClause(
companyId: string,
filters: SessionQueryInput
): Prisma.SessionWhereInput {
const whereClause: Prisma.SessionWhereInput = { companyId };
// Search across multiple fields
if (filters.searchTerm?.trim()) {
whereClause.OR = [
{ id: { contains: filters.searchTerm, mode: "insensitive" } },
{ initialMsg: { contains: filters.searchTerm, mode: "insensitive" } },
{ summary: { contains: filters.searchTerm, mode: "insensitive" } },
];
}
// Category filter
if (filters.category) {
whereClause.category = filters.category;
}
// Language filter
if (filters.language) {
whereClause.language = filters.language;
}
// Date range filter
if (filters.startDate || filters.endDate) {
whereClause.startTime = {};
if (filters.startDate) {
whereClause.startTime.gte = new Date(filters.startDate);
}
if (filters.endDate) {
// Make end date inclusive by adding one day
const inclusiveEndDate = new Date(filters.endDate);
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
whereClause.startTime.lt = inclusiveEndDate;
}
}
return whereClause;
}
/**
* Build order by clause for session sorting
*/
function buildOrderByClause(
filters: SessionQueryInput
):
| Prisma.SessionOrderByWithRelationInput
| Prisma.SessionOrderByWithRelationInput[] {
if (filters.sortKey === "startTime") {
return { startTime: filters.sortOrder };
}
// For non-time fields, add secondary sort by startTime
return [{ [filters.sortKey]: filters.sortOrder }, { startTime: "desc" }];
}
/**
* Convert Prisma session to ChatSession format
*/
function convertPrismaSessionToChatSession(ps: {
id: string;
companyId: string;
startTime: Date;
endTime: Date | null;
createdAt: Date;
updatedAt: Date;
category: string | null;
language: string | null;
country: string | null;
ipAddress: string | null;
sentiment: string | null;
messagesSent: number | null;
avgResponseTime: number | null;
escalated: boolean | null;
forwardedHr: boolean | null;
initialMsg: string | null;
fullTranscriptUrl: string | null;
summary: string | null;
}): ChatSession {
return {
id: ps.id,
sessionId: ps.id, // Using ID as sessionId for consistency
companyId: ps.companyId,
startTime: ps.startTime,
endTime: ps.endTime,
createdAt: ps.createdAt,
updatedAt: ps.updatedAt,
userId: null, // Not stored at session level
category: ps.category,
language: ps.language,
country: ps.country,
ipAddress: ps.ipAddress,
sentiment: ps.sentiment,
messagesSent: ps.messagesSent ?? undefined,
avgResponseTime: ps.avgResponseTime,
escalated: ps.escalated ?? undefined,
forwardedHr: ps.forwardedHr ?? undefined,
initialMsg: ps.initialMsg ?? undefined,
fullTranscriptUrl: ps.fullTranscriptUrl,
summary: ps.summary,
transcriptContent: null, // Not included in list view for performance
};
}
/**
* GET /api/dashboard/sessions
*
* Retrieve paginated list of sessions with filtering and sorting capabilities.
*
* Features:
* - Automatic authentication and company access validation
* - Input validation with Zod schemas
* - Consistent error handling and response format
* - Audit logging for security monitoring
* - Rate limiting protection
* - Pagination with metadata
*/
export const GET = createAuthenticatedHandler(
async (context, _, validatedQuery) => {
const filters = validatedQuery as SessionQueryInput;
const { page, limit } = context.pagination!;
try {
// Validate company access (users can only see their company's sessions)
const companyId = context.user!.companyId;
// Build query conditions
const whereClause = buildWhereClause(companyId, filters);
const orderByClause = buildOrderByClause(filters);
// Execute queries in parallel for better performance
const [sessions, totalCount] = await Promise.all([
prisma.session.findMany({
where: whereClause,
orderBy: orderByClause,
skip: (page - 1) * limit,
take: limit,
// Only select needed fields for performance
select: {
id: true,
companyId: true,
startTime: true,
endTime: true,
createdAt: true,
updatedAt: true,
category: true,
language: true,
country: true,
ipAddress: true,
sentiment: true,
messagesSent: true,
avgResponseTime: true,
escalated: true,
forwardedHr: true,
initialMsg: true,
fullTranscriptUrl: true,
summary: true,
},
}),
prisma.session.count({ where: whereClause }),
]);
// Transform data
const transformedSessions: ChatSession[] = sessions.map(
convertPrismaSessionToChatSession
);
// Calculate pagination metadata
const paginationMeta = calculatePaginationMeta(page, limit, totalCount);
// Return paginated response with metadata
return createPaginatedResponse(transformedSessions, paginationMeta);
} catch (error) {
// Database errors are automatically handled by the error system
if (error instanceof Error) {
throw new DatabaseError("Failed to fetch sessions", {
companyId: context.user!.companyId,
filters,
error: error.message,
});
}
throw error;
}
},
{
// Configuration
validateQuery: SessionQuerySchema,
enablePagination: true,
auditLog: true,
rateLimit: {
maxRequests: 60, // 60 requests per window
windowMs: 60 * 1000, // 1 minute window
},
cacheControl: "private, max-age=30", // Cache for 30 seconds
}
);
/*
COMPARISON: Before vs After Refactoring
BEFORE (Original Implementation):
- ❌ Manual session authentication with repetitive code
- ❌ Inconsistent error responses: { error: "...", details: "..." }
- ❌ No input validation - accepts any query parameters
- ❌ No rate limiting protection
- ❌ No audit logging for security monitoring
- ❌ Manual pagination parameter extraction
- ❌ Inconsistent response format: { sessions, totalSessions }
- ❌ Basic error logging without context
- ❌ No company access validation
- ❌ Performance issue: sequential database queries
AFTER (Refactored with New Architecture):
- ✅ Automatic authentication via createAuthenticatedHandler middleware
- ✅ Standardized error responses with proper status codes and request IDs
- ✅ Strong input validation with Zod schemas and type safety
- ✅ Built-in rate limiting (60 req/min) with configurable limits
- ✅ Automatic audit logging for security compliance
- ✅ Automatic pagination handling via middleware
- ✅ Consistent API response format with metadata
- ✅ Comprehensive error handling with proper categorization
- ✅ Automatic company access validation for multi-tenant security
- ✅ Performance optimization: parallel database queries
BENEFITS:
1. **Consistency**: All endpoints follow the same patterns
2. **Security**: Built-in auth, rate limiting, audit logging, company isolation
3. **Maintainability**: Less boilerplate, centralized logic, type safety
4. **Performance**: Optimized queries, caching headers, parallel execution
5. **Developer Experience**: Better error messages, validation, debugging
6. **Scalability**: Standardized patterns that can be applied across all endpoints
MIGRATION STRATEGY:
1. Replace the original route.ts with this refactored version
2. Update any frontend code to expect the new response format
3. Test thoroughly to ensure backward compatibility where needed
4. Repeat this pattern for other endpoints
*/