Files
thrilltrack-explorer/src/lib/runtimeValidation.ts
2025-10-17 14:04:57 +00:00

241 lines
7.9 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.string(),
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.string(),
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;
}