feat: Implement comprehensive validation error handling

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 19:00:28 +00:00
parent 882959bce6
commit d29e873e14
4 changed files with 290 additions and 105 deletions

View File

@@ -247,28 +247,68 @@ export function SubmissionReviewManager({
}
// Run validation on all selected items
const validationResultsMap = await validateMultipleItems(
selectedItems.map(item => ({
item_type: item.item_type,
item_data: item.item_data,
id: item.id
}))
);
let validationResultsMap: Map<string, any>;
setValidationResults(validationResultsMap);
// Check for blocking errors
const itemsWithBlockingErrors = selectedItems.filter(item => {
const result = validationResultsMap.get(item.id);
return result && result.blockingErrors.length > 0;
});
// CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings
if (itemsWithBlockingErrors.length > 0) {
setHasBlockingErrors(true);
setShowValidationBlockerDialog(true);
dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } });
return; // Block approval
try {
validationResultsMap = await validateMultipleItems(
selectedItems.map(item => ({
item_type: item.item_type,
item_data: item.item_data,
id: item.id
}))
);
setValidationResults(validationResultsMap);
// Check for blocking errors
const itemsWithBlockingErrors = selectedItems.filter(item => {
const result = validationResultsMap.get(item.id);
return result && result.blockingErrors.length > 0;
});
// CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings
if (itemsWithBlockingErrors.length > 0) {
// Log which items have blocking errors
itemsWithBlockingErrors.forEach(item => {
const result = validationResultsMap.get(item.id);
logger.error('Blocking validation errors prevent approval', {
submissionId,
itemId: item.id,
itemType: item.item_type,
errors: result?.blockingErrors
});
});
setHasBlockingErrors(true);
setShowValidationBlockerDialog(true);
dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } });
return; // Block approval
}
} catch (error) {
// Validation itself failed (network error, bug, etc.)
const errorId = handleError(error, {
action: 'Validation System Error',
userId: user?.id,
metadata: {
submissionId,
selectedItemCount: selectedItems.length,
itemTypes: selectedItems.map(i => i.item_type)
}
});
toast({
title: 'Validation System Error',
description: (
<div className="space-y-2">
<p>Unable to validate submission. Please try again.</p>
<p className="text-xs font-mono">Ref: {errorId.slice(0, 8)}</p>
</div>
),
variant: 'destructive'
});
dispatch({ type: 'ERROR', payload: { error: 'Validation system error' } });
return;
}
// Check for warnings

View File

