mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 07:51:13 -05:00
241 lines
8.2 KiB
TypeScript
241 lines
8.2 KiB
TypeScript
/**
|
|
* Runtime Type Validation
|
|
*
|
|
* Validates data from Supabase queries at runtime to ensure type safety.
|
|
* Uses Zod schemas to parse and validate database responses.
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { Park, Ride, Company, RideModel } from '@/types/database';
|
|
|
|
// ============================================
|
|
// RUNTIME SCHEMAS (Mirror Database Types)
|
|
// ============================================
|
|
|
|
export const parkRuntimeSchema = z.object({
|
|
id: z.string().uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
description: z.string().nullable().optional(),
|
|
park_type: z.string(),
|
|
status: z.enum(['operating', 'closed_permanently', 'closed_temporarily', 'under_construction', 'planned', 'abandoned']),
|
|
opening_date: z.string().nullable().optional(),
|
|
opening_date_precision: z.string().nullable().optional(),
|
|
closing_date: z.string().nullable().optional(),
|
|
closing_date_precision: z.string().nullable().optional(),
|
|
location_id: z.string().uuid().nullable().optional(),
|
|
operator_id: z.string().uuid().nullable().optional(),
|
|
property_owner_id: z.string().uuid().nullable().optional(),
|
|
website_url: z.string().nullable().optional(),
|
|
phone: z.string().nullable().optional(),
|
|
email: z.string().nullable().optional(),
|
|
banner_image_url: z.string().nullable().optional(),
|
|
banner_image_id: z.string().nullable().optional(),
|
|
card_image_url: z.string().nullable().optional(),
|
|
card_image_id: z.string().nullable().optional(),
|
|
ride_count: z.number().optional(),
|
|
coaster_count: z.number().optional(),
|
|
average_rating: z.number().nullable().optional(),
|
|
review_count: z.number().optional(),
|
|
view_count_7d: z.number().optional(),
|
|
view_count_30d: z.number().optional(),
|
|
view_count_all: z.number().optional(),
|
|
created_at: z.string().optional(),
|
|
updated_at: z.string().optional(),
|
|
}).passthrough(); // Allow additional fields from joins
|
|
|
|
export const rideRuntimeSchema = z.object({
|
|
id: z.string().uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
description: z.string().nullable().optional(),
|
|
category: z.string(),
|
|
ride_sub_type: z.string().nullable().optional(),
|
|
status: z.enum(['operating', 'closed_permanently', 'closed_temporarily', 'under_construction', 'relocated', 'stored', 'demolished']),
|
|
park_id: z.string().uuid().nullable().optional(),
|
|
manufacturer_id: z.string().uuid().nullable().optional(),
|
|
designer_id: z.string().uuid().nullable().optional(),
|
|
ride_model_id: z.string().uuid().nullable().optional(),
|
|
opening_date: z.string().nullable().optional(),
|
|
opening_date_precision: z.string().nullable().optional(),
|
|
closing_date: z.string().nullable().optional(),
|
|
closing_date_precision: z.string().nullable().optional(),
|
|
height_requirement_cm: z.number().nullable().optional(),
|
|
age_requirement: z.number().nullable().optional(),
|
|
max_speed_kmh: z.number().nullable().optional(),
|
|
duration_seconds: z.number().nullable().optional(),
|
|
capacity_per_hour: z.number().nullable().optional(),
|
|
gforce_max: z.number().nullable().optional(),
|
|
inversions_count: z.number().nullable().optional(),
|
|
length_meters: z.number().nullable().optional(),
|
|
height_meters: z.number().nullable().optional(),
|
|
drop_meters: z.number().nullable().optional(),
|
|
angle_degrees: z.number().nullable().optional(),
|
|
coaster_type: z.string().nullable().optional(),
|
|
seating_type: z.string().nullable().optional(),
|
|
intensity_level: z.string().nullable().optional(),
|
|
former_names: z.array(z.unknown()).nullable().optional(),
|
|
banner_image_url: z.string().nullable().optional(),
|
|
banner_image_id: z.string().nullable().optional(),
|
|
card_image_url: z.string().nullable().optional(),
|
|
card_image_id: z.string().nullable().optional(),
|
|
average_rating: z.number().nullable().optional(),
|
|
review_count: z.number().optional(),
|
|
view_count_7d: z.number().optional(),
|
|
view_count_30d: z.number().optional(),
|
|
view_count_all: z.number().optional(),
|
|
created_at: z.string().optional(),
|
|
updated_at: z.string().optional(),
|
|
}).passthrough();
|
|
|
|
export const companyRuntimeSchema = z.object({
|
|
id: z.string().uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
description: z.string().nullable().optional(),
|
|
company_type: z.string(),
|
|
person_type: z.string().nullable().optional(),
|
|
founded_year: z.number().nullable().optional(),
|
|
founded_date: z.string().nullable().optional(),
|
|
founded_date_precision: z.string().nullable().optional(),
|
|
headquarters_location: z.string().nullable().optional(),
|
|
website_url: z.string().nullable().optional(),
|
|
logo_url: z.string().nullable().optional(),
|
|
banner_image_url: z.string().nullable().optional(),
|
|
banner_image_id: z.string().nullable().optional(),
|
|
card_image_url: z.string().nullable().optional(),
|
|
card_image_id: z.string().nullable().optional(),
|
|
average_rating: z.number().nullable().optional(),
|
|
review_count: z.number().optional(),
|
|
view_count_7d: z.number().optional(),
|
|
view_count_30d: z.number().optional(),
|
|
view_count_all: z.number().optional(),
|
|
created_at: z.string().optional(),
|
|
updated_at: z.string().optional(),
|
|
}).passthrough();
|
|
|
|
export const rideModelRuntimeSchema = z.object({
|
|
id: z.string().uuid(),
|
|
name: z.string(),
|
|
slug: z.string(),
|
|
manufacturer_id: z.string().uuid().nullable().optional(),
|
|
category: z.string(),
|
|
description: z.string().nullable().optional(),
|
|
// Note: technical_specs deprecated - use ride_model_technical_specifications table
|
|
created_at: z.string().optional(),
|
|
updated_at: z.string().optional(),
|
|
}).passthrough();
|
|
|
|
// ============================================
|
|
// VALIDATION HELPERS
|
|
// ============================================
|
|
|
|
/**
|
|
* Validate a single park record
|
|
*/
|
|
export function validatePark(data: unknown): Park {
|
|
return parkRuntimeSchema.parse(data) as Park;
|
|
}
|
|
|
|
/**
|
|
* Validate an array of park records
|
|
*/
|
|
export function validateParks(data: unknown): Park[] {
|
|
return z.array(parkRuntimeSchema).parse(data) as Park[];
|
|
}
|
|
|
|
/**
|
|
* Safely validate parks (returns null on error)
|
|
*/
|
|
export function safeValidateParks(data: unknown): Park[] | null {
|
|
const result = z.array(parkRuntimeSchema).safeParse(data);
|
|
return result.success ? (result.data as Park[]) : null;
|
|
}
|
|
|
|
/**
|
|
* Validate a single ride record
|
|
*/
|
|
export function validateRide(data: unknown): Ride {
|
|
return rideRuntimeSchema.parse(data) as Ride;
|
|
}
|
|
|
|
/**
|
|
* Validate an array of ride records
|
|
*/
|
|
export function validateRides(data: unknown): Ride[] {
|
|
return z.array(rideRuntimeSchema).parse(data) as Ride[];
|
|
}
|
|
|
|
/**
|
|
* Safely validate rides (returns null on error)
|
|
*/
|
|
export function safeValidateRides(data: unknown): Ride[] | null {
|
|
const result = z.array(rideRuntimeSchema).safeParse(data);
|
|
return result.success ? (result.data as Ride[]) : null;
|
|
}
|
|
|
|
/**
|
|
* Validate a single company record
|
|
*/
|
|
export function validateCompany(data: unknown): Company {
|
|
return companyRuntimeSchema.parse(data) as Company;
|
|
}
|
|
|
|
/**
|
|
* Validate an array of company records
|
|
*/
|
|
export function validateCompanies(data: unknown): Company[] {
|
|
return z.array(companyRuntimeSchema).parse(data) as Company[];
|
|
}
|
|
|
|
/**
|
|
* Safely validate companies (returns null on error)
|
|
*/
|
|
export function safeValidateCompanies(data: unknown): Company[] | null {
|
|
const result = z.array(companyRuntimeSchema).safeParse(data);
|
|
return result.success ? (result.data as Company[]) : null;
|
|
}
|
|
|
|
/**
|
|
* Validate a single ride model record
|
|
*/
|
|
export function validateRideModel(data: unknown): RideModel {
|
|
return rideModelRuntimeSchema.parse(data) as RideModel;
|
|
}
|
|
|
|
/**
|
|
* Validate an array of ride model records
|
|
*/
|
|
export function validateRideModels(data: unknown): RideModel[] {
|
|
return z.array(rideModelRuntimeSchema).parse(data) as RideModel[];
|
|
}
|
|
|
|
/**
|
|
* Safely validate ride models (returns null on error)
|
|
*/
|
|
export function safeValidateRideModels(data: unknown): RideModel[] | null {
|
|
const result = z.array(rideModelRuntimeSchema).safeParse(data);
|
|
return result.success ? (result.data as RideModel[]) : null;
|
|
}
|
|
|
|
/**
|
|
* Generic validator for any entity type
|
|
*/
|
|
export function validateEntity<T>(
|
|
data: unknown,
|
|
schema: z.ZodSchema<T>
|
|
): T {
|
|
return schema.parse(data);
|
|
}
|
|
|
|
/**
|
|
* Safe generic validator (returns null on error)
|
|
*/
|
|
export function safeValidateEntity<T>(
|
|
data: unknown,
|
|
schema: z.ZodSchema<T>
|
|
): T | null {
|
|
const result = schema.safeParse(data);
|
|
return result.success ? result.data : null;
|
|
}
|