mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 23:51:13 -05:00
Fix: Resolve form validation and type errors
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
import { entitySchemas } from '@/lib/entityValidationSchemas';
|
||||||
import { validateSubmissionHandler } from '@/lib/entityFormValidation';
|
import { validateSubmissionHandler } from '@/lib/entityFormValidation';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ export const parkValidationSchema = z.object({
|
|||||||
email: z.string().email('Invalid email format').optional().or(z.literal('')),
|
email: z.string().email('Invalid email format').optional().or(z.literal('')),
|
||||||
operator_id: z.string().uuid().optional(),
|
operator_id: z.string().uuid().optional(),
|
||||||
property_owner_id: z.string().uuid().optional(),
|
property_owner_id: z.string().uuid().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional(),
|
||||||
|
images: z.object({
|
||||||
|
uploaded: z.array(z.any()),
|
||||||
|
banner_assignment: z.number().nullable().optional(),
|
||||||
|
card_assignment: z.number().nullable().optional(),
|
||||||
|
}).optional(),
|
||||||
}).refine((data) => {
|
}).refine((data) => {
|
||||||
if (data.closing_date && data.opening_date) {
|
if (data.closing_date && data.opening_date) {
|
||||||
return new Date(data.closing_date) >= new Date(data.opening_date);
|
return new Date(data.closing_date) >= new Date(data.opening_date);
|
||||||
@@ -45,6 +54,8 @@ export const rideValidationSchema = z.object({
|
|||||||
category: z.string().min(1, 'Category is required'),
|
category: z.string().min(1, 'Category is required'),
|
||||||
ride_sub_type: z.string().max(100, 'Sub type must be less than 100 characters').optional(),
|
ride_sub_type: z.string().max(100, 'Sub type must be less than 100 characters').optional(),
|
||||||
status: z.string().min(1, 'Status is required'),
|
status: z.string().min(1, 'Status is required'),
|
||||||
|
park_id: z.string().uuid().optional(),
|
||||||
|
designer_id: z.string().uuid().optional(),
|
||||||
opening_date: z.string().optional(),
|
opening_date: z.string().optional(),
|
||||||
closing_date: z.string().optional(),
|
closing_date: z.string().optional(),
|
||||||
height_requirement: z.number().min(0, 'Height requirement must be positive').max(300, 'Height requirement must be less than 300cm').optional(),
|
height_requirement: z.number().min(0, 'Height requirement must be positive').max(300, 'Height requirement must be less than 300cm').optional(),
|
||||||
@@ -57,6 +68,20 @@ export const rideValidationSchema = z.object({
|
|||||||
inversions: z.number().min(0, 'Inversions must be positive').optional(),
|
inversions: z.number().min(0, 'Inversions must be positive').optional(),
|
||||||
manufacturer_id: z.string().uuid().optional(),
|
manufacturer_id: z.string().uuid().optional(),
|
||||||
ride_model_id: z.string().uuid().optional(),
|
ride_model_id: z.string().uuid().optional(),
|
||||||
|
coaster_type: z.string().optional(),
|
||||||
|
seating_type: z.string().optional(),
|
||||||
|
intensity_level: z.string().optional(),
|
||||||
|
drop_height_meters: z.number().min(0, 'Drop height must be positive').max(200, 'Drop height must be less than 200 meters').optional(),
|
||||||
|
max_g_force: z.number().optional(),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional(),
|
||||||
|
images: z.object({
|
||||||
|
uploaded: z.array(z.any()),
|
||||||
|
banner_assignment: z.number().nullable().optional(),
|
||||||
|
card_assignment: z.number().nullable().optional(),
|
||||||
|
}).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Company Schema (Manufacturer, Designer, Operator, Property Owner)
|
// Company Schema (Manufacturer, Designer, Operator, Property Owner)
|
||||||
@@ -69,6 +94,15 @@ export const companyValidationSchema = z.object({
|
|||||||
founded_year: z.number().min(1800, 'Founded year must be after 1800').max(currentYear, `Founded year cannot be in the future`).optional(),
|
founded_year: z.number().min(1800, 'Founded year must be after 1800').max(currentYear, `Founded year cannot be in the future`).optional(),
|
||||||
headquarters_location: z.string().max(200, 'Location must be less than 200 characters').optional(),
|
headquarters_location: z.string().max(200, 'Location must be less than 200 characters').optional(),
|
||||||
website_url: z.string().url('Invalid URL format').optional().or(z.literal('')),
|
website_url: z.string().url('Invalid URL format').optional().or(z.literal('')),
|
||||||
|
banner_image_id: z.string().optional(),
|
||||||
|
banner_image_url: z.string().optional(),
|
||||||
|
card_image_id: z.string().optional(),
|
||||||
|
card_image_url: z.string().optional(),
|
||||||
|
images: z.object({
|
||||||
|
uploaded: z.array(z.any()),
|
||||||
|
banner_assignment: z.number().nullable().optional(),
|
||||||
|
card_assignment: z.number().nullable().optional(),
|
||||||
|
}).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ride Model Schema
|
// Ride Model Schema
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||||
|
import { validateEntityData } from "./validation.ts";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -205,6 +206,12 @@ serve(async (req) => {
|
|||||||
try {
|
try {
|
||||||
console.log(`Processing item ${item.id} of type ${item.item_type}`);
|
console.log(`Processing item ${item.id} of type ${item.item_type}`);
|
||||||
|
|
||||||
|
// Validate entity data before processing
|
||||||
|
const validation = validateEntityData(item.item_type, item.item_data);
|
||||||
|
if (!validation.valid) {
|
||||||
|
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Set user context for versioning trigger
|
// Set user context for versioning trigger
|
||||||
// This allows auto_create_entity_version() to capture the submitter
|
// This allows auto_create_entity_version() to capture the submitter
|
||||||
const { error: setConfigError } = await supabase.rpc('set_config_value', {
|
const { error: setConfigError } = await supabase.rpc('set_config_value', {
|
||||||
|
|||||||
104
supabase/functions/process-selective-approval/validation.ts
Normal file
104
supabase/functions/process-selective-approval/validation.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Server-side validation for entity data
|
||||||
|
* This provides a final safety layer before database writes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate entity data before database write
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user