diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 5760dbf3..f5890a96 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -8,6 +8,13 @@ import type { CompanyDatabaseRecord, TimelineEventDatabaseRecord } from '@/types import { logger } from './logger'; import { handleError } from './errorHandler'; import type { TimelineEventFormData, EntityType } from '@/types/timeline'; +import { + validateParkCreateFields, + validateRideCreateFields, + validateCompanyCreateFields, + validateRideModelCreateFields, + assertValid +} from './submissionValidation'; // ============================================ // COMPOSITE SUBMISSION TYPES @@ -382,6 +389,9 @@ export async function submitParkCreation( data: ParkFormData & { _compositeSubmission?: any }, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateParkCreateFields(data)); + // Check for composite submission with dependencies if (data._compositeSubmission) { const dependencies: CompositeSubmissionDependency[] = []; @@ -640,6 +650,9 @@ export async function submitRideCreation( }, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateRideCreateFields(data)); + // Check for composite submission with dependencies if (data._tempNewPark || data._tempNewManufacturer || data._tempNewDesigner || data._tempNewRideModel) { const dependencies: CompositeSubmissionDependency[] = []; @@ -959,6 +972,9 @@ export async function submitRideModelCreation( data: RideModelFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateRideModelCreateFields(data)); + // Check if user is banned const { data: profile } = await supabase .from('profiles') @@ -1114,6 +1130,9 @@ export async function submitManufacturerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateCompanyCreateFields({ ...data, company_type: 'manufacturer' })); + let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -1224,6 +1243,9 @@ export async function submitDesignerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateCompanyCreateFields({ ...data, company_type: 'designer' })); + let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -1334,6 +1356,9 @@ export async function submitOperatorCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateCompanyCreateFields({ ...data, company_type: 'operator' })); + let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -1444,6 +1469,9 @@ export async function submitPropertyOwnerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Validate required fields client-side + assertValid(validateCompanyCreateFields({ ...data, company_type: 'property_owner' })); + let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { diff --git a/src/lib/submissionValidation.ts b/src/lib/submissionValidation.ts new file mode 100644 index 00000000..c99aff9e --- /dev/null +++ b/src/lib/submissionValidation.ts @@ -0,0 +1,106 @@ +/** + * Client-side validation for entity submissions + * Prevents missing required fields before database calls + */ + +export interface ValidationResult { + valid: boolean; + missingFields: string[]; + errorMessage?: string; +} + +/** + * 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(', ')}` + }; + } + + 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(', ')}` + }; + } + + 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(', ')}` + }; + } + + 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(', ')}` + }; + } + + 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); + } +} diff --git a/supabase/migrations/20251105025945_336079fb-2c55-4f58-b09b-7515120518d6.sql b/supabase/migrations/20251105025945_336079fb-2c55-4f58-b09b-7515120518d6.sql new file mode 100644 index 00000000..f38d42c1 --- /dev/null +++ b/supabase/migrations/20251105025945_336079fb-2c55-4f58-b09b-7515120518d6.sql @@ -0,0 +1,6 @@ +-- Clean up orphaned data from broken submission +DELETE FROM submission_items +WHERE submission_id = '4ace4a83-1642-48d0-b8b9-586981284e8c'; + +DELETE FROM content_submissions +WHERE id = '4ace4a83-1642-48d0-b8b9-586981284e8c'; \ No newline at end of file