Files
thrilltrack-explorer/supabase/functions/process-selective-approval/validation.ts
2025-10-13 20:39:39 +00:00

266 lines
8.8 KiB
TypeScript

/**
* Server-side validation for entity data
* This provides a final safety layer before database writes
*/
export interface ValidationResult {
valid: boolean;
errors: string[];
}
export interface StrictValidationResult {
valid: boolean;
blockingErrors: string[];
warnings: string[];
}
/**
* Strict validation that separates blocking errors from warnings
* Used by the approval flow to prevent invalid data from being approved
*/
export function validateEntityDataStrict(
entityType: string,
data: any
): StrictValidationResult {
const result: StrictValidationResult = {
valid: true,
blockingErrors: [],
warnings: []
};
// Common validations (blocking)
if (!data.name?.trim()) {
result.blockingErrors.push('Name is required');
}
if (!data.slug?.trim()) {
result.blockingErrors.push('Slug is required');
}
if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) {
result.blockingErrors.push('Slug must contain only lowercase letters, numbers, and hyphens');
}
if (data.name && data.name.length > 200) {
result.blockingErrors.push('Name must be less than 200 characters');
}
if (data.description && data.description.length > 2000) {
result.blockingErrors.push('Description must be less than 2000 characters');
}
// URL validation (warning)
if (data.website_url && data.website_url !== '' && !isValidUrl(data.website_url)) {
result.warnings.push('Website URL format may be invalid');
}
// Email validation (warning)
if (data.email && data.email !== '' && !isValidEmail(data.email)) {
result.warnings.push('Email format may be invalid');
}
// Entity-specific validations
switch (entityType) {
case 'park':
if (!data.park_type) {
result.blockingErrors.push('Park type is required');
}
if (!data.status) {
result.blockingErrors.push('Status is required');
}
if (data.location_id === null || data.location_id === undefined) {
result.blockingErrors.push('Location is required for parks');
}
if (data.opening_date && data.closing_date) {
const opening = new Date(data.opening_date);
const closing = new Date(data.closing_date);
if (closing < opening) {
result.blockingErrors.push('Closing date must be after opening date');
}
}
break;
case 'ride':
if (!data.category) {
result.blockingErrors.push('Category is required');
}
if (!data.status) {
result.blockingErrors.push('Status is required');
}
if (data.park_id === null || data.park_id === undefined) {
result.blockingErrors.push('Park is required for rides');
}
if (data.max_speed_kmh && (data.max_speed_kmh < 0 || data.max_speed_kmh > 300)) {
result.blockingErrors.push('Max speed must be between 0 and 300 km/h');
}
if (data.max_height_meters && (data.max_height_meters < 0 || data.max_height_meters > 200)) {
result.blockingErrors.push('Max height must be between 0 and 200 meters');
}
if (data.drop_height_meters && (data.drop_height_meters < 0 || data.drop_height_meters > 200)) {
result.blockingErrors.push('Drop height must be between 0 and 200 meters');
}
if (data.height_requirement && (data.height_requirement < 0 || data.height_requirement > 300)) {
result.blockingErrors.push('Height requirement must be between 0 and 300 cm');
}
break;
case 'manufacturer':
case 'designer':
case 'operator':
case 'property_owner':
if (!data.company_type) {
result.blockingErrors.push(`Company type is required (expected: ${entityType})`);
} else if (data.company_type !== entityType) {
result.blockingErrors.push(`Company type mismatch: expected '${entityType}' but got '${data.company_type}'`);
}
if (data.founded_year) {
const year = parseInt(data.founded_year);
const currentYear = new Date().getFullYear();
if (year < 1800 || year > currentYear) {
result.warnings.push(`Founded year should be between 1800 and ${currentYear}`);
}
}
break;
case 'ride_model':
if (!data.category) {
result.blockingErrors.push('Category is required');
}
if (!data.ride_type) {
result.blockingErrors.push('Ride type is required');
}
break;
case 'photo':
if (!data.cloudflare_image_id) {
result.blockingErrors.push('Image ID is required');
}
if (!data.entity_type) {
result.blockingErrors.push('Entity type is required');
}
if (!data.entity_id) {
result.blockingErrors.push('Entity ID is required');
}
if (data.caption && data.caption.length > 500) {
result.blockingErrors.push('Caption must be less than 500 characters');
}
break;
}
result.valid = result.blockingErrors.length === 0;
return result;
}
// Helper functions
function isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
/**
* Validate entity data before database write (legacy function)
*/
export function validateEntityData(entityType: string, data: any): ValidationResult {
const errors: string[] = [];
// Common validations for all entities
if (!data.name || data.name.trim().length === 0) {
errors.push('Name is required');
}
if (!data.slug || data.slug.trim().length === 0) {
errors.push('Slug is required');
}
if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) {
errors.push('Slug must contain only lowercase letters, numbers, and hyphens');
}
if (data.name && data.name.length > 200) {
errors.push('Name must be less than 200 characters');
}
if (data.description && data.description.length > 2000) {
errors.push('Description must be less than 2000 characters');
}
if (data.website_url && data.website_url !== '' && !data.website_url.startsWith('http')) {
errors.push('Website URL must start with http:// or https://');
}
if (data.email && data.email !== '' && !data.email.includes('@')) {
errors.push('Invalid email format');
}
// Entity-specific validations
switch (entityType) {
case 'park':
if (!data.park_type) errors.push('Park type is required');
if (!data.status) errors.push('Status is required');
if (data.opening_date && data.closing_date) {
const opening = new Date(data.opening_date);
const closing = new Date(data.closing_date);
if (closing < opening) {
errors.push('Closing date must be after opening date');
}
}
break;
case 'ride':
if (!data.category) errors.push('Category is required');
if (!data.status) errors.push('Status is required');
if (data.max_speed_kmh && (data.max_speed_kmh < 0 || data.max_speed_kmh > 300)) {
errors.push('Max speed must be between 0 and 300 km/h');
}
if (data.max_height_meters && (data.max_height_meters < 0 || data.max_height_meters > 200)) {
errors.push('Max height must be between 0 and 200 meters');
}
if (data.drop_height_meters && (data.drop_height_meters < 0 || data.drop_height_meters > 200)) {
errors.push('Drop height must be between 0 and 200 meters');
}
if (data.height_requirement && (data.height_requirement < 0 || data.height_requirement > 300)) {
errors.push('Height requirement must be between 0 and 300 cm');
}
break;
case 'manufacturer':
case 'designer':
case 'operator':
case 'property_owner':
if (!data.company_type) {
errors.push(`Company type is required (expected: ${entityType})`);
} else if (data.company_type !== entityType) {
errors.push(`Company type mismatch: expected '${entityType}' but got '${data.company_type}'`);
}
if (data.founded_year) {
const year = parseInt(data.founded_year);
const currentYear = new Date().getFullYear();
if (year < 1800 || year > currentYear) {
errors.push(`Founded year must be between 1800 and ${currentYear}`);
}
}
break;
case 'ride_model':
if (!data.category) errors.push('Category is required');
if (!data.ride_type) errors.push('Ride type is required');
break;
case 'photo':
if (!data.cloudflare_image_id) errors.push('Image ID is required');
if (!data.entity_type) errors.push('Entity type is required');
if (!data.entity_id) errors.push('Entity ID is required');
if (data.caption && data.caption.length > 500) {
errors.push('Caption must be less than 500 characters');
}
break;
}
return {
valid: errors.length === 0,
errors
};
}