Refactor: Complete error handling overhaul

This commit is contained in:
gpt-engineer-app[bot]
2025-11-02 23:19:46 +00:00
parent d057ddc8cc
commit 35c7c3e957
7 changed files with 303 additions and 26 deletions

View File

@@ -24,8 +24,13 @@ export async function invokeWithTracking<T = any>(
payload: any = {},
userId?: string,
parentRequestId?: string,
traceId?: string
traceId?: string,
timeout: number = 30000 // Default 30s timeout
): Promise<{ data: T | null; error: any; requestId: string; duration: number }> {
// Create AbortController for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const { result, requestId, duration } = await trackRequest(
{
@@ -39,6 +44,7 @@ export async function invokeWithTracking<T = any>(
// Include client request ID in payload for correlation
const { data, error } = await supabase.functions.invoke<T>(functionName, {
body: { ...payload, clientRequestId: context.requestId },
signal: controller.signal, // Add abort signal for timeout
});
if (error) throw error;
@@ -46,10 +52,25 @@ export async function invokeWithTracking<T = any>(
}
);
clearTimeout(timeoutId);
return { data: result, error: null, requestId, duration };
} catch (error: unknown) {
clearTimeout(timeoutId);
// Handle AbortError specifically
if (error instanceof Error && error.name === 'AbortError') {
return {
data: null,
error: {
message: `Request timeout: ${functionName} took longer than ${timeout}ms to respond`,
code: 'TIMEOUT',
},
requestId: 'timeout',
duration: timeout,
};
}
const errorMessage = getErrorMessage(error);
// On error, we don't have tracking info, so create basic response
return {
data: null,
error: { message: errorMessage },

View File

@@ -0,0 +1,79 @@
/**
* Runtime Data Validation for Moderation Queue
*
* Uses Zod to validate data shapes from the database at runtime.
* Prevents runtime errors if database schema changes unexpectedly.
*/
import { z } from 'zod';
import { logger } from '@/lib/logger';
// Profile schema
const ProfileSchema = z.object({
username: z.string(),
display_name: z.string().optional().nullable(),
avatar_url: z.string().optional().nullable(),
});
// Submission item schema
const SubmissionItemSchema = z.object({
id: z.string().uuid(),
status: z.string(),
item_type: z.string().optional(),
item_data: z.record(z.string(), z.any()).optional().nullable(),
original_data: z.record(z.string(), z.any()).optional().nullable(),
error_message: z.string().optional().nullable(),
});
// Main moderation item schema
export const ModerationItemSchema = z.object({
id: z.string().uuid(),
status: z.enum(['pending', 'approved', 'rejected', 'partially_approved', 'flagged']),
type: z.string(),
submission_type: z.string(),
created_at: z.string(),
updated_at: z.string().optional().nullable(),
content: z.record(z.string(), z.any()),
submitter_id: z.string().uuid(),
assigned_to: z.string().uuid().optional().nullable(),
locked_until: z.string().optional().nullable(),
reviewed_at: z.string().optional().nullable(),
reviewed_by: z.string().uuid().optional().nullable(),
reviewer_notes: z.string().optional().nullable(),
submission_items: z.array(SubmissionItemSchema).optional(),
submitter_profile: ProfileSchema.optional().nullable(),
assigned_profile: ProfileSchema.optional().nullable(),
reviewer_profile: ProfileSchema.optional().nullable(),
});
export const ModerationItemArraySchema = z.array(ModerationItemSchema);
/**
* Validate moderation items array
*
* @param data - Data to validate
* @returns Validation result with typed data or error
*/
export function validateModerationItems(data: unknown): {
success: boolean;
data?: any[];
error?: string
} {
const result = ModerationItemArraySchema.safeParse(data);
if (!result.success) {
logger.error('❌ Data validation failed', {
errors: result.error.issues.slice(0, 5) // Log first 5 issues
});
return {
success: false,
error: 'Received invalid data format from server. Please refresh the page.',
};
}
return {
success: true,
data: result.data,
};
}