/** * 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 { handleError } from '@/lib/errorHandler'; // Profile schema (matches database JSONB structure) const ProfileSchema = z.object({ user_id: z.string().uuid(), username: z.string(), display_name: z.string().optional().nullable(), avatar_url: z.string().optional().nullable(), }); // Legacy profile schema (for backward compatibility) const LegacyProfileSchema = 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(), // Typed FK columns (optional, only one will be populated) park_submission_id: z.string().uuid().optional().nullable(), ride_submission_id: z.string().uuid().optional().nullable(), photo_submission_id: z.string().uuid().optional().nullable(), company_submission_id: z.string().uuid().optional().nullable(), ride_model_submission_id: z.string().uuid().optional().nullable(), timeline_event_submission_id: z.string().uuid().optional().nullable(), action_type: z.enum(['create', 'edit', 'delete']).optional(), 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(), // Accept both created_at and submitted_at for flexibility created_at: z.string(), submitted_at: z.string().optional(), updated_at: z.string().optional().nullable(), reviewed_at: z.string().optional().nullable(), content: z.record(z.string(), z.any()).optional().nullable(), // User fields (support both old and new naming) submitter_id: z.string().uuid().optional(), user_id: z.string().uuid().optional(), assigned_to: z.string().uuid().optional().nullable(), locked_until: z.string().optional().nullable(), reviewed_by: z.string().uuid().optional().nullable(), reviewer_notes: z.string().optional().nullable(), // Escalation fields escalated: z.boolean().optional().default(false), escalation_reason: z.string().optional().nullable(), // Profile objects (new structure from view) submitter_profile: ProfileSchema.optional().nullable(), assigned_profile: ProfileSchema.optional().nullable(), reviewer_profile: ProfileSchema.optional().nullable(), // Legacy profile support user_profile: LegacyProfileSchema.optional().nullable(), // Submission items submission_items: z.array(SubmissionItemSchema).optional().nullable(), // Entity names entity_name: z.string().optional(), park_name: z.string().optional(), }); 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) { handleError(result.error, { action: 'Data validation failed', metadata: { errors: result.error.issues.slice(0, 5) } }); return { success: false, error: 'Received invalid data format from server. Please refresh the page.', }; } return { success: true, data: result.data, }; }