mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:11:17 -05:00
Refactors `uploadPendingImages` to use `Promise.allSettled` for parallel uploads and implements JSON path queries in `clearTestData` and `getTestDataStats` for more robust test data management. Enhances `seed-test-data` function to support creating data conflicts and version chains, and adds validation for `imageId` format in `upload-image` function. Updates `AutocompleteSearch` to use a default search types constant. Replit-Commit-Author: Agent Replit-Commit-Session-Id: dc31cf9d-7a06-4420-8ade-e7b7f5200e71 Replit-Commit-Checkpoint-Type: intermediate_checkpoint
328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
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<string, SeedPlan> = {
|
|
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 {
|
|
// SECURITY: Service Role Key Usage
|
|
// ---------------------------------
|
|
// This function uses the service role key to seed test data bypassing RLS.
|
|
// This is required because:
|
|
// 1. Test data generation needs to create entities in protected tables
|
|
// 2. Moderator role is verified via is_moderator() RPC call before proceeding
|
|
// Scope: Limited to moderators only, for test/development purposes
|
|
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, includeConflicts = false, includeVersionChains = false, 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, conflicts: 0, versionChains: 0 };
|
|
const createdParks: string[] = [];
|
|
const createdCompanies: Record<string, string[]> = { manufacturer: [], operator: [], designer: [], property_owner: [] };
|
|
const createdParkSlugs: string[] = [];
|
|
const createdRideSlugs: string[] = [];
|
|
|
|
// 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 = userId;
|
|
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<string, string> = {
|
|
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++) {
|
|
// Determine if this should be a conflict or version chain
|
|
const shouldConflict = includeConflicts && createdParkSlugs.length > 0 && Math.random() < 0.15;
|
|
const shouldVersionChain = includeVersionChains && createdParkSlugs.length > 0 && Math.random() < 0.15;
|
|
|
|
let slug = `test-park-${i + 1}`;
|
|
if (shouldConflict) {
|
|
// Reuse an existing slug to create a conflict
|
|
slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)];
|
|
summary.conflicts++;
|
|
} else if (shouldVersionChain) {
|
|
// Reuse an existing slug for a version chain with different data
|
|
slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)];
|
|
summary.versionChains++;
|
|
}
|
|
|
|
const parkData = {
|
|
name: shouldVersionChain ? `Test Park ${slug} (Updated)` : `Test Park ${i + 1}`,
|
|
slug: slug,
|
|
description: shouldVersionChain ? 'Updated test park 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(slug);
|
|
if (!shouldConflict && !shouldVersionChain) {
|
|
createdParkSlugs.push(slug);
|
|
}
|
|
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++) {
|
|
// Determine if this should be a conflict or version chain
|
|
const shouldConflict = includeConflicts && createdRideSlugs.length > 0 && Math.random() < 0.15;
|
|
const shouldVersionChain = includeVersionChains && createdRideSlugs.length > 0 && Math.random() < 0.15;
|
|
|
|
let slug = `test-ride-${i + 1}`;
|
|
if (shouldConflict) {
|
|
slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)];
|
|
summary.conflicts++;
|
|
} else if (shouldVersionChain) {
|
|
slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)];
|
|
summary.versionChains++;
|
|
}
|
|
|
|
// 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: shouldVersionChain ? `Test Ride ${slug} (Updated)` : `Test Ride ${i + 1}`,
|
|
slug: slug,
|
|
description: shouldVersionChain ? 'Updated test ride 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);
|
|
if (!shouldConflict && !shouldVersionChain) {
|
|
createdRideSlugs.push(slug);
|
|
}
|
|
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);
|
|
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
|
return new Response(
|
|
JSON.stringify({ error: errorMessage }),
|
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
});
|