mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 02:51:13 -05:00
feat: Complete production readiness
This commit is contained in:
@@ -2,12 +2,167 @@ import { supabase } from '@/integrations/supabase/client';
|
|||||||
import { getErrorMessage } from './errorHandler';
|
import { getErrorMessage } from './errorHandler';
|
||||||
import { logger } from './logger';
|
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 {
|
export interface SubmissionItemWithDeps {
|
||||||
id: string;
|
id: string;
|
||||||
submission_id: string;
|
submission_id: string;
|
||||||
item_type: string;
|
item_type: string;
|
||||||
item_data: any;
|
item_data: EntityFormData;
|
||||||
original_data: any;
|
original_data: EntityFormData | null;
|
||||||
action_type?: 'create' | 'edit' | 'delete';
|
action_type?: 'create' | 'edit' | 'delete';
|
||||||
status: 'pending' | 'approved' | 'rejected';
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
depends_on: string | null;
|
depends_on: string | null;
|
||||||
@@ -310,7 +465,7 @@ function topologicalSort(items: SubmissionItemWithDeps[]): SubmissionItemWithDep
|
|||||||
/**
|
/**
|
||||||
* Extract image URLs from ImageAssignments structure
|
* 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)) {
|
if (!images || !images.uploaded || !Array.isArray(images.uploaded)) {
|
||||||
return {
|
return {
|
||||||
banner_image_url: null,
|
banner_image_url: null,
|
||||||
@@ -339,7 +494,7 @@ function extractImageAssignments(images: any) {
|
|||||||
/**
|
/**
|
||||||
* Helper functions to create entities with dependency resolution
|
* 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 { transformParkData, validateSubmissionData } = await import('./entityTransformers');
|
||||||
const { ensureUniqueSlug } = await import('./slugUtils');
|
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
|
* 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) {
|
if (!locationData || !locationData.latitude || !locationData.longitude) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -460,6 +621,7 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new location (moderator has permission via RLS)
|
// 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
|
const { data: newLocation, error } = await supabase
|
||||||
.from('locations')
|
.from('locations')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -487,7 +649,7 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
|
|||||||
return newLocation.id;
|
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 { transformRideData, validateSubmissionData } = await import('./entityTransformers');
|
||||||
const { ensureUniqueSlug } = await import('./slugUtils');
|
const { ensureUniqueSlug } = await import('./slugUtils');
|
||||||
|
|
||||||
@@ -575,7 +737,7 @@ async function createRide(data: any, dependencyMap: Map<string, string>): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createCompany(
|
async function createCompany(
|
||||||
data: any,
|
data: Record<string, unknown>,
|
||||||
companyType: string,
|
companyType: string,
|
||||||
dependencyMap: Map<string, string>
|
dependencyMap: Map<string, string>
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -652,7 +814,7 @@ async function createCompany(
|
|||||||
return company.id;
|
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 { transformRideModelData, validateSubmissionData } = await import('./entityTransformers');
|
||||||
const { ensureUniqueSlug } = await import('./slugUtils');
|
const { ensureUniqueSlug } = await import('./slugUtils');
|
||||||
|
|
||||||
@@ -689,7 +851,7 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>): P
|
|||||||
return model.id;
|
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
|
// Photos are already uploaded to Cloudflare
|
||||||
// Resolve dependencies for entity associations
|
// Resolve dependencies for entity associations
|
||||||
const resolvedData = resolveDependencies(data, dependencyMap);
|
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
|
// 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
|
// Extract CloudFlare image ID from URL if not provided
|
||||||
let cloudflareImageId = photo.cloudflare_image_id;
|
let cloudflareImageId = photo.cloudflare_image_id;
|
||||||
if (!cloudflareImageId && photo.url) {
|
if (!cloudflareImageId && photo.url) {
|
||||||
@@ -846,7 +1008,7 @@ async function updateEntityFeaturedImage(
|
|||||||
* Resolve dependency references in submission data
|
* Resolve dependency references in submission data
|
||||||
* Replaces submission item IDs with actual database entity IDs
|
* 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 };
|
const resolved = { ...data };
|
||||||
|
|
||||||
// List of foreign key fields that may need resolution
|
// List of foreign key fields that may need resolution
|
||||||
@@ -1021,7 +1183,7 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
|
|||||||
*/
|
*/
|
||||||
export async function editSubmissionItem(
|
export async function editSubmissionItem(
|
||||||
itemId: string,
|
itemId: string,
|
||||||
newData: any,
|
newData: Record<string, unknown>,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user