mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 23:11:12 -05:00
122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|