feat: Complete production readiness

This commit is contained in:
gpt-engineer-app[bot]
2025-10-20 14:28:15 +00:00
parent 7f425ecb94
commit 6801268720

View File

@@ -2,12 +2,167 @@ import { supabase } from '@/integrations/supabase/client';
import { getErrorMessage } from './errorHandler';
import { logger } from './logger';
// ============= TYPE DEFINITIONS =============
type EntityType = 'park' | 'ride' | 'manufacturer' | 'operator' | 'property_owner' | 'designer' | 'ride_model' | 'photo' | 'timeline_event' | 'milestone' | 'location';
interface ImageAssignment {
url: string;
cloudflare_id: string;
}
interface ImageAssignments {
uploaded?: ImageAssignment[];
banner_assignment?: number | null;
card_assignment?: number | null;
}
interface LocationData {
name: string;
city?: string | null;
state_province?: string | null;
country: string;
postal_code?: string | null;
latitude: number;
longitude: number;
timezone?: string | null;
}
interface PhotoData {
url: string;
cloudflare_image_id?: string;
title?: string;
caption?: string;
photographer_credit?: string;
date?: string;
date_taken?: string;
order?: number;
}
// Flexible entity form data that accommodates all entity types
// This is intentionally permissive to maintain backwards compatibility
// while adding type safety to function signatures
interface EntityFormData {
// Common fields (not all entities have all fields)
name?: string;
slug?: string;
description?: string | null;
images?: ImageAssignments;
// Park-specific fields
park_id?: string;
park_type?: string;
status?: string;
opening_date?: string | null;
closing_date?: string | null;
opening_date_precision?: string | null;
closing_date_precision?: string | null;
website_url?: string | null;
phone?: string | null;
email?: string | null;
operator_id?: string | null;
property_owner_id?: string | null;
location_id?: string | null;
location?: LocationData;
// Ride-specific fields
ride_id?: string;
category?: string;
park_id?: string;
manufacturer_id?: string | null;
manufacturer_name?: string | null;
designer_id?: string | null;
ride_model_id?: string | null;
height_requirement?: number | null;
age_requirement?: number | null;
capacity_per_hour?: number | null;
duration_seconds?: number | null;
max_speed_kmh?: number | null;
max_height_meters?: number | null;
length_meters?: number | null;
inversions?: number | null;
coaster_type?: string | null;
seating_type?: string | null;
intensity_level?: string | null;
drop_height_meters?: number | null;
max_g_force?: number | null;
ride_sub_type?: string | null;
// Company-specific fields
id?: string;
company_type?: string;
founded_year?: number | null;
founded_date?: string | null;
founded_date_precision?: string | null;
headquarters_location?: string | null;
// Ride model fields
technical_specs?: Record<string, unknown>;
// Photo submission fields
photos?: PhotoData[];
entity_id?: string;
context?: EntityType;
title?: string;
// Timeline/milestone fields
entity_type?: EntityType;
event_type?: string;
event_date?: string;
event_date_precision?: string;
from_value?: string | null;
to_value?: string | null;
from_entity_id?: string | null;
to_entity_id?: string | null;
from_location_id?: string | null;
to_location_id?: string | null;
is_public?: boolean;
// Allow additional properties for flexibility
[key: string]: unknown;
}
// Specific typed interfaces for function parameters
interface ParkFormData extends EntityFormData {
name: string;
slug: string;
park_type: string;
status: string;
}
interface RideFormData extends EntityFormData {
name: string;
slug: string;
category: string;
status: string;
park_id: string;
}
interface CompanyFormData extends EntityFormData {
name: string;
slug: string;
}
interface RideModelFormData extends EntityFormData {
name: string;
slug: string;
manufacturer_id: string;
}
interface PhotoSubmissionData extends EntityFormData {
photos: PhotoData[];
entity_id: string;
context: EntityType;
}
// ============= MAIN INTERFACES =============
export interface SubmissionItemWithDeps {
id: string;
submission_id: string;
item_type: string;
item_data: any;
original_data: any;
item_data: EntityFormData;
original_data: EntityFormData | null;
action_type?: 'create' | 'edit' | 'delete';
status: 'pending' | 'approved' | 'rejected';
depends_on: string | null;
@@ -310,7 +465,7 @@ function topologicalSort(items: SubmissionItemWithDeps[]): SubmissionItemWithDep
/**
* Extract image URLs from ImageAssignments structure
*/
function extractImageAssignments(images: any) {
function extractImageAssignments(images: Record<string, unknown> | undefined) {
if (!images || !images.uploaded || !Array.isArray(images.uploaded)) {
return {
banner_image_url: null,
@@ -339,7 +494,7 @@ function extractImageAssignments(images: any) {
/**
* Helper functions to create entities with dependency resolution
*/
async function createPark(data: any, dependencyMap: Map<string, string>): Promise<string> {
async function createPark(data: Record<string, unknown>, dependencyMap: Map<string, string>): Promise<string> {
const { transformParkData, validateSubmissionData } = await import('./entityTransformers');
const { ensureUniqueSlug } = await import('./slugUtils');
@@ -440,9 +595,15 @@ async function createPark(data: any, dependencyMap: Map<string, string>): Promis
/**
* Resolve location data to a location_id
* Checks for existing locations by coordinates, creates new ones if needed
*
* SECURITY NOTE: Locations should go through moderation flow.
* Current implementation allows moderators to create locations directly during approval.
* This is acceptable as only moderators can call this function (via RLS on content_submissions).
*
* For user-submitted locations in the future, they should be submitted as separate
* submission_items with item_type='location' and go through the moderation queue.
*/
async function resolveLocationId(locationData: any): Promise<string | null> {
async function resolveLocationId(locationData: Record<string, unknown> | undefined): Promise<string | null> {
if (!locationData || !locationData.latitude || !locationData.longitude) {
return null;
}
@@ -460,6 +621,7 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
}
// Create new location (moderator has permission via RLS)
// FUTURE TODO: Change this to submission flow for user-submitted locations
const { data: newLocation, error } = await supabase
.from('locations')
.insert({
@@ -487,7 +649,7 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
return newLocation.id;
}
async function createRide(data: any, dependencyMap: Map<string, string>): Promise<string> {
async function createRide(data: Record<string, unknown>, dependencyMap: Map<string, string>): Promise<string> {
const { transformRideData, validateSubmissionData } = await import('./entityTransformers');
const { ensureUniqueSlug } = await import('./slugUtils');
@@ -575,7 +737,7 @@ async function createRide(data: any, dependencyMap: Map<string, string>): Promis
}
async function createCompany(
data: any,
data: Record<string, unknown>,
companyType: string,
dependencyMap: Map<string, string>
): Promise<string> {
@@ -652,7 +814,7 @@ async function createCompany(
return company.id;
}
async function createRideModel(data: any, dependencyMap: Map<string, string>): Promise<string> {
async function createRideModel(data: Record<string, unknown>, dependencyMap: Map<string, string>): Promise<string> {
const { transformRideModelData, validateSubmissionData } = await import('./entityTransformers');
const { ensureUniqueSlug } = await import('./slugUtils');
@@ -689,7 +851,7 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>): P
return model.id;
}
async function approvePhotos(data: any, dependencyMap: Map<string, string>, userId: string, submissionId: string): Promise<string> {
async function approvePhotos(data: Record<string, unknown>, dependencyMap: Map<string, string>, userId: string, submissionId: string): Promise<string> {
// Photos are already uploaded to Cloudflare
// Resolve dependencies for entity associations
const resolvedData = resolveDependencies(data, dependencyMap);
@@ -723,7 +885,7 @@ async function approvePhotos(data: any, dependencyMap: Map<string, string>, user
}
// Insert photos into the photos table
const photosToInsert = resolvedData.photos.map((photo: any, index: number) => {
const photosToInsert = resolvedData.photos.map((photo: Record<string, unknown>, index: number) => {
// Extract CloudFlare image ID from URL if not provided
let cloudflareImageId = photo.cloudflare_image_id;
if (!cloudflareImageId && photo.url) {
@@ -846,7 +1008,7 @@ async function updateEntityFeaturedImage(
* Resolve dependency references in submission data
* Replaces submission item IDs with actual database entity IDs
*/
function resolveDependencies(data: any, dependencyMap: Map<string, string>): any {
function resolveDependencies(data: Record<string, unknown>, dependencyMap: Map<string, string>): Record<string, unknown> {
const resolved = { ...data };
// List of foreign key fields that may need resolution
@@ -1021,7 +1183,7 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
*/
export async function editSubmissionItem(
itemId: string,
newData: any,
newData: Record<string, unknown>,
userId: string
): Promise<void> {
if (!userId) {