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 // Run validation on all selected items
const validationResultsMap = await validateMultipleItems( let validationResultsMap: Map<string, any>;
selectedItems.map(item => ({
item_type: item.item_type,
item_data: item.item_data,
id: item.id
}))
);
setValidationResults(validationResultsMap); try {
validationResultsMap = await validateMultipleItems(
// Check for blocking errors selectedItems.map(item => ({
const itemsWithBlockingErrors = selectedItems.filter(item => { item_type: item.item_type,
const result = validationResultsMap.get(item.id); item_data: item.item_data,
return result && result.blockingErrors.length > 0; id: item.id
}); }))
);
// CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings
if (itemsWithBlockingErrors.length > 0) { setValidationResults(validationResultsMap);
setHasBlockingErrors(true);
setShowValidationBlockerDialog(true); // Check for blocking errors
dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } }); const itemsWithBlockingErrors = selectedItems.filter(item => {
return; // Block approval 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 // 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 { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -9,6 +10,9 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { Alert, AlertDescription } from '@/components/ui/alert'; 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'; import { ValidationError } from '@/lib/entityValidationSchemas';
interface ValidationBlockerDialogProps { interface ValidationBlockerDialogProps {
@@ -24,9 +28,11 @@ export function ValidationBlockerDialog({
blockingErrors, blockingErrors,
itemNames, itemNames,
}: ValidationBlockerDialogProps) { }: ValidationBlockerDialogProps) {
const [showDetails, setShowDetails] = useState(false);
return ( return (
<AlertDialog open={open} onOpenChange={onClose}> <AlertDialog open={open} onOpenChange={onClose}>
<AlertDialogContent> <AlertDialogContent className="max-w-2xl">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-destructive"> <AlertDialogTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="w-5 h-5" /> <AlertCircle className="w-5 h-5" />
@@ -34,28 +40,51 @@ export function ValidationBlockerDialog({
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
The following items have blocking validation errors that MUST be fixed before approval. 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> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<div className="space-y-3 my-4"> <div className="space-y-3 my-4">
{itemNames.map((name, index) => ( {itemNames.map((name, index) => {
<div key={index} className="space-y-2"> const itemErrors = blockingErrors.filter((_, i) =>
<div className="font-medium text-sm">{name}</div> itemNames.length === 1 || i === index
<Alert variant="destructive"> );
<AlertDescription className="space-y-1">
{blockingErrors return (
.filter((_, i) => i === index || itemNames.length === 1) <div key={index} className="space-y-2">
.map((error, errIndex) => ( <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"> <div key={errIndex} className="text-sm">
<span className="font-medium">{error.field}:</span> {error.message} <span className="font-medium">{error.field}:</span> {error.message}
</div> </div>
))} ))}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</div> </div>
))} );
})}
</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> <AlertDialogFooter>
<AlertDialogAction onClick={onClose}> <AlertDialogAction onClick={onClose}>

View File

@@ -171,42 +171,93 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
}); });
// Run validation on all items // Run validation on all items
const validationResults = await validateMultipleItems(itemsWithData); try {
const validationResults = await validateMultipleItems(itemsWithData);
// Check for blocking errors
const itemsWithBlockingErrors = itemsWithData.filter(item => { // Check for blocking errors
const result = validationResults.get(item.id); const itemsWithBlockingErrors = itemsWithData.filter(item => {
return result && result.blockingErrors.length > 0;
});
// CRITICAL: Block approval if any item has blocking errors
if (itemsWithBlockingErrors.length > 0) {
const errorDetails = itemsWithBlockingErrors.map(item => {
const result = validationResults.get(item.id); const result = validationResults.get(item.id);
return `${item.item_type}: ${result?.blockingErrors[0]?.message || 'Unknown error'}`; return result && result.blockingErrors.length > 0;
}).join(', ');
toast({
title: 'Cannot Approve - Validation Errors',
description: `${itemsWithBlockingErrors.length} item(s) have blocking errors that must be fixed first. ${errorDetails}`,
variant: 'destructive',
}); });
// Return early - do NOT proceed with approval // CRITICAL: Block approval if any item has blocking errors
return; if (itemsWithBlockingErrors.length > 0) {
} // Log detailed blocking errors
itemsWithBlockingErrors.forEach(item => {
// Check for warnings (optional - can proceed but inform user) const result = validationResults.get(item.id);
const itemsWithWarnings = itemsWithData.filter(item => { logger.error('Validation blocking approval', {
const result = validationResults.get(item.id); submissionId: item.id,
return result && result.warnings.length > 0; itemId: item.id,
}); itemType: item.item_type,
blockingErrors: result?.blockingErrors
if (itemsWithWarnings.length > 0) { });
logger.info('Approval proceeding with warnings', { });
submissionId: item.id,
warningCount: itemsWithWarnings.length 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 { z } from 'zod';
import { supabase } from '@/lib/supabaseClient'; import { supabase } from '@/lib/supabaseClient';
import { handleNonCriticalError, getErrorMessage } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
// ============================================ // ============================================
// CENTRALIZED VALIDATION SCHEMAS // CENTRALIZED VALIDATION SCHEMAS
@@ -452,35 +454,71 @@ export async function validateEntityData(
entityType: keyof typeof entitySchemas, entityType: keyof typeof entitySchemas,
data: unknown data: unknown
): Promise<ValidationResult> { ): Promise<ValidationResult> {
const schema = entitySchemas[entityType]; try {
// Debug logging for operator entity
if (!schema) { if (entityType === 'operator') {
return { logger.log('Validating operator entity', {
isValid: false, dataKeys: data ? Object.keys(data as object) : [],
blockingErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }], dataTypes: data ? Object.entries(data as object).reduce((acc, [key, val]) => {
warnings: [], acc[key] = typeof val;
suggestions: [], return acc;
allErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }], }, {} as Record<string, string>) : {},
}; rawData: JSON.stringify(data).substring(0, 500)
}
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',
}); });
}); }
}
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 // Add warnings for optional but recommended fields
const validData = data as Record<string, unknown>; const validData = data as Record<string, unknown>;
@@ -585,16 +623,43 @@ export async function validateEntityData(
} }
} }
const allErrors = [...blockingErrors, ...warnings, ...suggestions]; const allErrors = [...blockingErrors, ...warnings, ...suggestions];
const isValid = blockingErrors.length === 0; const isValid = blockingErrors.length === 0;
return { return {
isValid, isValid,
blockingErrors, blockingErrors,
warnings, warnings,
suggestions, suggestions,
allErrors, 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'
}],
};
}
} }
/** /**