mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:12:09 +01:00
fix: resolved biome errors
This commit is contained in:
17
CLAUDE.md
17
CLAUDE.md
@ -199,3 +199,20 @@ Environment variables are managed through `lib/env.ts` with .env.local file supp
|
||||
- JWT tokens with 24-hour expiration and secure cookie settings
|
||||
- HttpOnly, Secure, SameSite cookies with proper CSP integration
|
||||
- Company isolation and multi-tenant security
|
||||
|
||||
**Code Quality & Linting:**
|
||||
|
||||
- **Biome Integration**: Primary linting and formatting tool
|
||||
- Pre-commit hooks enforce code quality standards
|
||||
- Some security-critical patterns require `biome-ignore` comments
|
||||
- Non-null assertions (`!`) used intentionally in authenticated contexts require ignore comments
|
||||
- Complex functions may need refactoring to meet complexity thresholds (max 15)
|
||||
- Performance classes use static-only patterns which may trigger warnings
|
||||
- **TypeScript Strict Mode**: Comprehensive type checking
|
||||
- Avoid `any` types where possible; use proper type definitions
|
||||
- Optional chaining vs non-null assertions: choose based on security context
|
||||
- In authenticated API handlers, non-null assertions are often safer than optional chaining
|
||||
- **Security vs Linting Balance**:
|
||||
- Security takes precedence over linting rules when they conflict
|
||||
- Document security-critical choices with detailed comments
|
||||
- Use `// biome-ignore` with explanations for intentional rule violations
|
||||
|
||||
@ -11,29 +11,34 @@ This document summarizes the comprehensive documentation audit performed on the
|
||||
The following areas were found to have comprehensive, accurate documentation:
|
||||
|
||||
1. **CSRF Protection** (`docs/CSRF_PROTECTION.md`)
|
||||
|
||||
- Multi-layer protection implementation
|
||||
- Client-side integration guide
|
||||
- tRPC integration details
|
||||
- Comprehensive examples
|
||||
|
||||
2. **Enhanced CSP Implementation** (`docs/security/enhanced-csp.md`)
|
||||
|
||||
- Nonce-based script execution
|
||||
- Environment-specific policies
|
||||
- Violation reporting and monitoring
|
||||
- Testing framework
|
||||
|
||||
3. **Security Headers** (`docs/security-headers.md`)
|
||||
|
||||
- Complete header implementation details
|
||||
- Testing procedures
|
||||
- Compatibility information
|
||||
|
||||
4. **Security Monitoring System** (`docs/security-monitoring.md`)
|
||||
|
||||
- Real-time threat detection
|
||||
- Alert management
|
||||
- API usage examples
|
||||
- Performance considerations
|
||||
|
||||
5. **Migration Guide** (`MIGRATION_GUIDE.md`)
|
||||
|
||||
- Comprehensive v2.0.0 migration procedures
|
||||
- Rollback procedures
|
||||
- Health checks and validation
|
||||
|
||||
@ -50,14 +50,14 @@ function usePlatformSession() {
|
||||
if (sessionData?.user?.isPlatformUser) {
|
||||
setSession({
|
||||
user: {
|
||||
id: sessionData.user.id || '',
|
||||
email: sessionData.user.email || '',
|
||||
id: sessionData.user.id || "",
|
||||
email: sessionData.user.email || "",
|
||||
name: sessionData.user.name,
|
||||
role: sessionData.user.role || '',
|
||||
role: sessionData.user.role || "",
|
||||
companyId: sessionData.user.companyId,
|
||||
isPlatformUser: sessionData.user.isPlatformUser,
|
||||
platformRole: sessionData.user.platformRole,
|
||||
}
|
||||
},
|
||||
});
|
||||
setStatus("authenticated");
|
||||
} else {
|
||||
|
||||
@ -104,11 +104,7 @@ function SystemHealthCard({
|
||||
schedulerStatus,
|
||||
}: {
|
||||
health: { status: string; message: string };
|
||||
schedulerStatus: {
|
||||
csvImport?: boolean;
|
||||
processing?: boolean;
|
||||
batch?: boolean;
|
||||
};
|
||||
schedulerStatus: SchedulerStatus;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
@ -125,25 +121,33 @@ function SystemHealthCard({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>CSV Import Scheduler:</span>
|
||||
<span>Batch Creation:</span>
|
||||
<Badge
|
||||
variant={schedulerStatus?.csvImport ? "default" : "secondary"}
|
||||
variant={
|
||||
schedulerStatus?.createBatchesRunning ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{schedulerStatus?.csvImport ? "Running" : "Stopped"}
|
||||
{schedulerStatus?.createBatchesRunning ? "Running" : "Stopped"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Processing Scheduler:</span>
|
||||
<span>Status Check:</span>
|
||||
<Badge
|
||||
variant={schedulerStatus?.processing ? "default" : "secondary"}
|
||||
variant={
|
||||
schedulerStatus?.checkStatusRunning ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{schedulerStatus?.processing ? "Running" : "Stopped"}
|
||||
{schedulerStatus?.checkStatusRunning ? "Running" : "Stopped"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Batch Scheduler:</span>
|
||||
<Badge variant={schedulerStatus?.batch ? "default" : "secondary"}>
|
||||
{schedulerStatus?.batch ? "Running" : "Stopped"}
|
||||
<span>Result Processing:</span>
|
||||
<Badge
|
||||
variant={
|
||||
schedulerStatus?.processResultsRunning ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{schedulerStatus?.processResultsRunning ? "Running" : "Stopped"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@ -155,7 +159,7 @@ function SystemHealthCard({
|
||||
function CircuitBreakerCard({
|
||||
circuitBreakerStatus,
|
||||
}: {
|
||||
circuitBreakerStatus: Record<string, string> | null;
|
||||
circuitBreakerStatus: Record<string, CircuitBreakerStatus> | null;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
@ -172,10 +176,8 @@ function CircuitBreakerCard({
|
||||
{Object.entries(circuitBreakerStatus).map(([key, status]) => (
|
||||
<div key={key} className="flex justify-between text-sm">
|
||||
<span>{key}:</span>
|
||||
<Badge
|
||||
variant={status === "CLOSED" ? "default" : "destructive"}
|
||||
>
|
||||
{status as string}
|
||||
<Badge variant={!status.isOpen ? "default" : "destructive"}>
|
||||
{status.isOpen ? "OPEN" : "CLOSED"}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
@ -412,13 +414,8 @@ export default function BatchMonitoringDashboard() {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<SystemHealthCard
|
||||
health={health}
|
||||
schedulerStatus={schedulerStatus as any}
|
||||
/>
|
||||
<CircuitBreakerCard
|
||||
circuitBreakerStatus={circuitBreakerStatus as any}
|
||||
/>
|
||||
<SystemHealthCard health={health} schedulerStatus={schedulerStatus} />
|
||||
<CircuitBreakerCard circuitBreakerStatus={circuitBreakerStatus} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -280,6 +280,84 @@ function addCORSHeaders(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process authentication and authorization
|
||||
*/
|
||||
async function processAuthAndAuthz(
|
||||
context: APIContext,
|
||||
options: APIHandlerOptions
|
||||
): Promise<void> {
|
||||
if (options.requireAuth) {
|
||||
await validateAuthentication(context);
|
||||
}
|
||||
|
||||
if (options.requiredRole || options.requirePlatformAccess) {
|
||||
await validateAuthorization(context, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process input validation
|
||||
*/
|
||||
async function processValidation(
|
||||
request: NextRequest,
|
||||
options: APIHandlerOptions
|
||||
): Promise<{ validatedData: unknown; validatedQuery: unknown }> {
|
||||
let validatedData: unknown;
|
||||
if (options.validateInput && request.method !== "GET") {
|
||||
validatedData = await validateInput(request, options.validateInput);
|
||||
}
|
||||
|
||||
let validatedQuery: unknown;
|
||||
if (options.validateQuery) {
|
||||
validatedQuery = validateQuery(request, options.validateQuery);
|
||||
}
|
||||
|
||||
return { validatedData, validatedQuery };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure response
|
||||
*/
|
||||
function createAPIResponse<T>(
|
||||
result: T,
|
||||
context: APIContext,
|
||||
options: APIHandlerOptions
|
||||
): NextResponse {
|
||||
const response = NextResponse.json(
|
||||
createSuccessResponse(result, { requestId: context.requestId })
|
||||
);
|
||||
|
||||
response.headers.set("X-Request-ID", context.requestId);
|
||||
|
||||
if (options.cacheControl) {
|
||||
response.headers.set("Cache-Control", options.cacheControl);
|
||||
}
|
||||
|
||||
addCORSHeaders(response, options);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle request execution with audit logging
|
||||
*/
|
||||
async function executeWithAudit<T>(
|
||||
handler: APIHandler<T>,
|
||||
context: APIContext,
|
||||
validatedData: unknown,
|
||||
validatedQuery: unknown,
|
||||
request: NextRequest,
|
||||
options: APIHandlerOptions
|
||||
): Promise<T> {
|
||||
const result = await handler(context, validatedData, validatedQuery);
|
||||
|
||||
if (options.auditLog) {
|
||||
await logAPIAccess(context, "success", request.url);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main API handler factory
|
||||
*/
|
||||
@ -291,64 +369,32 @@ export function createAPIHandler<T = unknown>(
|
||||
let context: APIContext | undefined;
|
||||
|
||||
try {
|
||||
// 1. Create request context
|
||||
context = await createAPIContext(request);
|
||||
|
||||
// 2. Apply rate limiting
|
||||
if (options.rateLimit) {
|
||||
await applyRateLimit(context, options.rateLimit);
|
||||
}
|
||||
|
||||
// 3. Validate authentication
|
||||
if (options.requireAuth) {
|
||||
await validateAuthentication(context);
|
||||
}
|
||||
await processAuthAndAuthz(context, options);
|
||||
|
||||
// 4. Validate authorization
|
||||
if (options.requiredRole || options.requirePlatformAccess) {
|
||||
await validateAuthorization(context, options);
|
||||
}
|
||||
|
||||
// 5. Validate input
|
||||
let validatedData;
|
||||
if (options.validateInput && request.method !== "GET") {
|
||||
validatedData = await validateInput(request, options.validateInput);
|
||||
}
|
||||
|
||||
// 6. Validate query parameters
|
||||
let validatedQuery;
|
||||
if (options.validateQuery) {
|
||||
validatedQuery = validateQuery(request, options.validateQuery);
|
||||
}
|
||||
|
||||
// 7. Execute handler
|
||||
const result = await handler(context, validatedData, validatedQuery);
|
||||
|
||||
// 8. Audit logging
|
||||
if (options.auditLog) {
|
||||
await logAPIAccess(context, "success", request.url);
|
||||
}
|
||||
|
||||
// 9. Create response
|
||||
const response = NextResponse.json(
|
||||
createSuccessResponse(result, { requestId: context.requestId })
|
||||
const { validatedData, validatedQuery } = await processValidation(
|
||||
request,
|
||||
options
|
||||
);
|
||||
|
||||
// 10. Add headers
|
||||
response.headers.set("X-Request-ID", context.requestId);
|
||||
const result = await executeWithAudit(
|
||||
handler,
|
||||
context,
|
||||
validatedData,
|
||||
validatedQuery,
|
||||
request,
|
||||
options
|
||||
);
|
||||
|
||||
if (options.cacheControl) {
|
||||
response.headers.set("Cache-Control", options.cacheControl);
|
||||
}
|
||||
|
||||
addCORSHeaders(response, options);
|
||||
|
||||
return response;
|
||||
return createAPIResponse(result, context, options);
|
||||
} catch (error) {
|
||||
// Handle errors consistently
|
||||
const requestId = context?.requestId || crypto.randomUUID();
|
||||
|
||||
// Log failed requests
|
||||
if (options.auditLog && context) {
|
||||
await logAPIAccess(context, "error", request.url, error as Error);
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ export function stopOptimizedBatchScheduler(): void {
|
||||
{ task: retryFailedTask, name: "retryFailedTask" },
|
||||
];
|
||||
|
||||
for (const { task, name } of tasks) {
|
||||
for (const { task } of tasks) {
|
||||
if (task) {
|
||||
task.stop();
|
||||
task.destroy();
|
||||
|
||||
@ -169,6 +169,10 @@ const ConfigSchema = z.object({
|
||||
|
||||
export type AppConfig = z.infer<typeof ConfigSchema>;
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration provider class
|
||||
*/
|
||||
@ -230,8 +234,8 @@ class ConfigProvider {
|
||||
/**
|
||||
* Get environment-specific configuration
|
||||
*/
|
||||
forEnvironment(env: Environment): Partial<AppConfig> {
|
||||
const overrides: Record<Environment, any> = {
|
||||
forEnvironment(env: Environment): DeepPartial<AppConfig> {
|
||||
const overrides: Record<Environment, DeepPartial<AppConfig>> = {
|
||||
development: {
|
||||
app: {
|
||||
logLevel: "debug",
|
||||
@ -291,28 +295,31 @@ class ConfigProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract configuration from environment variables
|
||||
* Extract app configuration from environment
|
||||
*/
|
||||
private extractFromEnvironment(): Partial<AppConfig> {
|
||||
const env = process.env;
|
||||
const environment = (env.NODE_ENV as Environment) || "development";
|
||||
|
||||
private extractAppConfig(env: NodeJS.ProcessEnv, environment: Environment) {
|
||||
return {
|
||||
app: {
|
||||
name: env.APP_NAME || "LiveDash",
|
||||
version: env.APP_VERSION || "1.0.0",
|
||||
environment,
|
||||
baseUrl: env.NEXTAUTH_URL || "http://localhost:3000",
|
||||
port: Number.parseInt(env.PORT || "3000", 10),
|
||||
logLevel: (env.LOG_LEVEL as any) || "info",
|
||||
logLevel:
|
||||
(env.LOG_LEVEL as "debug" | "info" | "warn" | "error") || "info",
|
||||
features: {
|
||||
enableMetrics: env.ENABLE_METRICS !== "false",
|
||||
enableAnalytics: env.ENABLE_ANALYTICS !== "false",
|
||||
enableCaching: env.ENABLE_CACHING !== "false",
|
||||
enableCompression: env.ENABLE_COMPRESSION !== "false",
|
||||
},
|
||||
},
|
||||
database: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract database configuration from environment
|
||||
*/
|
||||
private extractDatabaseConfig(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
url: env.DATABASE_URL || "",
|
||||
directUrl: env.DATABASE_URL_DIRECT,
|
||||
maxConnections: Number.parseInt(env.DB_MAX_CONNECTIONS || "10", 10),
|
||||
@ -323,8 +330,14 @@ class ConfigProvider {
|
||||
queryTimeout: Number.parseInt(env.DB_QUERY_TIMEOUT || "60000", 10),
|
||||
retryAttempts: Number.parseInt(env.DB_RETRY_ATTEMPTS || "3", 10),
|
||||
retryDelay: Number.parseInt(env.DB_RETRY_DELAY || "1000", 10),
|
||||
},
|
||||
auth: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract auth configuration from environment
|
||||
*/
|
||||
private extractAuthConfig(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
secret: env.NEXTAUTH_SECRET || "",
|
||||
url: env.NEXTAUTH_URL || "http://localhost:3000",
|
||||
sessionMaxAge: Number.parseInt(env.AUTH_SESSION_MAX_AGE || "86400", 10),
|
||||
@ -333,8 +346,14 @@ class ConfigProvider {
|
||||
github: env.AUTH_GITHUB_ENABLED === "true",
|
||||
google: env.AUTH_GOOGLE_ENABLED === "true",
|
||||
},
|
||||
},
|
||||
security: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract security configuration from environment
|
||||
*/
|
||||
private extractSecurityConfig(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
csp: {
|
||||
enabled: env.CSP_ENABLED !== "false",
|
||||
reportUri: env.CSP_REPORT_URI,
|
||||
@ -347,18 +366,21 @@ class ConfigProvider {
|
||||
rateLimit: {
|
||||
enabled: env.RATE_LIMIT_ENABLED !== "false",
|
||||
windowMs: Number.parseInt(env.RATE_LIMIT_WINDOW_MS || "900000", 10),
|
||||
maxRequests: Number.parseInt(
|
||||
env.RATE_LIMIT_MAX_REQUESTS || "100",
|
||||
10
|
||||
),
|
||||
maxRequests: Number.parseInt(env.RATE_LIMIT_MAX_REQUESTS || "100", 10),
|
||||
},
|
||||
audit: {
|
||||
enabled: env.AUDIT_ENABLED !== "false",
|
||||
retentionDays: Number.parseInt(env.AUDIT_RETENTION_DAYS || "90", 10),
|
||||
bufferSize: Number.parseInt(env.AUDIT_BUFFER_SIZE || "1000", 10),
|
||||
},
|
||||
},
|
||||
openai: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract OpenAI configuration from environment
|
||||
*/
|
||||
private extractOpenAIConfig(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
apiKey: env.OPENAI_API_KEY || "",
|
||||
organization: env.OPENAI_ORGANIZATION,
|
||||
mockMode: env.OPENAI_MOCK_MODE === "true",
|
||||
@ -380,8 +402,14 @@ class ConfigProvider {
|
||||
10
|
||||
),
|
||||
},
|
||||
},
|
||||
scheduler: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract scheduler configuration from environment
|
||||
*/
|
||||
private extractSchedulerConfig(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
enabled: env.SCHEDULER_ENABLED !== "false",
|
||||
csvImport: {
|
||||
enabled: env.CSV_IMPORT_SCHEDULER_ENABLED !== "false",
|
||||
@ -405,8 +433,14 @@ class ConfigProvider {
|
||||
statusInterval: env.BATCH_STATUS_INTERVAL || "*/2 * * * *",
|
||||
resultInterval: env.BATCH_RESULT_INTERVAL || "*/1 * * * *",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract email configuration from environment
|
||||
*/
|
||||
private extractEmailConfig(env: NodeJS.ProcessEnv) {
|
||||
return {
|
||||
enabled: env.EMAIL_ENABLED === "true",
|
||||
smtp: {
|
||||
host: env.SMTP_HOST,
|
||||
@ -418,13 +452,29 @@ class ConfigProvider {
|
||||
from: env.EMAIL_FROM || "noreply@livedash.com",
|
||||
templates: {
|
||||
passwordReset: env.EMAIL_TEMPLATE_PASSWORD_RESET || "password-reset",
|
||||
userInvitation:
|
||||
env.EMAIL_TEMPLATE_USER_INVITATION || "user-invitation",
|
||||
},
|
||||
userInvitation: env.EMAIL_TEMPLATE_USER_INVITATION || "user-invitation",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract configuration from environment variables
|
||||
*/
|
||||
private extractFromEnvironment(): Partial<AppConfig> {
|
||||
const env = process.env;
|
||||
const environment = (env.NODE_ENV as Environment) || "development";
|
||||
|
||||
return {
|
||||
app: this.extractAppConfig(env, environment),
|
||||
database: this.extractDatabaseConfig(env),
|
||||
auth: this.extractAuthConfig(env),
|
||||
security: this.extractSecurityConfig(env),
|
||||
openai: this.extractOpenAIConfig(env),
|
||||
scheduler: this.extractSchedulerConfig(env),
|
||||
email: this.extractEmailConfig(env),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log configuration status without sensitive information
|
||||
*/
|
||||
|
||||
@ -191,7 +191,7 @@ export const DynamicAuditLogsPanel = createDynamicComponent(
|
||||
|
||||
// React wrapper for React.lazy with Suspense
|
||||
export function createLazyComponent<
|
||||
T extends Record<string, any> = Record<string, any>,
|
||||
T extends Record<string, unknown> = Record<string, unknown>,
|
||||
>(
|
||||
importFunc: () => Promise<{ default: ComponentType<T> }>,
|
||||
fallback: ComponentType = LoadingSpinner
|
||||
|
||||
@ -15,6 +15,26 @@ import {
|
||||
type MockResponseType,
|
||||
} from "./openai-responses";
|
||||
|
||||
interface ChatCompletionParams {
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface BatchCreateParams {
|
||||
input_file_id: string;
|
||||
endpoint: string;
|
||||
completion_window: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface FileCreateParams {
|
||||
file: string; // File content as string for mock purposes
|
||||
purpose: string;
|
||||
}
|
||||
|
||||
interface MockOpenAIConfig {
|
||||
enabled: boolean;
|
||||
baseDelay: number; // Base delay in ms to simulate API latency
|
||||
@ -115,12 +135,9 @@ class OpenAIMockServer {
|
||||
/**
|
||||
* Mock chat completions endpoint
|
||||
*/
|
||||
async mockChatCompletion(request: {
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
}): Promise<MockChatCompletion> {
|
||||
async mockChatCompletion(
|
||||
request: ChatCompletionParams
|
||||
): Promise<MockChatCompletion> {
|
||||
this.requestCount++;
|
||||
|
||||
await this.simulateDelay();
|
||||
@ -172,12 +189,9 @@ class OpenAIMockServer {
|
||||
/**
|
||||
* Mock batch creation endpoint
|
||||
*/
|
||||
async mockCreateBatch(request: {
|
||||
input_file_id: string;
|
||||
endpoint: string;
|
||||
completion_window: string;
|
||||
metadata?: Record<string, string>;
|
||||
}): Promise<MockBatchResponse> {
|
||||
async mockCreateBatch(
|
||||
request: BatchCreateParams
|
||||
): Promise<MockBatchResponse> {
|
||||
await this.simulateDelay();
|
||||
|
||||
if (this.shouldSimulateError()) {
|
||||
@ -214,10 +228,7 @@ class OpenAIMockServer {
|
||||
/**
|
||||
* Mock file upload endpoint
|
||||
*/
|
||||
async mockUploadFile(request: {
|
||||
file: string; // File content
|
||||
purpose: string;
|
||||
}): Promise<{
|
||||
async mockUploadFile(request: FileCreateParams): Promise<{
|
||||
id: string;
|
||||
object: string;
|
||||
purpose: string;
|
||||
@ -364,23 +375,42 @@ export const openAIMock = new OpenAIMockServer();
|
||||
/**
|
||||
* Drop-in replacement for OpenAI client that uses mocks when enabled
|
||||
*/
|
||||
export class MockOpenAIClient {
|
||||
private realClient: unknown;
|
||||
interface OpenAIClient {
|
||||
chat: {
|
||||
completions: {
|
||||
create: (params: ChatCompletionParams) => Promise<MockChatCompletion>;
|
||||
};
|
||||
};
|
||||
batches: {
|
||||
create: (params: BatchCreateParams) => Promise<MockBatchResponse>;
|
||||
retrieve: (batchId: string) => Promise<MockBatchResponse>;
|
||||
};
|
||||
files: {
|
||||
create: (params: FileCreateParams) => Promise<{
|
||||
id: string;
|
||||
object: string;
|
||||
purpose: string;
|
||||
filename: string;
|
||||
}>;
|
||||
content: (fileId: string) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
constructor(realClient: unknown) {
|
||||
export class MockOpenAIClient {
|
||||
private realClient: OpenAIClient;
|
||||
|
||||
constructor(realClient: OpenAIClient) {
|
||||
this.realClient = realClient;
|
||||
}
|
||||
|
||||
get chat() {
|
||||
return {
|
||||
completions: {
|
||||
create: async (params: any) => {
|
||||
create: async (params: ChatCompletionParams) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockChatCompletion(params as any);
|
||||
return openAIMock.mockChatCompletion(params);
|
||||
}
|
||||
return (this.realClient as any).chat.completions.create(
|
||||
params as any
|
||||
);
|
||||
return this.realClient.chat.completions.create(params);
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -388,34 +418,34 @@ export class MockOpenAIClient {
|
||||
|
||||
get batches() {
|
||||
return {
|
||||
create: async (params: any) => {
|
||||
create: async (params: BatchCreateParams) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockCreateBatch(params as any);
|
||||
return openAIMock.mockCreateBatch(params);
|
||||
}
|
||||
return (this.realClient as any).batches.create(params as any);
|
||||
return this.realClient.batches.create(params);
|
||||
},
|
||||
retrieve: async (batchId: string) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockGetBatch(batchId);
|
||||
}
|
||||
return (this.realClient as any).batches.retrieve(batchId);
|
||||
return this.realClient.batches.retrieve(batchId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get files() {
|
||||
return {
|
||||
create: async (params: any) => {
|
||||
create: async (params: FileCreateParams) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockUploadFile(params);
|
||||
}
|
||||
return (this.realClient as any).files.create(params);
|
||||
return this.realClient.files.create(params);
|
||||
},
|
||||
content: async (fileId: string) => {
|
||||
if (openAIMock.isEnabled()) {
|
||||
return openAIMock.mockGetFileContent(fileId);
|
||||
}
|
||||
return (this.realClient as any).files.content(fileId);
|
||||
return this.realClient.files.content(fileId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -181,7 +181,8 @@ class PerformanceMonitor {
|
||||
// Placeholder for analytics integration
|
||||
// You could send this to Google Analytics, Vercel Analytics, etc.
|
||||
if (typeof window !== "undefined" && "gtag" in window) {
|
||||
(window as any).gtag("event", "core_web_vital", {
|
||||
const gtag = (window as { gtag?: (...args: unknown[]) => void }).gtag;
|
||||
gtag?.("event", "core_web_vital", {
|
||||
name: metricName,
|
||||
value: Math.round(value),
|
||||
metric_rating: this.getRating(metricName, value),
|
||||
|
||||
@ -169,7 +169,7 @@ export class PerformanceCache<K extends {} = string, V = unknown> {
|
||||
/**
|
||||
* Memoize a function with caching
|
||||
*/
|
||||
memoize<Args extends any[], Return extends V>(
|
||||
memoize<Args extends unknown[], Return extends V>(
|
||||
fn: (...args: Args) => Promise<Return> | Return,
|
||||
keyGenerator?: (...args: Args) => K,
|
||||
ttl?: number
|
||||
@ -421,7 +421,7 @@ export class CacheUtils {
|
||||
/**
|
||||
* Cache the result of an async function
|
||||
*/
|
||||
static cached<T extends any[], R>(
|
||||
static cached<T extends unknown[], R>(
|
||||
cacheName: string,
|
||||
fn: (...args: T) => Promise<R>,
|
||||
options: CacheOptions & {
|
||||
|
||||
@ -155,12 +155,12 @@ export class RequestDeduplicator {
|
||||
}> = [];
|
||||
|
||||
// Create the main promise
|
||||
const promise = new Promise<T>(async (resolve, reject) => {
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
resolvers.push({ resolve, reject });
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
|
||||
// Execute the async function
|
||||
fn()
|
||||
.then((result) => {
|
||||
// Cache the result
|
||||
if (options.ttl && options.ttl > 0) {
|
||||
this.results.set(key, {
|
||||
@ -172,17 +172,19 @@ export class RequestDeduplicator {
|
||||
|
||||
// Resolve all waiting promises
|
||||
resolvers.forEach(({ resolve: res }) => res(result));
|
||||
} catch (error) {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.stats.errors++;
|
||||
|
||||
// Reject all waiting promises
|
||||
const errorToReject =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
resolvers.forEach(({ reject: rej }) => rej(errorToReject));
|
||||
} finally {
|
||||
})
|
||||
.finally(() => {
|
||||
// Clean up pending request
|
||||
this.pendingRequests.delete(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set up timeout if specified
|
||||
|
||||
@ -167,7 +167,11 @@ function startMonitoringIfEnabled(enabled?: boolean): void {
|
||||
/**
|
||||
* Helper function to record request metrics if enabled
|
||||
*/
|
||||
function recordRequestIfEnabled(timer: ReturnType<typeof PerformanceUtils.createTimer>, isError: boolean, enabled?: boolean): void {
|
||||
function recordRequestIfEnabled(
|
||||
timer: ReturnType<typeof PerformanceUtils.createTimer>,
|
||||
isError: boolean,
|
||||
enabled?: boolean
|
||||
): void {
|
||||
if (enabled) {
|
||||
performanceMonitor.recordRequest(timer.end(), isError);
|
||||
}
|
||||
@ -223,11 +227,9 @@ async function executeWithCacheOrDeduplication(
|
||||
opts.deduplication?.deduplicatorName as keyof typeof deduplicators
|
||||
] || deduplicators.api;
|
||||
|
||||
return deduplicator.execute(
|
||||
cacheKey,
|
||||
() => originalHandler(req),
|
||||
{ ttl: opts.deduplication?.ttl }
|
||||
);
|
||||
return deduplicator.execute(cacheKey, () => originalHandler(req), {
|
||||
ttl: opts.deduplication?.ttl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,7 +249,12 @@ export function enhanceAPIRoute(
|
||||
|
||||
try {
|
||||
startMonitoringIfEnabled(opts.monitoring?.enabled);
|
||||
const response = await executeRequestWithOptimizations(req, opts, routeName, originalHandler);
|
||||
const response = await executeRequestWithOptimizations(
|
||||
req,
|
||||
opts,
|
||||
routeName,
|
||||
originalHandler
|
||||
);
|
||||
recordRequestIfEnabled(timer, false, opts.monitoring?.recordRequests);
|
||||
return response;
|
||||
} catch (error) {
|
||||
@ -263,8 +270,10 @@ export function enhanceAPIRoute(
|
||||
export function PerformanceEnhanced(
|
||||
options: PerformanceIntegrationOptions = {}
|
||||
) {
|
||||
return <T extends new (...args: any[]) => {}>(constructor: T) =>
|
||||
class extends constructor {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Required for mixin class pattern - TypeScript requires any[] for constructor parameters in mixins
|
||||
return <T extends new (...args: any[]) => {}>(Constructor: T) =>
|
||||
class extends Constructor {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Required for mixin class pattern - TypeScript requires any[] for constructor parameters in mixins
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
||||
@ -279,7 +288,7 @@ export function PerformanceEnhanced(
|
||||
if (typeof originalMethod === "function") {
|
||||
(this as Record<string, unknown>)[methodName] =
|
||||
enhanceServiceMethod(
|
||||
`${constructor.name}.${methodName}`,
|
||||
`${Constructor.name}.${methodName}`,
|
||||
originalMethod.bind(this),
|
||||
options
|
||||
);
|
||||
|
||||
@ -777,9 +777,8 @@ export class PerformanceUtils {
|
||||
}
|
||||
|
||||
descriptor.value = async function (...args: unknown[]) {
|
||||
const { result } = await PerformanceUtils.measureAsync(
|
||||
metricName,
|
||||
() => originalMethod.apply(this, args)
|
||||
const { result } = await PerformanceUtils.measureAsync(metricName, () =>
|
||||
originalMethod.apply(this, args)
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
18
package.json
18
package.json
@ -8,14 +8,18 @@
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"dev": "pnpm exec tsx server.ts",
|
||||
"dev:next-only": "next dev --turbopack",
|
||||
"format": "npx prettier --write .",
|
||||
"format:check": "npx prettier --check .",
|
||||
"format": "pnpm format:prettier && pnpm format:biome",
|
||||
"format:check": "pnpm format:check-prettier && pnpm format:check-biome",
|
||||
"format:biome": "biome format --write",
|
||||
"format:check-biome": "biome format",
|
||||
"format:prettier": "npx prettier --write .",
|
||||
"format:check-prettier": "npx prettier --check .",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "npx eslint --fix",
|
||||
"biome:check": "biome check .",
|
||||
"biome:fix": "biome check --write .",
|
||||
"biome:format": "biome format --write .",
|
||||
"biome:lint": "biome lint .",
|
||||
"biome:check": "biome check",
|
||||
"biome:fix": "biome check --write",
|
||||
"biome:format": "biome format --write",
|
||||
"biome:lint": "biome lint",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:seed": "pnpm exec tsx prisma/seed.ts",
|
||||
@ -37,6 +41,8 @@
|
||||
"test:csp:full": "pnpm test:csp && pnpm test:csp:validate && pnpm test:vitest tests/unit/enhanced-csp.test.ts tests/integration/csp-middleware.test.ts tests/integration/csp-report-endpoint.test.ts",
|
||||
"lint:md": "markdownlint-cli2 \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
||||
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"",
|
||||
"lint:ci-warning": "biome ci --diagnostic-level=warn",
|
||||
"lint:ci-error": "biome ci --diagnostic-level=error",
|
||||
"migration:backup": "pnpm exec tsx scripts/migration/backup-database.ts full",
|
||||
"migration:backup:schema": "pnpm exec tsx scripts/migration/backup-database.ts schema",
|
||||
"migration:backup:data": "pnpm exec tsx scripts/migration/backup-database.ts data",
|
||||
|
||||
10001
pnpm-lock.yaml
generated
10001
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -450,7 +450,7 @@ Examples:
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
runCommand()
|
||||
.then((result) => {
|
||||
|
||||
@ -517,7 +517,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
runTests()
|
||||
.then((result) => {
|
||||
|
||||
Reference in New Issue
Block a user