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:
pac7
2025-10-08 18:14:34 +00:00
parent bdc9f5695e
commit 3832439d67
6 changed files with 164 additions and 91 deletions

View File

@@ -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);
}