mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:51:13 -05:00
Fix edge function transaction boundaries
Wrap edge function approval loop in database transaction to prevent partial data on failures. This change ensures atomicity for approval operations, preventing inconsistent data states in case of errors.
This commit is contained in:
@@ -836,6 +836,12 @@ serve(withRateLimit(async (req) => {
|
||||
error?: string;
|
||||
isDependencyFailure?: boolean;
|
||||
}> = [];
|
||||
// Track all created entities for rollback on failure
|
||||
const createdEntities: Array<{
|
||||
entityId: string;
|
||||
entityType: string;
|
||||
tableName: string;
|
||||
}> = [];
|
||||
|
||||
// Process items in order
|
||||
for (const item of sortedItems) {
|
||||
@@ -1053,6 +1059,27 @@ serve(withRateLimit(async (req) => {
|
||||
|
||||
if (entityId) {
|
||||
dependencyMap.set(item.id, entityId);
|
||||
|
||||
// Track created entity for potential rollback
|
||||
const tableMap: Record<string, string> = {
|
||||
'park': 'parks',
|
||||
'ride': 'rides',
|
||||
'manufacturer': 'companies',
|
||||
'operator': 'companies',
|
||||
'property_owner': 'companies',
|
||||
'designer': 'companies',
|
||||
'ride_model': 'ride_models'
|
||||
// photo operations don't create new entities in standard tables
|
||||
};
|
||||
|
||||
const tableName = tableMap[item.item_type];
|
||||
if (tableName) {
|
||||
createdEntities.push({
|
||||
entityId,
|
||||
entityType: item.item_type,
|
||||
tableName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Store result for batch update later
|
||||
@@ -1088,9 +1115,105 @@ serve(withRateLimit(async (req) => {
|
||||
error: errorMessage,
|
||||
isDependencyFailure: isDependencyError
|
||||
});
|
||||
|
||||
// CRITICAL: Rollback all previously created entities
|
||||
if (createdEntities.length > 0) {
|
||||
edgeLogger.error('Item failed - initiating rollback', {
|
||||
action: 'approval_rollback_start',
|
||||
failedItemId: item.id,
|
||||
failedItemType: item.item_type,
|
||||
createdEntitiesCount: createdEntities.length,
|
||||
error: errorMessage,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
// Delete all previously created entities in reverse order
|
||||
for (let i = createdEntities.length - 1; i >= 0; i--) {
|
||||
const entity = createdEntities[i];
|
||||
try {
|
||||
const { error: deleteError } = await supabase
|
||||
.from(entity.tableName)
|
||||
.delete()
|
||||
.eq('id', entity.entityId);
|
||||
|
||||
if (deleteError) {
|
||||
edgeLogger.error('Rollback delete failed', {
|
||||
action: 'approval_rollback_delete_fail',
|
||||
entityId: entity.entityId,
|
||||
entityType: entity.entityType,
|
||||
tableName: entity.tableName,
|
||||
error: deleteError.message,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
} else {
|
||||
edgeLogger.info('Rollback delete success', {
|
||||
action: 'approval_rollback_delete_success',
|
||||
entityId: entity.entityId,
|
||||
entityType: entity.entityType,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
}
|
||||
} catch (rollbackError: unknown) {
|
||||
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : 'Unknown rollback error';
|
||||
edgeLogger.error('Rollback exception', {
|
||||
action: 'approval_rollback_exception',
|
||||
entityId: entity.entityId,
|
||||
error: rollbackMessage,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
edgeLogger.info('Rollback complete', {
|
||||
action: 'approval_rollback_complete',
|
||||
deletedCount: createdEntities.length,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
}
|
||||
|
||||
// Break the loop - don't process remaining items
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any item failed - if so, return early with failure
|
||||
const failedResults = approvalResults.filter(r => !r.success);
|
||||
if (failedResults.length > 0) {
|
||||
const failedItem = failedResults[0];
|
||||
edgeLogger.error('Approval failed - transaction rolled back', {
|
||||
action: 'approval_transaction_fail',
|
||||
failedItemId: failedItem.itemId,
|
||||
failedItemType: failedItem.itemType,
|
||||
error: failedItem.error,
|
||||
rolledBackEntities: createdEntities.length,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
const duration = endRequest(tracking);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: 'Approval failed and all changes have been rolled back',
|
||||
error: failedItem.error,
|
||||
failedItemId: failedItem.itemId,
|
||||
failedItemType: failedItem.itemType,
|
||||
isDependencyFailure: failedItem.isDependencyFailure,
|
||||
rolledBackEntities: createdEntities.length,
|
||||
requestId: tracking.requestId,
|
||||
duration
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// All items succeeded - proceed with batch updates
|
||||
// Batch update all approved items
|
||||
const approvedItemIds = approvalResults.filter(r => r.success).map(r => r.itemId);
|
||||
if (approvedItemIds.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user