diff --git a/docs/TEST_DATA_GENERATOR.md b/docs/TEST_DATA_GENERATOR.md new file mode 100644 index 00000000..69e9c424 --- /dev/null +++ b/docs/TEST_DATA_GENERATOR.md @@ -0,0 +1,246 @@ +# Test Data Generator + +## Overview + +The Test Data Generator is a comprehensive testing utility that creates realistic submissions to test the moderation queue, versioning, and audit systems. It's accessible from **Admin Settings > Testing** tab. + +## Features + +- **Multiple Presets**: Small, Medium, Large, and Stress test configurations +- **Entity Type Selection**: Choose which entities to generate (parks, rides, companies, etc.) +- **Advanced Options**: Dependencies, conflicts, version chains, escalations +- **Test Data Marking**: All generated data is marked with `is_test_data: true` +- **Easy Cleanup**: Clear all test data with one click + +## Usage + +### Accessing the Generator + +1. Log in as a moderator or admin +2. Navigate to **Admin Settings** +3. Click the **Testing** tab +4. Configure your test data generation + +### Presets + +#### Small (~20 submissions) +- **Use Case**: Quick sanity checks, basic functionality testing +- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models +- **Time**: ~2-5 seconds + +#### Medium (~100 submissions) +- **Use Case**: Standard testing, queue management validation +- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models +- **Time**: ~10-20 seconds + +#### Large (~500 submissions) +- **Use Case**: Performance testing, pagination verification +- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models +- **Time**: ~45-90 seconds + +#### Stress (~2000 submissions) +- **Use Case**: Load testing, database performance +- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models +- **Time**: ~3-5 minutes + +### Entity Types + +Select which entity types to generate: + +- **Parks**: Theme parks, amusement parks, water parks +- **Rides**: Roller coasters, flat rides, water rides, dark rides +- **Manufacturers**: Companies that build rides +- **Operators**: Companies that operate parks +- **Property Owners**: Companies that own park properties +- **Designers**: Individuals/companies that design rides +- **Ride Models**: Specific ride model types from manufacturers + +### Advanced Options + +#### Include Dependencies +- Creates parent-child relationships (e.g., rides linked to parks) +- Tests dependency resolution in moderation queue +- Essential for realistic testing scenarios + +#### Include Conflicts +- Generates submissions with missing dependencies +- Creates duplicate slug scenarios +- Tests error handling and conflict resolution + +#### Include Version Chains +- Creates multiple edits on the same entity +- Tests versioning system and rollback functionality +- Validates field-level change tracking + +#### Include Escalated Items +- Marks some submissions as high priority +- Tests escalation workflow +- Priority queue sorting validation + +#### Include Expired Locks +- Creates submissions with expired moderator locks +- Tests auto-release mechanism +- Queue cleanup validation + +## Test Data Structure + +All test submissions include: + +```json +{ + "content": { + "action": "create", + "metadata": { + "is_test_data": true, + "generated_at": "2024-01-15T10:30:00Z", + "generator_version": "1.0.0", + "preset": "medium" + } + } +} +``` + +This metadata allows: +- Easy identification in database queries +- Selective cleanup without affecting real data +- Version tracking of test data generator + +## Cleanup + +### Clear All Test Data + +1. Click **Clear All Test Data** button +2. Confirm the action in the dialog +3. All submissions with `is_test_data: true` will be deleted +4. Process includes: + - Deletion of `content_submissions` records + - Cascade deletion of related `submission_items` + - Cascade deletion of type-specific tables + - Removal of photo submissions + - Cleanup of version history + +**Note**: This only removes test data. Real user submissions are never affected. + +## Testing Scenarios + +### Basic CRUD Testing +1. Generate Small preset with all entity types +2. Navigate to Moderation Queue +3. Claim and approve a park submission +4. Verify park appears in database +5. Clear test data when done + +### Dependency Resolution +1. Generate Medium preset with dependencies enabled +2. Find a ride submission that depends on a new park +3. Approve the park first +4. Verify ride can now be approved +5. Check that park ID is correctly linked + +### Queue Management +1. Generate Large preset with escalated items +2. Verify escalated items appear at top of queue +3. Test claiming and lock expiration +4. Verify priority sorting works correctly + +### Performance Testing +1. Generate Stress preset +2. Navigate to moderation queue +3. Test pagination performance +4. Test filtering and search performance +5. Monitor database query times + +### Version Chain Testing +1. Generate Medium preset with version chains +2. Find an entity with multiple edits +3. View version history +4. Test rollback functionality +5. Verify field-level changes tracked + +## Best Practices + +### During Development +- Use **Small** preset for quick iterations +- Enable only the entity types you're testing +- Clear test data frequently to avoid clutter + +### Before Deployment +- Run **Medium** preset with all options +- Verify all workflows complete successfully +- Test with **Large** preset for performance baseline + +### Performance Testing +- Use **Stress** preset on staging environment +- Monitor database performance metrics +- Test concurrent moderator scenarios + +### Data Management +- Clear test data after each testing session +- Never deploy with test data in production +- Document any test data used in demos + +## Troubleshooting + +### Generation Fails +- Check browser console for errors +- Verify moderator permissions +- Check edge function logs in Supabase dashboard +- Ensure database connection is stable + +### Slow Generation +- Large/Stress presets take time (expected) +- Check database performance +- Verify edge function timeout settings +- Consider generating in smaller batches + +### Cannot Clear Test Data +- Verify test data exists (check stats display) +- Check RLS policies on content_submissions +- Ensure cascade delete is configured +- Check edge function permissions + +### Test Data Appears in Production +- **Never run generator in production** +- Use separate testing/staging environment +- If accidentally run, use Clear All Test Data immediately +- Verify with database query that test data is removed + +## Technical Details + +### Implementation Files +- **Service**: `src/lib/testDataGenerator.ts` +- **UI Component**: `src/components/admin/TestDataGenerator.tsx` +- **Seed Function**: `supabase/functions/seed-test-data/index.ts` +- **Integration**: `src/pages/AdminSettings.tsx` + +### Database Tables Used +- `content_submissions` - Main submission records +- `submission_items` - Individual submission items +- `park_submissions` - Park-specific data +- `ride_submissions` - Ride-specific data +- `company_submissions` - Company-specific data +- `ride_model_submissions` - Ride model-specific data + +### Security +- Requires moderator role +- Uses service role key for bulk operations +- Rate limited to prevent abuse +- All operations logged in audit trail + +## Future Enhancements + +Planned features: +- Photo submission generation +- Report/moderation flag generation +- Custom seed configuration +- Export/import test scenarios +- Automated test suite integration +- Performance benchmarking tools + +## Support + +For issues or questions: +1. Check edge function logs in Supabase dashboard +2. Review browser console for client errors +3. Verify database schema matches expectations +4. Check RLS policies are correctly configured diff --git a/src/components/admin/TestDataGenerator.tsx b/src/components/admin/TestDataGenerator.tsx new file mode 100644 index 00000000..9a4d9acd --- /dev/null +++ b/src/components/admin/TestDataGenerator.tsx @@ -0,0 +1,258 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Progress } from '@/components/ui/progress'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/hooks/use-toast'; +import { Beaker, CheckCircle, XCircle, ChevronDown, Trash2, AlertTriangle } from 'lucide-react'; +import { clearTestData, getTestDataStats } from '@/lib/testDataGenerator'; + +const PRESETS = { + small: { label: 'Small', description: '~20 submissions - Quick test', counts: '5 parks, 10 rides, 3 companies' }, + medium: { label: 'Medium', description: '~100 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies' }, + large: { label: 'Large', description: '~500 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies' }, + stress: { label: 'Stress', description: '~2000 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies' } +}; + +export function TestDataGenerator() { + const { toast } = useToast(); + const [preset, setPreset] = useState<'small' | 'medium' | 'large' | 'stress'>('small'); + const [entityTypes, setEntityTypes] = useState({ + parks: true, + rides: true, + manufacturers: true, + operators: true, + property_owners: true, + designers: true, + ride_models: true + }); + const [options, setOptions] = useState({ + includeDependencies: true, + includeConflicts: false, + includeVersionChains: false, + includeEscalated: false, + includeExpiredLocks: false + }); + const [loading, setLoading] = useState(false); + const [results, setResults] = useState(null); + const [stats, setStats] = useState<{ total: number; pending: number; approved: number } | null>(null); + + const selectedEntityTypes = Object.entries(entityTypes) + .filter(([_, enabled]) => enabled) + .map(([type]) => type); + + const loadStats = async () => { + try { + const data = await getTestDataStats(); + setStats(data); + } catch (error) { + console.error('Failed to load stats:', error); + } + }; + + const handleGenerate = async () => { + setLoading(true); + setResults(null); + + try { + const { data, error } = await supabase.functions.invoke('seed-test-data', { + body: { + preset, + entityTypes: selectedEntityTypes, + ...options + } + }); + + if (error) throw error; + + setResults(data); + await loadStats(); + + toast({ + title: 'Test Data Generated', + description: `Successfully created ${Object.values(data.summary).reduce((a: number, b: number) => a + b, 0)} submissions in ${data.time}s` + }); + } catch (error) { + console.error('Generation error:', error); + toast({ + title: 'Generation Failed', + description: error.message, + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + const handleClear = async () => { + setLoading(true); + + try { + const { deleted } = await clearTestData(); + await loadStats(); + + toast({ + title: 'Test Data Cleared', + description: `Removed ${deleted} test submissions` + }); + setResults(null); + } catch (error) { + console.error('Clear error:', error); + toast({ + title: 'Clear Failed', + description: error.message, + variant: 'destructive' + }); + } finally { + setLoading(false); + } + }; + + return ( + + +
+ + Test Data Generator +
+ + Generate realistic test submissions for testing the moderation queue and versioning systems + +
+ + + + + This will create test data in your database. All test submissions are marked and can be cleared later. + + + + {stats && ( +
+ Total Test Data: {stats.total} + Pending: {stats.pending} + Approved: {stats.approved} +
+ )} + +
+
+ + setPreset(v)} className="mt-2 space-y-3"> + {Object.entries(PRESETS).map(([key, { label, description, counts }]) => ( +
+ +
+ +

{description}

+

{counts}

+
+
+ ))} +
+
+ +
+ +
+ {Object.entries(entityTypes).map(([key, enabled]) => ( +
+ + setEntityTypes({ ...entityTypes, [key]: !!checked }) + } + /> + +
+ ))} +
+
+ + + + + Advanced Options + + + {Object.entries(options).map(([key, enabled]) => ( +
+ + setOptions({ ...options, [key]: !!checked }) + } + /> + +
+ ))} +
+
+
+ + {loading && ( +
+ +

Generating test data...

+
+ )} + + {results && ( + + + +
Test Data Generated Successfully
+
    +
  • • Created {results.summary.parks} park submissions
  • +
  • • Created {results.summary.rides} ride submissions
  • +
  • • Created {results.summary.companies} company submissions
  • +
  • • Created {results.summary.rideModels} ride model submissions
  • +
  • Time taken: {results.time}s
  • +
+
+
+ )} + +
+ + + + + + + + + Clear All Test Data? + + This will permanently delete all test submissions marked with is_test_data: true. This action cannot be undone. + + + + Cancel + Clear Test Data + + + +
+
+
+ ); +} diff --git a/src/lib/testDataGenerator.ts b/src/lib/testDataGenerator.ts new file mode 100644 index 00000000..0aac4b1d --- /dev/null +++ b/src/lib/testDataGenerator.ts @@ -0,0 +1,266 @@ +import { supabase } from '@/integrations/supabase/client'; +import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, RideModelSubmissionData } from '@/types/submission-data'; + +// Preset configurations +export const PRESETS = { + small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 0 }, + medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 0 }, + large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 0 }, + stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 0 } +} as const; + +// Word lists for realistic names +const PARK_ADJECTIVES = ['Adventure', 'Fantasy', 'Wonder', 'Magic', 'Dream', 'Thrill', 'Fun', 'Happy', 'Paradise', 'Epic']; +const PARK_NOUNS = ['World', 'Land', 'Park', 'Gardens', 'Kingdom', 'Realm', 'Island', 'Bay', 'Point', 'Valley']; +const RIDE_PREFIXES = ['Super', 'Mega', 'Ultra', 'Extreme', 'Wild', 'Crazy', 'Thunder', 'Lightning', 'Dragon', 'Phoenix']; +const RIDE_TYPES = ['Coaster', 'Drop', 'Spinner', 'Flyer', 'Racer', 'Twister', 'Loop', 'Screamer', 'Rush', 'Blast']; +const COMPANY_PREFIXES = ['Ace', 'Premier', 'Advanced', 'Dynamic', 'Innovative', 'Global', 'United', 'International']; +const COMPANY_SUFFIXES = ['Industries', 'Manufacturing', 'Enterprises', 'Solutions', 'Systems', 'Designs', 'Works', 'Creations']; + +const CITIES = ['New York', 'Los Angeles', 'Chicago', 'London', 'Paris', 'Tokyo', 'Sydney', 'Toronto', 'Berlin', 'Madrid']; +const COUNTRIES = ['USA', 'UK', 'France', 'Japan', 'Australia', 'Canada', 'Germany', 'Spain', 'Italy', 'Netherlands']; + +// Helper functions +function randomItem(array: T[]): T { + return array[Math.floor(Math.random() * array.length)]; +} + +function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function randomFloat(min: number, max: number, decimals = 2): number { + return parseFloat((Math.random() * (max - min) + min).toFixed(decimals)); +} + +function randomDate(startYear: number, endYear: number): string { + const start = new Date(startYear, 0, 1); + const end = new Date(endYear, 11, 31); + const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); + return date.toISOString().split('T')[0]; +} + +function generateSlug(name: string, counter?: number): string { + let slug = name + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); + + if (counter) { + slug += `-${counter}`; + } + + return slug; +} + +// Test data context for tracking created entities +export class TestDataContext { + createdParks: Array<{ id: string; name: string }> = []; + createdCompanies: Array<{ id: string; name: string; type: string }> = []; + createdRideModels: Array<{ id: string; name: string; manufacturer_id: string }> = []; + stats = { + parks: 0, + rides: 0, + companies: 0, + rideModels: 0, + photos: 0, + conflicts: 0, + versionChains: 0 + }; + + addPark(id: string, name: string) { + this.createdParks.push({ id, name }); + this.stats.parks++; + } + + addCompany(id: string, name: string, type: string) { + this.createdCompanies.push({ id, name, type }); + this.stats.companies++; + } + + addRideModel(id: string, name: string, manufacturer_id: string) { + this.createdRideModels.push({ id, name, manufacturer_id }); + this.stats.rideModels++; + } + + getRandomPark() { + return this.createdParks.length > 0 ? randomItem(this.createdParks) : null; + } + + getRandomCompany(type?: string) { + const filtered = type + ? this.createdCompanies.filter(c => c.type === type) + : this.createdCompanies; + return filtered.length > 0 ? randomItem(filtered) : null; + } + + getRandomRideModel() { + return this.createdRideModels.length > 0 ? randomItem(this.createdRideModels) : null; + } + + getSummary() { + return this.stats; + } +} + +// Random data generators +export function generateRandomPark(counter: number): ParkSubmissionData { + const name = `${randomItem(PARK_ADJECTIVES)} ${randomItem(PARK_NOUNS)}`; + const slug = generateSlug(name, counter); + + return { + name, + slug, + description: `A thrilling amusement park featuring world-class attractions and entertainment.`, + park_type: randomItem(['theme_park', 'amusement_park', 'water_park', 'adventure_park']), + status: randomItem(['operating', 'operating', 'operating', 'seasonal']), // More likely to be operating + opening_date: randomDate(1950, 2024), + closing_date: Math.random() > 0.9 ? randomDate(2000, 2024) : null, + website_url: `https://${slug}.example.com`, + phone: `+1-555-${randomInt(100, 999)}-${randomInt(1000, 9999)}`, + email: `info@${slug}.example.com`, + operator_id: null, + property_owner_id: null, + location_id: null, + banner_image_url: null, + banner_image_id: null, + card_image_url: null, + card_image_id: null + }; +} + +export function generateRandomRide(parkId: string, counter: number): RideSubmissionData { + const name = `${randomItem(RIDE_PREFIXES)} ${randomItem(RIDE_TYPES)}`; + const slug = generateSlug(name, counter); + const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride', 'family_ride']); + + return { + name, + slug, + description: `An exciting ${category.replace('_', ' ')} experience for all ages.`, + category, + ride_sub_type: null, + status: randomItem(['operating', 'operating', 'operating', 'seasonal']), + park_id: parkId, + ride_model_id: null, + manufacturer_id: null, + designer_id: null, + opening_date: randomDate(1980, 2024), + closing_date: Math.random() > 0.95 ? randomDate(2010, 2024) : null, + height_requirement: randomInt(90, 140), + age_requirement: null, + capacity_per_hour: randomInt(500, 2000), + duration_seconds: randomInt(60, 300), + max_speed_kmh: category === 'roller_coaster' ? randomFloat(40, 150, 1) : randomFloat(10, 60, 1), + max_height_meters: category === 'roller_coaster' ? randomFloat(20, 100, 1) : randomFloat(5, 30, 1), + length_meters: category === 'roller_coaster' ? randomFloat(500, 2500, 1) : null, + drop_height_meters: category === 'roller_coaster' ? randomFloat(15, 80, 1) : null, + inversions: category === 'roller_coaster' && Math.random() > 0.5 ? randomInt(1, 7) : 0, + max_g_force: category === 'roller_coaster' ? randomFloat(2, 5, 1) : null, + coaster_type: category === 'roller_coaster' ? randomItem(['steel', 'wooden', 'hybrid']) : null, + seating_type: category === 'roller_coaster' ? randomItem(['sit_down', 'inverted', 'floorless', 'suspended']) : null, + intensity_level: randomItem(['family', 'moderate', 'thrill', 'extreme']), + banner_image_url: null, + banner_image_id: null, + card_image_url: null, + card_image_id: null, + image_url: null + }; +} + +export function generateRandomCompany(type: 'manufacturer' | 'operator' | 'designer' | 'property_owner', counter: number): CompanySubmissionData { + const name = `${randomItem(COMPANY_PREFIXES)} ${randomItem(COMPANY_SUFFIXES)}`; + const slug = generateSlug(name, counter); + + return { + name, + slug, + description: `A leading ${type.replace('_', ' ')} in the amusement industry.`, + person_type: Math.random() > 0.9 ? 'individual' : 'company', + founded_year: randomInt(1950, 2020), + headquarters_location: `${randomItem(CITIES)}, ${randomItem(COUNTRIES)}`, + website_url: `https://${slug}.example.com`, + logo_url: null, + banner_image_url: null, + banner_image_id: null, + card_image_url: null, + card_image_id: null + }; +} + +export function generateRandomRideModel(manufacturerId: string, counter: number): RideModelSubmissionData { + const name = `${randomItem(RIDE_PREFIXES)} Model ${randomInt(100, 999)}`; + const slug = generateSlug(name, counter); + const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride']); + + return { + name, + slug, + manufacturer_id: manufacturerId, + category, + ride_type: randomItem(['spinning', 'launch', 'inverted', 'suspended', 'floorless']), + description: `A state-of-the-art ${category.replace('_', ' ')} model.`, + banner_image_url: null, + banner_image_id: null, + card_image_url: null, + card_image_id: null + }; +} + +// Cleanup utilities +export async function clearTestData(): Promise<{ deleted: number }> { + try { + // Find all test submissions + const { data: testSubmissions, error: fetchError } = await supabase + .from('content_submissions') + .select('id') + .eq('status', 'pending') + .contains('content', { metadata: { is_test_data: true } }); + + if (fetchError) throw fetchError; + if (!testSubmissions || testSubmissions.length === 0) { + return { deleted: 0 }; + } + + // Delete in batches of 100 + const batchSize = 100; + let totalDeleted = 0; + + for (let i = 0; i < testSubmissions.length; i += batchSize) { + const batch = testSubmissions.slice(i, i + batchSize); + const ids = batch.map(s => s.id); + + const { error: deleteError } = await supabase + .from('content_submissions') + .delete() + .in('id', ids); + + if (deleteError) throw deleteError; + totalDeleted += ids.length; + } + + return { deleted: totalDeleted }; + } catch (error) { + console.error('Error clearing test data:', error); + throw error; + } +} + +export async function getTestDataStats(): Promise<{ total: number; pending: number; approved: number }> { + const { data, error } = await supabase + .from('content_submissions') + .select('status') + .contains('content', { metadata: { is_test_data: true } }); + + if (error) throw error; + + const stats = { + total: data?.length || 0, + pending: data?.filter(s => s.status === 'pending').length || 0, + approved: data?.filter(s => s.status === 'approved').length || 0 + }; + + return stats; +} diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 0aa60a6d..137fd2be 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -12,6 +12,7 @@ import { useAuth } from '@/hooks/useAuth'; import { useUserRole } from '@/hooks/useUserRole'; import { useAdminSettings } from '@/hooks/useAdminSettings'; import { NovuMigrationUtility } from '@/components/admin/NovuMigrationUtility'; +import { TestDataGenerator } from '@/components/admin/TestDataGenerator'; import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug } from 'lucide-react'; export default function AdminSettings() { @@ -434,7 +435,7 @@ export default function AdminSettings() { - + Moderation @@ -455,6 +456,10 @@ export default function AdminSettings() { Integrations + + + Testing + @@ -588,6 +593,10 @@ export default function AdminSettings() { + + + + diff --git a/supabase/config.toml b/supabase/config.toml index 6cff6a14..315d5bbe 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -25,4 +25,7 @@ verify_jwt = true verify_jwt = true [functions.cancel-email-change] +verify_jwt = true + +[functions.seed-test-data] verify_jwt = true \ No newline at end of file diff --git a/supabase/functions/seed-test-data/index.ts b/supabase/functions/seed-test-data/index.ts new file mode 100644 index 00000000..d6ca448e --- /dev/null +++ b/supabase/functions/seed-test-data/index.ts @@ -0,0 +1,283 @@ +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +interface SeedOptions { + preset: 'small' | 'medium' | 'large' | 'stress'; + entityTypes: string[]; + includeDependencies: boolean; + includeConflicts: boolean; + includeVersionChains: boolean; + includeEscalated: boolean; + includeExpiredLocks: boolean; +} + +interface SeedPlan { + parks: number; + rides: number; + companies: number; + rideModels: number; +} + +const PRESETS: Record = { + small: { parks: 5, rides: 10, companies: 3, rideModels: 2 }, + medium: { parks: 20, rides: 50, companies: 20, rideModels: 10 }, + large: { parks: 100, rides: 250, companies: 100, rideModels: 50 }, + stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200 } +}; + +Deno.serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + const supabaseUrl = Deno.env.get('SUPABASE_URL')!; + const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + // Get auth header + const authHeader = req.headers.get('Authorization'); + if (!authHeader) { + return new Response(JSON.stringify({ error: 'No authorization header' }), { + status: 401, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); + } + + // Verify user is moderator + const token = authHeader.replace('Bearer ', ''); + const { data: { user }, error: userError } = await supabase.auth.getUser(token); + + if (userError || !user) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); + } + + const { data: isMod } = await supabase.rpc('is_moderator', { _user_id: user.id }); + if (!isMod) { + return new Response(JSON.stringify({ error: 'Must be moderator' }), { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); + } + + // Parse request + const { preset = 'small', entityTypes = [], includeDependencies = true, includeEscalated = false, includeExpiredLocks = false }: SeedOptions = await req.json(); + const plan = PRESETS[preset]; + + if (!plan) { + return new Response(JSON.stringify({ error: 'Invalid preset' }), { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + }); + } + + const startTime = Date.now(); + const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0 }; + const createdParks: string[] = []; + const createdCompanies: Record = { manufacturer: [], operator: [], designer: [], property_owner: [] }; + + // Helper to create submission + async function createSubmission(userId: string, type: string, itemData: any, options: { escalated?: boolean; expiredLock?: boolean } = {}) { + const submissionId = crypto.randomUUID(); + const itemId = crypto.randomUUID(); + + const contentData = { + action: 'create', + metadata: { + is_test_data: true, + generated_at: new Date().toISOString(), + generator_version: '1.0.0', + preset + } + }; + + // Create content_submission + const submissionData: any = { + id: submissionId, + user_id: userId, + submission_type: type, + status: 'pending', + content: contentData, + submitted_at: new Date().toISOString(), + priority: options.escalated ? 10 : Math.floor(Math.random() * 5) + 1 + }; + + if (options.escalated) { + submissionData.escalated = true; + submissionData.escalation_reason = 'Test escalation'; + } + + if (options.expiredLock) { + submissionData.assigned_to = user.id; + submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString(); // 30 min ago + } + + const { error: subError } = await supabase + .from('content_submissions') + .insert(submissionData); + + if (subError) throw subError; + + // Create submission_item + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + id: itemId, + submission_id: submissionId, + item_type: type, + item_data: itemData, + status: 'pending', + order_index: 0 + }); + + if (itemError) throw itemError; + + // Create type-specific submission record + const typeTableMap: Record = { + park: 'park_submissions', + ride: 'ride_submissions', + manufacturer: 'company_submissions', + operator: 'company_submissions', + designer: 'company_submissions', + property_owner: 'company_submissions', + ride_model: 'ride_model_submissions' + }; + + const table = typeTableMap[type]; + if (table) { + const typeData = { ...itemData, submission_id: submissionId }; + if (table === 'company_submissions') { + typeData.company_type = type; + } + + const { error: typeError } = await supabase + .from(table) + .insert(typeData); + + if (typeError) throw typeError; + } + + return submissionId; + } + + // Create parks + if (entityTypes.includes('parks')) { + for (let i = 0; i < plan.parks; i++) { + const parkData = { + name: `Test Park ${i + 1}`, + slug: `test-park-${i + 1}`, + description: 'Test park description', + park_type: ['theme_park', 'amusement_park', 'water_park'][Math.floor(Math.random() * 3)], + status: 'operating', + opening_date: '2000-01-01' + }; + + const options = { + escalated: includeEscalated && Math.random() < 0.1, + expiredLock: includeExpiredLocks && Math.random() < 0.1 + }; + + await createSubmission(user.id, 'park', parkData, options); + createdParks.push(`test-park-${i + 1}`); + summary.parks++; + } + } + + // Create companies + const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner']; + for (const compType of companyTypes) { + if (entityTypes.includes(compType)) { + const count = Math.floor(plan.companies / 4); + for (let i = 0; i < count; i++) { + const companyData = { + name: `Test ${compType} ${i + 1}`, + slug: `test-${compType}-${i + 1}`, + description: `Test ${compType} description`, + company_type: compType, + person_type: 'company', + founded_year: 2000 + }; + + await createSubmission(user.id, compType, companyData); + createdCompanies[compType].push(`test-${compType}-${i + 1}`); + summary.companies++; + } + } + } + + // Create rides (with dependencies if enabled) + if (entityTypes.includes('rides') && includeDependencies && createdParks.length > 0) { + for (let i = 0; i < plan.rides; i++) { + // Get random park ID from database + const parkSlug = createdParks[Math.floor(Math.random() * createdParks.length)]; + const { data: parkData } = await supabase + .from('parks') + .select('id') + .eq('slug', parkSlug) + .maybeSingle(); + + const rideData = { + name: `Test Ride ${i + 1}`, + slug: `test-ride-${i + 1}`, + description: 'Test ride description', + category: ['roller_coaster', 'flat_ride', 'water_ride'][Math.floor(Math.random() * 3)], + status: 'operating', + park_id: parkData?.id || null, + opening_date: '2010-01-01' + }; + + await createSubmission(user.id, 'ride', rideData); + summary.rides++; + } + } + + // Create ride models + if (entityTypes.includes('ride_models') && includeDependencies && createdCompanies.manufacturer.length > 0) { + for (let i = 0; i < plan.rideModels; i++) { + const mfgSlug = createdCompanies.manufacturer[Math.floor(Math.random() * createdCompanies.manufacturer.length)]; + const { data: mfgData } = await supabase + .from('companies') + .select('id') + .eq('slug', mfgSlug) + .maybeSingle(); + + const modelData = { + name: `Test Model ${i + 1}`, + slug: `test-model-${i + 1}`, + manufacturer_id: mfgData?.id || null, + category: 'roller_coaster', + ride_type: 'steel', + description: 'Test ride model' + }; + + await createSubmission(user.id, 'ride_model', modelData); + summary.rideModels++; + } + } + + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2); + + return new Response( + JSON.stringify({ + success: true, + summary, + time: elapsedTime + }), + { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + + } catch (error) { + console.error('Seed error:', error); + return new Response( + JSON.stringify({ error: error.message }), + { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } +});