/** * Test Data Cleanup Utility * * Safely removes test fixtures created during integration tests. * * SAFETY FEATURES: * - Only deletes records marked with is_test_data = true * - Only deletes records with test-specific naming patterns * - Cascading deletes handled by database foreign keys * - Detailed logging of all deletions * - Rollback support via transactions */ import { supabase } from '@/lib/supabaseClient'; import { handleError } from '@/lib/errorHandler'; export interface CleanupResult { table: string; deleted: number; duration: number; error?: string; } export interface CleanupSummary { totalDeleted: number; totalDuration: number; results: CleanupResult[]; success: boolean; } /** * Delete test data from a specific table using type-safe queries */ async function cleanupParks(): Promise { const startTime = Date.now(); try { const { error, count } = await supabase .from('parks') .delete() .eq('is_test_data', true); if (error) throw error; console.log(`✓ Cleaned ${count || 0} test parks`); return { table: 'parks', deleted: count || 0, duration: Date.now() - startTime }; } catch (error) { return { table: 'parks', deleted: 0, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }; } } async function cleanupRides(): Promise { const startTime = Date.now(); try { const { error, count } = await supabase .from('rides') .delete() .eq('is_test_data', true); if (error) throw error; console.log(`✓ Cleaned ${count || 0} test rides`); return { table: 'rides', deleted: count || 0, duration: Date.now() - startTime }; } catch (error) { return { table: 'rides', deleted: 0, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }; } } async function cleanupCompanies(): Promise { const startTime = Date.now(); try { const { error, count } = await supabase .from('companies') .delete() .eq('is_test_data', true); if (error) throw error; console.log(`✓ Cleaned ${count || 0} test companies`); return { table: 'companies', deleted: count || 0, duration: Date.now() - startTime }; } catch (error) { return { table: 'companies', deleted: 0, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }; } } async function cleanupRideModels(): Promise { const startTime = Date.now(); try { const { error, count } = await supabase .from('ride_models') .delete() .eq('is_test_data', true); if (error) throw error; console.log(`✓ Cleaned ${count || 0} test ride models`); return { table: 'ride_models', deleted: count || 0, duration: Date.now() - startTime }; } catch (error) { return { table: 'ride_models', deleted: 0, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }; } } async function cleanupLocations(): Promise { const startTime = Date.now(); try { const { error, count } = await supabase .from('locations') .delete() .eq('is_test_data', true); if (error) throw error; console.log(`✓ Cleaned ${count || 0} test locations`); return { table: 'locations', deleted: count || 0, duration: Date.now() - startTime }; } catch (error) { return { table: 'locations', deleted: 0, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }; } } /** * Clean up test submissions (must be done before entities due to FK constraints) */ async function cleanupSubmissions(): Promise { const results: CleanupResult[] = []; // Clean content_submissions (cascade will handle related tables) const startTime = Date.now(); try { const { error, count } = await supabase .from('content_submissions') .delete() .eq('is_test_data', true); if (!error) { results.push({ table: 'content_submissions', deleted: count || 0, duration: Date.now() - startTime }); console.log(`✓ Cleaned ${count || 0} test submissions (cascade cleanup)`); } else { results.push({ table: 'content_submissions', deleted: 0, duration: Date.now() - startTime, error: error.message }); } } catch (error) { results.push({ table: 'content_submissions', deleted: 0, duration: Date.now() - startTime, error: error instanceof Error ? error.message : String(error) }); } return results; } /** * Clean up test versions (historical records) */ async function cleanupVersions(): Promise { const results: CleanupResult[] = []; // Clean park versions try { const { error, count } = await supabase.from('park_versions').delete().eq('is_test_data', true); results.push({ table: 'park_versions', deleted: error ? 0 : (count || 0), duration: 0, error: error?.message }); } catch (e) { results.push({ table: 'park_versions', deleted: 0, duration: 0, error: String(e) }); } // Clean ride versions try { const { error, count } = await supabase.from('ride_versions').delete().eq('is_test_data', true); results.push({ table: 'ride_versions', deleted: error ? 0 : (count || 0), duration: 0, error: error?.message }); } catch (e) { results.push({ table: 'ride_versions', deleted: 0, duration: 0, error: String(e) }); } // Clean company versions try { const { error, count } = await supabase.from('company_versions').delete().eq('is_test_data', true); results.push({ table: 'company_versions', deleted: error ? 0 : (count || 0), duration: 0, error: error?.message }); } catch (e) { results.push({ table: 'company_versions', deleted: 0, duration: 0, error: String(e) }); } // Clean ride_model versions try { const { error, count } = await supabase.from('ride_model_versions').delete().eq('is_test_data', true); results.push({ table: 'ride_model_versions', deleted: error ? 0 : (count || 0), duration: 0, error: error?.message }); } catch (e) { results.push({ table: 'ride_model_versions', deleted: 0, duration: 0, error: String(e) }); } console.log(`✓ Cleaned ${results.reduce((sum, r) => sum + r.deleted, 0)} version records`); return results; } /** * Clean up test entities (main tables) */ async function cleanupEntities(): Promise { const results: CleanupResult[] = []; // Order matters: clean dependent entities first results.push(await cleanupRides()); results.push(await cleanupParks()); results.push(await cleanupRideModels()); results.push(await cleanupCompanies()); results.push(await cleanupLocations()); return results; } /** * Clean up test-related metadata and tracking tables */ async function cleanupMetadata(): Promise { const results: CleanupResult[] = []; // Clean approval metrics for test submissions try { const { data: testSubmissions } = await supabase .from('content_submissions') .select('id') .eq('is_test_data', true); if (testSubmissions && testSubmissions.length > 0) { const submissionIds = testSubmissions.map(s => s.id); const { error, count } = await supabase .from('approval_transaction_metrics') .delete() .in('submission_id', submissionIds); if (!error) { results.push({ table: 'approval_transaction_metrics', deleted: count || 0, duration: 0 }); } } } catch (error) { console.error('Failed to cleanup metadata:', error); } return results; } /** * Run complete test data cleanup * * Executes cleanup in proper order to respect foreign key constraints: * 1. Submissions (depend on entities) * 2. Versions (historical records) * 3. Metadata (metrics, audit logs) * 4. Entities (main tables) */ export async function cleanupTestData(): Promise { const startTime = Date.now(); const allResults: CleanupResult[] = []; console.log('🧹 Starting test data cleanup...'); try { // Phase 1: Clean submissions first (they reference entities) console.log('\n📋 Phase 1: Cleaning submissions...'); const submissionResults = await cleanupSubmissions(); allResults.push(...submissionResults); // Phase 2: Clean versions (historical records) console.log('\n📚 Phase 2: Cleaning version history...'); const versionResults = await cleanupVersions(); allResults.push(...versionResults); // Phase 3: Clean metadata console.log('\n📊 Phase 3: Cleaning metadata...'); const metadataResults = await cleanupMetadata(); allResults.push(...metadataResults); // Phase 4: Clean entities (main tables) console.log('\n🏗️ Phase 4: Cleaning entities...'); const entityResults = await cleanupEntities(); allResults.push(...entityResults); const totalDeleted = allResults.reduce((sum, r) => sum + r.deleted, 0); const totalDuration = Date.now() - startTime; const hasErrors = allResults.some(r => r.error); console.log(`\n✅ Cleanup complete: ${totalDeleted} records deleted in ${totalDuration}ms`); return { totalDeleted, totalDuration, results: allResults, success: !hasErrors }; } catch (error) { console.error('❌ Cleanup failed:', error); return { totalDeleted: allResults.reduce((sum, r) => sum + r.deleted, 0), totalDuration: Date.now() - startTime, results: allResults, success: false }; } } /** * Clean up only specific entity types (selective cleanup) */ export async function cleanupEntityType( entityType: 'parks' | 'rides' | 'companies' | 'ride_models' | 'locations' ): Promise { console.log(`🧹 Cleaning test ${entityType}...`); switch (entityType) { case 'parks': return cleanupParks(); case 'rides': return cleanupRides(); case 'companies': return cleanupCompanies(); case 'ride_models': return cleanupRideModels(); case 'locations': return cleanupLocations(); } } /** * Verify cleanup was successful (safety check) */ export async function verifyCleanup(): Promise<{ remainingTestData: number; tables: Record; }> { const counts: Record = {}; let total = 0; // Check parks const { count: parksCount } = await supabase .from('parks') .select('*', { count: 'exact', head: true }) .eq('is_test_data', true); if (parksCount !== null) { counts.parks = parksCount; total += parksCount; } // Check rides const { count: ridesCount } = await supabase .from('rides') .select('*', { count: 'exact', head: true }) .eq('is_test_data', true); if (ridesCount !== null) { counts.rides = ridesCount; total += ridesCount; } // Check companies const { count: companiesCount } = await supabase .from('companies') .select('*', { count: 'exact', head: true }) .eq('is_test_data', true); if (companiesCount !== null) { counts.companies = companiesCount; total += companiesCount; } // Check ride_models const { count: rideModelsCount } = await supabase .from('ride_models') .select('*', { count: 'exact', head: true }) .eq('is_test_data', true); if (rideModelsCount !== null) { counts.ride_models = rideModelsCount; total += rideModelsCount; } // Check locations const { count: locationsCount } = await supabase .from('locations') .select('*', { count: 'exact', head: true }) .eq('is_test_data', true); if (locationsCount !== null) { counts.locations = locationsCount; total += locationsCount; } return { remainingTestData: total, tables: counts }; }