mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:51:13 -05:00
Improve image upload and test data generation functionalities
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
This commit is contained in:
@@ -9,92 +9,113 @@ export interface CloudflareUploadResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// Internal type to track upload status
|
||||
interface UploadedImageWithFlag extends UploadedImage {
|
||||
wasNewlyUploaded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads pending local images to Cloudflare via Supabase Edge Function
|
||||
* @param images Array of UploadedImage objects (mix of local and already uploaded)
|
||||
* @returns Array of UploadedImage objects with all images uploaded
|
||||
*/
|
||||
export async function uploadPendingImages(images: UploadedImage[]): Promise<UploadedImage[]> {
|
||||
const uploadedImages: UploadedImage[] = [];
|
||||
const newlyUploadedIds: string[] = []; // Track newly uploaded IDs for cleanup on error
|
||||
let currentImageIndex = 0;
|
||||
// Process all images in parallel for better performance using allSettled
|
||||
const uploadPromises = images.map(async (image, index): Promise<UploadedImageWithFlag> => {
|
||||
if (image.isLocal && image.file) {
|
||||
// Step 1: Get upload URL from our Supabase Edge Function
|
||||
const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', {
|
||||
body: { action: 'get-upload-url' }
|
||||
});
|
||||
|
||||
try {
|
||||
for (const image of images) {
|
||||
if (image.isLocal && image.file) {
|
||||
// Step 1: Get upload URL from our Supabase Edge Function
|
||||
const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', {
|
||||
body: { action: 'get-upload-url' }
|
||||
});
|
||||
|
||||
if (urlError || !uploadUrlData?.uploadURL) {
|
||||
throw new Error(`Failed to get upload URL: ${urlError?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Step 2: Upload file directly to Cloudflare
|
||||
const formData = new FormData();
|
||||
formData.append('file', image.file);
|
||||
|
||||
const uploadResponse = await fetch(uploadUrlData.uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const errorText = await uploadResponse.text();
|
||||
throw new Error(`Upload failed (status ${uploadResponse.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
const result: CloudflareUploadResponse = await uploadResponse.json();
|
||||
|
||||
if (!result.success || !result.result) {
|
||||
throw new Error('Cloudflare upload returned unsuccessful response');
|
||||
}
|
||||
|
||||
// Track this newly uploaded image
|
||||
newlyUploadedIds.push(result.result.id);
|
||||
|
||||
// Step 3: Return uploaded image metadata
|
||||
uploadedImages.push({
|
||||
url: result.result.variants[0], // Use first variant (usually the original)
|
||||
cloudflare_id: result.result.id,
|
||||
caption: image.caption,
|
||||
isLocal: false,
|
||||
});
|
||||
|
||||
// Clean up object URL
|
||||
URL.revokeObjectURL(image.url);
|
||||
} else {
|
||||
// Already uploaded, keep as is
|
||||
uploadedImages.push({
|
||||
url: image.url,
|
||||
cloudflare_id: image.cloudflare_id,
|
||||
caption: image.caption,
|
||||
isLocal: false,
|
||||
});
|
||||
if (urlError || !uploadUrlData?.uploadURL) {
|
||||
throw new Error(`Failed to get upload URL for image ${index + 1}: ${urlError?.message || 'Unknown error'}`);
|
||||
}
|
||||
currentImageIndex++;
|
||||
}
|
||||
|
||||
return uploadedImages;
|
||||
} catch (error) {
|
||||
// Cleanup: Attempt to delete newly uploaded images on error
|
||||
if (newlyUploadedIds.length > 0) {
|
||||
console.error(`Upload failed at image ${currentImageIndex + 1}. Cleaning up ${newlyUploadedIds.length} uploaded images...`);
|
||||
// Step 2: Upload file directly to Cloudflare
|
||||
const formData = new FormData();
|
||||
formData.append('file', image.file);
|
||||
|
||||
const uploadResponse = await fetch(uploadUrlData.uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const errorText = await uploadResponse.text();
|
||||
throw new Error(`Upload failed for image ${index + 1} (status ${uploadResponse.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
const result: CloudflareUploadResponse = await uploadResponse.json();
|
||||
|
||||
if (!result.success || !result.result) {
|
||||
throw new Error(`Cloudflare upload returned unsuccessful response for image ${index + 1}`);
|
||||
}
|
||||
|
||||
// Clean up object URL
|
||||
URL.revokeObjectURL(image.url);
|
||||
|
||||
// Step 3: Return uploaded image metadata with wasNewlyUploaded flag
|
||||
return {
|
||||
url: result.result.variants[0], // Use first variant (usually the original)
|
||||
cloudflare_id: result.result.id,
|
||||
caption: image.caption,
|
||||
isLocal: false,
|
||||
wasNewlyUploaded: true // Flag to track newly uploaded images
|
||||
};
|
||||
} else {
|
||||
// Already uploaded, keep as is
|
||||
return {
|
||||
url: image.url,
|
||||
cloudflare_id: image.cloudflare_id,
|
||||
caption: image.caption,
|
||||
isLocal: false,
|
||||
wasNewlyUploaded: false // Pre-existing image
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all uploads to settle (succeed or fail)
|
||||
const results = await Promise.allSettled(uploadPromises);
|
||||
|
||||
// Separate successful and failed uploads
|
||||
const successfulUploads: UploadedImageWithFlag[] = [];
|
||||
const newlyUploadedImageIds: string[] = []; // Track ONLY newly uploaded images for cleanup
|
||||
const errors: string[] = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const uploadedImage = result.value;
|
||||
successfulUploads.push(uploadedImage);
|
||||
// Only track newly uploaded images for potential cleanup
|
||||
if (uploadedImage.wasNewlyUploaded && uploadedImage.cloudflare_id) {
|
||||
newlyUploadedImageIds.push(uploadedImage.cloudflare_id);
|
||||
}
|
||||
} else {
|
||||
errors.push(result.reason?.message || `Upload ${index + 1} failed`);
|
||||
}
|
||||
});
|
||||
|
||||
// If any uploads failed, clean up ONLY newly uploaded images and throw error
|
||||
if (errors.length > 0) {
|
||||
if (newlyUploadedImageIds.length > 0) {
|
||||
console.error(`Some uploads failed. Cleaning up ${newlyUploadedImageIds.length} newly uploaded images...`);
|
||||
|
||||
// Attempt cleanup but don't throw if it fails
|
||||
for (const imageId of newlyUploadedIds) {
|
||||
try {
|
||||
await supabase.functions.invoke('upload-image', {
|
||||
// Attempt cleanup in parallel but don't throw if it fails
|
||||
await Promise.allSettled(
|
||||
newlyUploadedImageIds.map(imageId =>
|
||||
supabase.functions.invoke('upload-image', {
|
||||
body: { action: 'delete', imageId }
|
||||
});
|
||||
} catch (cleanupError) {
|
||||
console.error(`Failed to cleanup image ${imageId}:`, cleanupError);
|
||||
}
|
||||
}
|
||||
}).catch(cleanupError => {
|
||||
console.error(`Failed to cleanup image ${imageId}:`, cleanupError);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new Error(`Failed to upload image ${currentImageIndex + 1} of ${images.length}: ${errorMessage}`);
|
||||
throw new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`);
|
||||
}
|
||||
|
||||
// Remove the wasNewlyUploaded flag before returning
|
||||
return successfulUploads.map(({ wasNewlyUploaded, ...image }) => image as UploadedImage);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user