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