feat: Add retry logic to updates

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 13:56:08 +00:00
parent 80826a83a8
commit dcc9e2af8f

View File

@@ -698,26 +698,41 @@ export async function submitParkUpdate(
data: ParkFormData, data: ParkFormData,
userId: string userId: string
): Promise<{ submitted: boolean; submissionId: string }> { ): Promise<{ submitted: boolean; submissionId: string }> {
// Check if user is banned const { withRetry, isRetryableError } = await import('./retryHelpers');
const { data: profile } = await supabase
.from('profiles') // Check if user is banned - with retry for transient failures
.select('banned') const profile = await withRetry(
.eq('user_id', userId) async () => {
.single(); const { data: profile } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', userId)
.single();
return profile;
},
{ maxAttempts: 2 }
);
if (profile?.banned) { if (profile?.banned) {
throw new Error('Account suspended. Contact support for assistance.'); throw new Error('Account suspended. Contact support for assistance.');
} }
// Fetch existing park data first // Fetch existing park data first - with retry for transient failures
const { data: existingPark, error: fetchError } = await supabase const existingPark = await withRetry(
.from('parks') async () => {
.select('id, name, slug, description, park_type, status, opening_date, opening_date_precision, closing_date, closing_date_precision, website_url, phone, email, location_id, operator_id, property_owner_id, banner_image_url, banner_image_id, card_image_url, card_image_id') const { data: existingPark, error: fetchError } = await supabase
.eq('id', parkId) .from('parks')
.single(); .select('id, name, slug, description, park_type, status, opening_date, opening_date_precision, closing_date, closing_date_precision, website_url, phone, email, location_id, operator_id, property_owner_id, banner_image_url, banner_image_id, card_image_url, card_image_id')
.eq('id', parkId)
.single();
if (fetchError) throw new Error(`Failed to fetch park: ${fetchError.message}`); if (fetchError) throw new Error(`Failed to fetch park: ${fetchError.message}`);
if (!existingPark) throw new Error('Park not found'); if (!existingPark) throw new Error('Park not found');
return existingPark;
},
{ maxAttempts: 2 }
);
// CRITICAL: Block new photo uploads on edits // CRITICAL: Block new photo uploads on edits
// Photos can only be submitted during creation or via the photo gallery // Photos can only be submitted during creation or via the photo gallery
@@ -728,43 +743,87 @@ export async function submitParkUpdate(
// Only allow banner/card reassignments from existing photos // Only allow banner/card reassignments from existing photos
let processedImages = data.images; let processedImages = data.images;
// Create the main submission record // Main submission logic with retry and error handling
const { data: submissionData, error: submissionError } = await supabase const result = await withRetry(
.from('content_submissions') async () => {
.insert({ // Create the main submission record
user_id: userId, const { data: submissionData, error: submissionError } = await supabase
submission_type: 'park', .from('content_submissions')
content: { .insert({
action: 'edit', user_id: userId,
park_id: parkId submission_type: 'park',
content: {
action: 'edit',
park_id: parkId
},
status: 'pending' as const
})
.select('id')
.single();
if (submissionError) throw submissionError;
// Create the submission item with actual park data AND original data
const { error: itemError } = await supabase
.from('submission_items')
.insert({
submission_id: submissionData.id,
item_type: 'park',
action_type: 'edit',
item_data: JSON.parse(JSON.stringify({
...extractChangedFields(data, existingPark as any),
park_id: parkId, // Always include for relational integrity
images: processedImages
})) as Json,
original_data: JSON.parse(JSON.stringify(existingPark)),
status: 'pending' as const,
order_index: 0
});
if (itemError) throw itemError;
return { submitted: true, submissionId: submissionData.id };
},
{
maxAttempts: 3,
onRetry: (attempt, error, delay) => {
logger.warn('Retrying park update submission', {
attempt,
delay,
parkId,
error: error instanceof Error ? error.message : String(error)
});
// Emit event for UI retry indicator
window.dispatchEvent(new CustomEvent('submission-retry', {
detail: { attempt, maxAttempts: 3, delay, type: 'park update' }
}));
}, },
status: 'pending' as const shouldRetry: (error) => {
}) // Don't retry validation/business logic errors
.select('id') if (error instanceof Error) {
.single(); const message = error.message.toLowerCase();
if (message.includes('required')) return false;
if (submissionError) throw submissionError; if (message.includes('banned')) return false;
if (message.includes('slug')) return false;
// Create the submission item with actual park data AND original data if (message.includes('permission')) return false;
const { error: itemError } = await supabase if (message.includes('not found')) return false;
.from('submission_items') if (message.includes('not allowed')) return false;
.insert({ }
submission_id: submissionData.id,
item_type: 'park', return isRetryableError(error);
action_type: 'edit', }
item_data: JSON.parse(JSON.stringify({ }
...extractChangedFields(data, existingPark as any), ).catch((error) => {
park_id: parkId, // Always include for relational integrity handleError(error, {
images: processedImages action: 'Park update submission',
})) as Json, userId,
original_data: JSON.parse(JSON.stringify(existingPark)), metadata: { retriesExhausted: true, parkId },
status: 'pending' as const,
order_index: 0
}); });
throw error;
});
if (itemError) throw itemError; return result;
return { submitted: true, submissionId: submissionData.id };
} }
/** /**
@@ -1073,26 +1132,41 @@ export async function submitRideUpdate(
data: RideFormData, data: RideFormData,
userId: string userId: string
): Promise<{ submitted: boolean; submissionId: string }> { ): Promise<{ submitted: boolean; submissionId: string }> {
// Check if user is banned const { withRetry, isRetryableError } = await import('./retryHelpers');
const { data: profile } = await supabase
.from('profiles') // Check if user is banned - with retry for transient failures
.select('banned') const profile = await withRetry(
.eq('user_id', userId) async () => {
.single(); const { data: profile } = await supabase
.from('profiles')
.select('banned')
.eq('user_id', userId)
.single();
return profile;
},
{ maxAttempts: 2 }
);
if (profile?.banned) { if (profile?.banned) {
throw new Error('Account suspended. Contact support for assistance.'); throw new Error('Account suspended. Contact support for assistance.');
} }
// Fetch existing ride data first // Fetch existing ride data first - with retry for transient failures
const { data: existingRide, error: fetchError } = await supabase const existingRide = await withRetry(
.from('rides') async () => {
.select('*') const { data: existingRide, error: fetchError } = await supabase
.eq('id', rideId) .from('rides')
.single(); .select('*')
.eq('id', rideId)
.single();
if (fetchError) throw new Error(`Failed to fetch ride: ${fetchError.message}`); if (fetchError) throw new Error(`Failed to fetch ride: ${fetchError.message}`);
if (!existingRide) throw new Error('Ride not found'); if (!existingRide) throw new Error('Ride not found');
return existingRide;
},
{ maxAttempts: 2 }
);
// CRITICAL: Block new photo uploads on edits // CRITICAL: Block new photo uploads on edits
// Photos can only be submitted during creation or via the photo gallery // Photos can only be submitted during creation or via the photo gallery
@@ -1103,43 +1177,87 @@ export async function submitRideUpdate(
// Only allow banner/card reassignments from existing photos // Only allow banner/card reassignments from existing photos
let processedImages = data.images; let processedImages = data.images;
// Create the main submission record // Main submission logic with retry and error handling
const { data: submissionData, error: submissionError } = await supabase const result = await withRetry(
.from('content_submissions') async () => {
.insert({ // Create the main submission record
user_id: userId, const { data: submissionData, error: submissionError } = await supabase
submission_type: 'ride', .from('content_submissions')
content: { .insert({
action: 'edit', user_id: userId,
ride_id: rideId submission_type: 'ride',
}, content: {
status: 'pending' as const action: 'edit',
}) ride_id: rideId
.select() },
.single(); status: 'pending' as const
})
.select()
.single();
if (submissionError) throw submissionError; if (submissionError) throw submissionError;
// Create the submission item with actual ride data AND original data // Create the submission item with actual ride data AND original data
const { error: itemError } = await supabase const { error: itemError } = await supabase
.from('submission_items') .from('submission_items')
.insert({ .insert({
submission_id: submissionData.id, submission_id: submissionData.id,
item_type: 'ride', item_type: 'ride',
action_type: 'edit', action_type: 'edit',
item_data: { item_data: {
...extractChangedFields(data, existingRide as any), ...extractChangedFields(data, existingRide as any),
ride_id: rideId, // Always include for relational integrity ride_id: rideId, // Always include for relational integrity
images: processedImages as unknown as Json images: processedImages as unknown as Json
},
original_data: JSON.parse(JSON.stringify(existingRide)),
status: 'pending' as const,
order_index: 0
});
if (itemError) throw itemError;
return { submitted: true, submissionId: submissionData.id };
},
{
maxAttempts: 3,
onRetry: (attempt, error, delay) => {
logger.warn('Retrying ride update submission', {
attempt,
delay,
rideId,
error: error instanceof Error ? error.message : String(error)
});
// Emit event for UI retry indicator
window.dispatchEvent(new CustomEvent('submission-retry', {
detail: { attempt, maxAttempts: 3, delay, type: 'ride update' }
}));
}, },
original_data: JSON.parse(JSON.stringify(existingRide)), shouldRetry: (error) => {
status: 'pending' as const, // Don't retry validation/business logic errors
order_index: 0 if (error instanceof Error) {
const message = error.message.toLowerCase();
if (message.includes('required')) return false;
if (message.includes('banned')) return false;
if (message.includes('slug')) return false;
if (message.includes('permission')) return false;
if (message.includes('not found')) return false;
if (message.includes('not allowed')) return false;
}
return isRetryableError(error);
}
}
).catch((error) => {
handleError(error, {
action: 'Ride update submission',
userId,
metadata: { retriesExhausted: true, rideId },
}); });
throw error;
});
if (itemError) throw itemError; return result;
return { submitted: true, submissionId: submissionData.id };
} }
/** /**