/** * Client-side validation for entity submissions * Prevents missing required fields before database calls */ export interface ValidationResult { valid: boolean; missingFields: string[]; errorMessage?: string; } export interface SlugValidationResult extends ValidationResult { suggestedSlug?: string; } /** * Validates slug format matching database constraints * Pattern: lowercase alphanumeric with hyphens only * No consecutive hyphens, no leading/trailing hyphens */ export function validateSlugFormat(slug: string): SlugValidationResult { if (!slug) { return { valid: false, missingFields: ['slug'], errorMessage: 'Slug is required' }; } // Must match DB regex: ^[a-z0-9]+(-[a-z0-9]+)*$ const slugRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; if (!slugRegex.test(slug)) { const suggested = slug .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); return { valid: false, missingFields: ['slug'], errorMessage: 'Slug must be lowercase alphanumeric with hyphens only (no spaces or special characters)', suggestedSlug: suggested }; } // Length constraints if (slug.length < 2) { return { valid: false, missingFields: ['slug'], errorMessage: 'Slug too short (minimum 2 characters)' }; } if (slug.length > 100) { return { valid: false, missingFields: ['slug'], errorMessage: 'Slug too long (maximum 100 characters)' }; } // Reserved slugs that could conflict with routes const reserved = [ 'admin', 'api', 'auth', 'new', 'edit', 'delete', 'create', 'update', 'null', 'undefined', 'settings', 'profile', 'login', 'logout', 'signup', 'dashboard', 'moderator', 'moderation' ]; if (reserved.includes(slug)) { return { valid: false, missingFields: ['slug'], errorMessage: `'${slug}' is a reserved slug and cannot be used`, suggestedSlug: `${slug}-1` }; } return { valid: true, missingFields: [] }; } /** * Validates required fields for park creation */ export function validateParkCreateFields(data: any): ValidationResult { const missingFields: string[] = []; if (!data.name?.trim()) missingFields.push('name'); if (!data.slug?.trim()) missingFields.push('slug'); if (!data.park_type) missingFields.push('park_type'); if (!data.status) missingFields.push('status'); if (missingFields.length > 0) { return { valid: false, missingFields, errorMessage: `Missing required fields for park creation: ${missingFields.join(', ')}` }; } // Validate slug format if (data.slug?.trim()) { const slugValidation = validateSlugFormat(data.slug.trim()); if (!slugValidation.valid) { return slugValidation; } } return { valid: true, missingFields: [] }; } /** * Validates required fields for ride creation */ export function validateRideCreateFields(data: any): ValidationResult { const missingFields: string[] = []; if (!data.name?.trim()) missingFields.push('name'); if (!data.slug?.trim()) missingFields.push('slug'); if (!data.category) missingFields.push('category'); if (!data.status) missingFields.push('status'); if (missingFields.length > 0) { return { valid: false, missingFields, errorMessage: `Missing required fields for ride creation: ${missingFields.join(', ')}` }; } // Validate slug format if (data.slug?.trim()) { const slugValidation = validateSlugFormat(data.slug.trim()); if (!slugValidation.valid) { return slugValidation; } } return { valid: true, missingFields: [] }; } /** * Validates required fields for company creation */ export function validateCompanyCreateFields(data: any): ValidationResult { const missingFields: string[] = []; if (!data.name?.trim()) missingFields.push('name'); if (!data.slug?.trim()) missingFields.push('slug'); if (!data.company_type) missingFields.push('company_type'); if (missingFields.length > 0) { return { valid: false, missingFields, errorMessage: `Missing required fields for company creation: ${missingFields.join(', ')}` }; } // Validate slug format if (data.slug?.trim()) { const slugValidation = validateSlugFormat(data.slug.trim()); if (!slugValidation.valid) { return slugValidation; } } return { valid: true, missingFields: [] }; } /** * Validates required fields for ride model creation */ export function validateRideModelCreateFields(data: any): ValidationResult { const missingFields: string[] = []; if (!data.name?.trim()) missingFields.push('name'); if (!data.slug?.trim()) missingFields.push('slug'); if (!data.manufacturer_id) missingFields.push('manufacturer_id'); if (!data.category) missingFields.push('category'); if (missingFields.length > 0) { return { valid: false, missingFields, errorMessage: `Missing required fields for ride model creation: ${missingFields.join(', ')}` }; } // Validate slug format if (data.slug?.trim()) { const slugValidation = validateSlugFormat(data.slug.trim()); if (!slugValidation.valid) { return slugValidation; } } return { valid: true, missingFields: [] }; } /** * Helper to throw validation error if validation fails */ export function assertValid(result: ValidationResult): void { if (!result.valid) { throw new Error(result.errorMessage); } }