@@ -10,7 +10,7 @@ import {
1010 workflowSchedule ,
1111} from '@sim/db/schema'
1212import { createLogger } from '@sim/logger'
13- import { and , eq , inArray , isNotNull , isNull } from 'drizzle-orm'
13+ import { and , eq , gte , inArray , isNotNull , isNull } from 'drizzle-orm'
1414import { AuditAction , AuditResourceType , recordAudit } from '@/lib/audit/log'
1515import { archiveWorkflowsByIdsInWorkspace } from '@/lib/workflows/lifecycle'
1616import type { OrchestrationErrorCode } from '@/lib/workflows/orchestration/types'
@@ -179,26 +179,31 @@ export async function performDeleteFolder(
179179}
180180
181181/**
182- * Recursively restores a folder and its archived children: unarchives child folders,
183- * then restores all archived workflows in each folder.
182+ * Recursively restores a folder and its archived children within a transaction.
183+ * Only restores workflows archived around the same time as the folder (within 5s),
184+ * so individually-deleted workflows are not silently un-deleted.
184185 */
185186async function restoreFolderRecursively (
186187 folderId : string ,
187- workspaceId : string
188+ workspaceId : string ,
189+ folderArchivedAt : Date ,
190+ tx : Parameters < Parameters < typeof db . transaction > [ 0 ] > [ 0 ]
188191) : Promise < { folders : number ; workflows : number } > {
189192 const stats = { folders : 0 , workflows : 0 }
190193
191- await db . update ( workflowFolder ) . set ( { archivedAt : null } ) . where ( eq ( workflowFolder . id , folderId ) )
194+ await tx . update ( workflowFolder ) . set ( { archivedAt : null } ) . where ( eq ( workflowFolder . id , folderId ) )
192195 stats . folders += 1
193196
194- const archivedWorkflows = await db
197+ const archiveWindowStart = new Date ( folderArchivedAt . getTime ( ) - 5_000 )
198+ const archivedWorkflows = await tx
195199 . select ( { id : workflow . id } )
196200 . from ( workflow )
197201 . where (
198202 and (
199203 eq ( workflow . folderId , folderId ) ,
200204 eq ( workflow . workspaceId , workspaceId ) ,
201- isNotNull ( workflow . archivedAt )
205+ isNotNull ( workflow . archivedAt ) ,
206+ gte ( workflow . archivedAt , archiveWindowStart )
202207 )
203208 )
204209
@@ -207,27 +212,25 @@ async function restoreFolderRecursively(
207212 const now = new Date ( )
208213 const restoreSet = { archivedAt : null , updatedAt : now }
209214
210- await db . transaction ( async ( tx ) => {
211- await tx . update ( workflow ) . set ( restoreSet ) . where ( inArray ( workflow . id , workflowIds ) )
212- await tx
213- . update ( workflowSchedule )
214- . set ( restoreSet )
215- . where ( inArray ( workflowSchedule . workflowId , workflowIds ) )
216- await tx . update ( webhook ) . set ( restoreSet ) . where ( inArray ( webhook . workflowId , workflowIds ) )
217- await tx . update ( chat ) . set ( restoreSet ) . where ( inArray ( chat . workflowId , workflowIds ) )
218- await tx . update ( form ) . set ( restoreSet ) . where ( inArray ( form . workflowId , workflowIds ) )
219- await tx
220- . update ( workflowMcpTool )
221- . set ( restoreSet )
222- . where ( inArray ( workflowMcpTool . workflowId , workflowIds ) )
223- await tx . update ( a2aAgent ) . set ( restoreSet ) . where ( inArray ( a2aAgent . workflowId , workflowIds ) )
224- } )
215+ await tx . update ( workflow ) . set ( restoreSet ) . where ( inArray ( workflow . id , workflowIds ) )
216+ await tx
217+ . update ( workflowSchedule )
218+ . set ( restoreSet )
219+ . where ( inArray ( workflowSchedule . workflowId , workflowIds ) )
220+ await tx . update ( webhook ) . set ( restoreSet ) . where ( inArray ( webhook . workflowId , workflowIds ) )
221+ await tx . update ( chat ) . set ( restoreSet ) . where ( inArray ( chat . workflowId , workflowIds ) )
222+ await tx . update ( form ) . set ( restoreSet ) . where ( inArray ( form . workflowId , workflowIds ) )
223+ await tx
224+ . update ( workflowMcpTool )
225+ . set ( restoreSet )
226+ . where ( inArray ( workflowMcpTool . workflowId , workflowIds ) )
227+ await tx . update ( a2aAgent ) . set ( restoreSet ) . where ( inArray ( a2aAgent . workflowId , workflowIds ) )
225228
226229 stats . workflows += archivedWorkflows . length
227230 }
228231
229- const archivedChildren = await db
230- . select ( { id : workflowFolder . id } )
232+ const archivedChildren = await tx
233+ . select ( { id : workflowFolder . id , archivedAt : workflowFolder . archivedAt } )
231234 . from ( workflowFolder )
232235 . where (
233236 and (
@@ -238,7 +241,12 @@ async function restoreFolderRecursively(
238241 )
239242
240243 for ( const child of archivedChildren ) {
241- const childStats = await restoreFolderRecursively ( child . id , workspaceId )
244+ const childStats = await restoreFolderRecursively (
245+ child . id ,
246+ workspaceId ,
247+ child . archivedAt ! ,
248+ tx
249+ )
242250 stats . folders += childStats . folders
243251 stats . workflows += childStats . workflows
244252 }
@@ -283,18 +291,23 @@ export async function performRestoreFolder(
283291 return { success : false , error : 'Folder is not archived' }
284292 }
285293
286- if ( folder . parentId ) {
287- const [ parentFolder ] = await db
288- . select ( { archivedAt : workflowFolder . archivedAt } )
289- . from ( workflowFolder )
290- . where ( eq ( workflowFolder . id , folder . parentId ) )
291-
292- if ( parentFolder ?. archivedAt ) {
293- await db . update ( workflowFolder ) . set ( { parentId : null } ) . where ( eq ( workflowFolder . id , folderId ) )
294+ const restoredStats = await db . transaction ( async ( tx ) => {
295+ if ( folder . parentId ) {
296+ const [ parentFolder ] = await tx
297+ . select ( { archivedAt : workflowFolder . archivedAt } )
298+ . from ( workflowFolder )
299+ . where ( eq ( workflowFolder . id , folder . parentId ) )
300+
301+ if ( parentFolder ?. archivedAt ) {
302+ await tx
303+ . update ( workflowFolder )
304+ . set ( { parentId : null } )
305+ . where ( eq ( workflowFolder . id , folderId ) )
306+ }
294307 }
295- }
296308
297- const restoredStats = await restoreFolderRecursively ( folderId , workspaceId )
309+ return restoreFolderRecursively ( folderId , workspaceId , folder . archivedAt ! , tx )
310+ } )
298311
299312 logger . info ( 'Restored folder and all contents:' , { folderId, restoredStats } )
300313
0 commit comments