From 1cc80e0dc4e90c51619d1fa2ee7084cf01b3b976 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:11:52 +0000 Subject: [PATCH] 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. --- .../process-selective-approval/index.ts | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 2e08f2c7..f4277bd9 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -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 = { + '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) {