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:
gpt-engineer-app[bot]
2025-11-06 16:11:52 +00:00
parent 41a396b063
commit 1cc80e0dc4

View File

@@ -836,6 +836,12 @@ serve(withRateLimit(async (req) => {
error?: string; error?: string;
isDependencyFailure?: boolean; isDependencyFailure?: boolean;
}> = []; }> = [];
// Track all created entities for rollback on failure
const createdEntities: Array<{
entityId: string;
entityType: string;
tableName: string;
}> = [];
// Process items in order // Process items in order
for (const item of sortedItems) { for (const item of sortedItems) {
@@ -1053,6 +1059,27 @@ serve(withRateLimit(async (req) => {
if (entityId) { if (entityId) {
dependencyMap.set(item.id, 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 // Store result for batch update later
@@ -1088,9 +1115,105 @@ serve(withRateLimit(async (req) => {
error: errorMessage, error: errorMessage,
isDependencyFailure: isDependencyError 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 // Batch update all approved items
const approvedItemIds = approvalResults.filter(r => r.success).map(r => r.itemId); const approvedItemIds = approvalResults.filter(r => r.success).map(r => r.itemId);
if (approvedItemIds.length > 0) { if (approvedItemIds.length > 0) {