mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:11:17 -05:00
feat: Add retry logic to updates
This commit is contained in:
@@ -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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user