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,7 +247,10 @@ export function SubmissionReviewManager({
} }
// Run validation on all selected items // Run validation on all selected items
const validationResultsMap = await validateMultipleItems( let validationResultsMap: Map<string, any>;
try {
validationResultsMap = await validateMultipleItems(
selectedItems.map(item => ({ selectedItems.map(item => ({
item_type: item.item_type, item_type: item.item_type,
item_data: item.item_data, item_data: item.item_data,
@@ -265,11 +268,48 @@ export function SubmissionReviewManager({
// CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings // CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings
if (itemsWithBlockingErrors.length > 0) { 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); setHasBlockingErrors(true);
setShowValidationBlockerDialog(true); setShowValidationBlockerDialog(true);
dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } }); dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } });
return; // Block approval 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
const itemsWithWarnings = selectedItems.filter(item => { const itemsWithWarnings = selectedItems.filter(item => {

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,19 +40,27 @@ 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) => {
const itemErrors = blockingErrors.filter((_, i) =>
itemNames.length === 1 || i === index
);
return (
<div key={index} className="space-y-2"> <div key={index} className="space-y-2">
<div className="font-medium text-sm">{name}</div> <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"> <Alert variant="destructive">
<AlertDescription className="space-y-1"> <AlertDescription className="space-y-1">
{blockingErrors {itemErrors.map((error, errIndex) => (
.filter((_, i) => i === index || itemNames.length === 1)
.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>
@@ -54,9 +68,24 @@ export function ValidationBlockerDialog({
</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}>
Close Close

View File

@@ -171,6 +171,7 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
}); });
// Run validation on all items // Run validation on all items
try {
const validationResults = await validateMultipleItems(itemsWithData); const validationResults = await validateMultipleItems(itemsWithData);
// Check for blocking errors // Check for blocking errors
@@ -181,19 +182,25 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
// CRITICAL: Block approval if any item has blocking errors // CRITICAL: Block approval if any item has blocking errors
if (itemsWithBlockingErrors.length > 0) { if (itemsWithBlockingErrors.length > 0) {
const errorDetails = itemsWithBlockingErrors.map(item => { // Log detailed blocking errors
itemsWithBlockingErrors.forEach(item => {
const result = validationResults.get(item.id); const result = validationResults.get(item.id);
return `${item.item_type}: ${result?.blockingErrors[0]?.message || 'Unknown error'}`; logger.error('Validation blocking approval', {
}).join(', '); submissionId: item.id,
itemId: item.id,
toast({ itemType: item.item_type,
title: 'Cannot Approve - Validation Errors', blockingErrors: result?.blockingErrors
description: `${itemsWithBlockingErrors.length} item(s) have blocking errors that must be fixed first. ${errorDetails}`, });
variant: 'destructive',
}); });
// Return early - do NOT proceed with approval const errorDetails = itemsWithBlockingErrors.map(item => {
return; 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) // Check for warnings (optional - can proceed but inform user)
@@ -208,6 +215,50 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
warningCount: itemsWithWarnings.length 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;
}
}
} }
const { data, error, requestId } = await invokeWithTracking( const { data, error, requestId } = await invokeWithTracking(

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,15 +454,39 @@ export async function validateEntityData(
entityType: keyof typeof entitySchemas, entityType: keyof typeof entitySchemas,
data: unknown data: unknown
): Promise<ValidationResult> { ): Promise<ValidationResult> {
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]; const schema = entitySchemas[entityType];
if (!schema) { 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 { return {
isValid: false, isValid: false,
blockingErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }], blockingErrors: [error],
warnings: [], warnings: [],
suggestions: [], suggestions: [],
allErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }], allErrors: [error],
}; };
} }
@@ -472,11 +498,23 @@ export async function validateEntityData(
// Process Zod errors // Process Zod errors
if (!result.success) { if (!result.success) {
const zodError = result.error as z.ZodError; 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) => { zodError.issues.forEach((issue) => {
const field = issue.path.join('.'); const field = issue.path.join('.') || entityType;
blockingErrors.push({ blockingErrors.push({
field: field || 'unknown', field,
message: issue.message, message: `${issue.message} (code: ${issue.code})`,
severity: 'blocking', severity: 'blocking',
}); });
}); });
@@ -595,6 +633,33 @@ export async function validateEntityData(
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'
}],
};
}
} }
/** /**