@@ -1,4 +1,5 @@
import { AlertCircle } from 'lucide-react';
import { useState } from 'react';
import { AlertCircle, ChevronDown } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
@@ -9,6 +10,9 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ValidationError } from '@/lib/entityValidationSchemas';
interface ValidationBlockerDialogProps {
@@ -24,9 +28,11 @@ export function ValidationBlockerDialog({
blockingErrors,
itemNames,
}: ValidationBlockerDialogProps) {
const [showDetails, setShowDetails] = useState(false);
return (
<AlertDialog open={open} onOpenChange={onClose}>
<AlertDialogContent>
<AlertDialogContent className="max-w-2xl">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="w-5 h-5" />
@@ -34,28 +40,51 @@ export function ValidationBlockerDialog({
</AlertDialogTitle>
<AlertDialogDescription>
The following items have blocking validation errors that MUST be fixed before approval.
These items cannot be approved until the errors are resolved. Please edit or reject them.
Edit the items to fix the errors, or reject them.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-3 my-4">
{itemNames.map((name, index) => (
<div key={index} className="space-y-2">
<div className="font-medium text-sm">{name}</div>
<Alert variant="destructive">
<AlertDescription className="space-y-1">
{blockingErrors
.filter((_, i) => i === index || itemNames.length === 1)
.map((error, errIndex) => (
{itemNames.map((name, index) => {
const itemErrors = blockingErrors.filter((_, i) =>
itemNames.length === 1 || i === index
);
return (
<div key={index} className="space-y-2">
<div className="font-medium text-sm flex items-center justify-between">
<span>{name}</span>
<Badge variant="destructive">
{itemErrors.length} error{itemErrors.length > 1 ? 's' : ''}
</Badge>
</div>
<Alert variant="destructive">
<AlertDescription className="space-y-1">
{itemErrors.map((error, errIndex) => (
<div key={errIndex} className="text-sm">
<span className="font-medium">{error.field}:</span> {error.message}
</div>
))}
</AlertDescription>
</Alert>
</div>
))}
</AlertDescription>
</Alert>
</div>
);
})}
</div>
<Collapsible open={showDetails} onOpenChange={setShowDetails}>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-full">
{showDetails ? 'Hide' : 'Show'} Technical Details
<ChevronDown className={`ml-2 h-4 w-4 transition-transform ${showDetails ? 'rotate-180' : ''}`} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-2">
<div className="bg-muted p-3 rounded text-xs font-mono max-h-60 overflow-auto">
<pre>{JSON.stringify(blockingErrors, null, 2)}</pre>
</div>
</CollapsibleContent>
</Collapsible>
<AlertDialogFooter>
<AlertDialogAction onClick={onClose}>

View File

@@ -171,42 +171,93 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
});
// Run validation on all items
const validationResults = await validateMultipleItems(itemsWithData);
// Check for blocking errors
const itemsWithBlockingErrors = itemsWithData.filter(item => {
const result = validationResults.get(item.id);
return result && result.blockingErrors.length > 0;
});
// CRITICAL: Block approval if any item has blocking errors
if (itemsWithBlockingErrors.length > 0) {
const errorDetails = itemsWithBlockingErrors.map(item => {
try {
const validationResults = await validateMultipleItems(itemsWithData);
// Check for blocking errors
const itemsWithBlockingErrors = itemsWithData.filter(item => {
const result = validationResults.get(item.id);
return `${item.item_type}: ${result?.blockingErrors[0]?.message || 'Unknown error'}`;
}).join(', ');
toast({
title: 'Cannot Approve - Validation Errors',
description: `${itemsWithBlockingErrors.length} item(s) have blocking errors that must be fixed first. ${errorDetails}`,
variant: 'destructive',
return result && result.blockingErrors.length > 0;
});
// Return early - do NOT proceed with approval
return;
}
// Check for warnings (optional - can proceed but inform user)
const itemsWithWarnings = itemsWithData.filter(item => {
const result = validationResults.get(item.id);
return result && result.warnings.length > 0;
});
if (itemsWithWarnings.length > 0) {
logger.info('Approval proceeding with warnings', {
submissionId: item.id,
warningCount: itemsWithWarnings.length
// CRITICAL: Block approval if any item has blocking errors
if (itemsWithBlockingErrors.length > 0) {
// Log detailed blocking errors
itemsWithBlockingErrors.forEach(item => {
const result = validationResults.get(item.id);
logger.error('Validation blocking approval', {
submissionId: item.id,
itemId: item.id,
itemType: item.item_type,
blockingErrors: result?.blockingErrors
});
});
const errorDetails = itemsWithBlockingErrors.map(item => {
const result = validationResults.get(item.id);
const itemName = (item.item_data as any)?.name || item.item_type;
const errors = result?.blockingErrors.map(e => `${e.field}: ${e.message}`).join(', ');
return `${itemName} - ${errors}`;
}).join('; ');
throw new Error(`Validation failed: ${errorDetails}`);
}
// Check for warnings (optional - can proceed but inform user)
const itemsWithWarnings = itemsWithData.filter(item => {
const result = validationResults.get(item.id);
return result && result.warnings.length > 0;
});
if (itemsWithWarnings.length > 0) {
logger.info('Approval proceeding with warnings', {
submissionId: item.id,
warningCount: itemsWithWarnings.length
});
}
} catch (error) {
// Check if this is a validation error or system error
if (getErrorMessage(error).includes('Validation failed:')) {
// This is expected - validation rules preventing approval
handleError(error, {
action: 'Validation Blocked Approval',
userId: user?.id,
metadata: {
submissionId: item.id,
submissionType: item.submission_type,
selectedItemCount: itemsWithData.length
}
});
toast({
title: 'Cannot Approve - Validation Errors',
description: getErrorMessage(error),
variant: 'destructive',
});
// Return early - do NOT proceed with approval
return;
} else {
// Unexpected validation system error
const errorId = handleError(error, {
action: 'Validation System Failure',
userId: user?.id,
metadata: {
submissionId: item.id,
submissionType: item.submission_type,
phase: 'validation'
}
});
toast({
title: 'Validation System Error',
description: `Unable to validate submission (ref: ${errorId.slice(0, 8)})`,
variant: 'destructive',
});
// Return early - do NOT proceed with approval
return;
}
}
}

View File

@@ -1,5 +1,7 @@
import { z } from 'zod';
import { supabase } from '@/lib/supabaseClient';
import { handleNonCriticalError, getErrorMessage } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
// ============================================
// CENTRALIZED VALIDATION SCHEMAS
@@ -452,35 +454,71 @@ export async function validateEntityData(
entityType: keyof typeof entitySchemas,
data: unknown
): Promise<ValidationResult> {
const schema = entitySchemas[entityType];
if (!schema) {
return {
isValid: false,
blockingErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }],
warnings: [],
suggestions: [],
allErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }],
};
}
const result = schema.safeParse(data);
const blockingErrors: ValidationError[] = [];
const warnings: ValidationError[] = [];
const suggestions: ValidationError[] = [];
// Process Zod errors
if (!result.success) {
const zodError = result.error as z.ZodError;
zodError.issues.forEach((issue) => {
const field = issue.path.join('.');
blockingErrors.push({
field: field || 'unknown',
message: issue.message,
severity: 'blocking',
try {
// Debug logging for operator entity
if (entityType === 'operator') {
logger.log('Validating operator entity', {
dataKeys: data ? Object.keys(data as object) : [],
dataTypes: data ? Object.entries(data as object).reduce((acc, [key, val]) => {
acc[key] = typeof val;
return acc;
}, {} as Record<string, string>) : {},
rawData: JSON.stringify(data).substring(0, 500)
});
});
}
}
const schema = entitySchemas[entityType];
if (!schema) {
const error = {
field: 'entity_type',
message: `Unknown entity type: ${entityType}`,
severity: 'blocking' as const
};
handleNonCriticalError(new Error(`Unknown entity type: ${entityType}`), {
action: 'Entity Validation',
metadata: { entityType, providedData: data }
});
return {
isValid: false,
blockingErrors: [error],
warnings: [],
suggestions: [],
allErrors: [error],
};
}
const result = schema.safeParse(data);
const blockingErrors: ValidationError[] = [];
const warnings: ValidationError[] = [];
const suggestions: ValidationError[] = [];
// Process Zod errors
if (!result.success) {
const zodError = result.error as z.ZodError;
// Log detailed validation failure
handleNonCriticalError(zodError, {
action: 'Zod Validation Failed',
metadata: {
entityType,
issues: zodError.issues,
providedData: JSON.stringify(data).substring(0, 500),
issueCount: zodError.issues.length
}
});
zodError.issues.forEach((issue) => {
const field = issue.path.join('.') || entityType;
blockingErrors.push({
field,
message: `${issue.message} (code: ${issue.code})`,
severity: 'blocking',
});
});
}
// Add warnings for optional but recommended fields
const validData = data as Record<string, unknown>;
@@ -585,16 +623,43 @@ export async function validateEntityData(
}
}
const allErrors = [...blockingErrors, ...warnings, ...suggestions];
const isValid = blockingErrors.length === 0;
const allErrors = [...blockingErrors, ...warnings, ...suggestions];
const isValid = blockingErrors.length === 0;
return {
isValid,
blockingErrors,
warnings,
suggestions,
allErrors,
};
return {
isValid,
blockingErrors,
warnings,
suggestions,
allErrors,
};
} catch (error) {
// Catch any unexpected errors during validation
const errorId = handleNonCriticalError(error, {
action: 'Entity Validation Unexpected Error',
metadata: {
entityType,
dataType: typeof data,
hasData: !!data
}
});
return {
isValid: false,
blockingErrors: [{
field: entityType,
message: `Validation error: ${getErrorMessage(error)} (ref: ${errorId.slice(0, 8)})`,
severity: 'blocking'
}],
warnings: [],
suggestions: [],
allErrors: [{
field: entityType,
message: `Validation error: ${getErrorMessage(error)} (ref: ${errorId.slice(0, 8)})`,
severity: 'blocking'
}],
};
}
}
/**