fix(deletions): folder deletions were hanging + use cascade deletions throughout (#620)

* use cascade deletion

* fix lint

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>
This commit is contained in:
Vikhyath Mondreti
2025-07-06 20:01:28 -07:00
committed by Vikhyath Mondreti
co-authored by Vikhyath Mondreti
parent cac9ad250d
commit 231bfb9add
6 changed files with 33 additions and 77 deletions
+1
View File
@@ -160,6 +160,7 @@ async function deleteFolderRecursively(
}
// Delete all workflows in this folder (workspace-scoped, not user-scoped)
// The database cascade will handle deleting related workflow_blocks, workflow_edges, workflow_subflows
const workflowsInFolder = await db
.select({ id: workflow.id })
.from(workflow)
+6 -18
View File
@@ -274,14 +274,6 @@ describe('Workflow By ID API Route', () => {
}),
}))
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
await callback({
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
})
})
vi.doMock('@/db', () => ({
db: {
select: vi.fn().mockReturnValue({
@@ -291,7 +283,9 @@ describe('Workflow By ID API Route', () => {
}),
}),
}),
transaction: mockTransaction,
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
},
}))
@@ -326,14 +320,6 @@ describe('Workflow By ID API Route', () => {
}),
}))
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
await callback({
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
})
})
vi.doMock('@/db', () => ({
db: {
select: vi.fn().mockReturnValue({
@@ -343,7 +329,9 @@ describe('Workflow By ID API Route', () => {
}),
}),
}),
transaction: mockTransaction,
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
},
}))
+2 -11
View File
@@ -7,7 +7,7 @@ import { createLogger } from '@/lib/logs/console-logger'
import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
import { workflow } from '@/db/schema'
const logger = createLogger('WorkflowByIdAPI')
@@ -206,16 +206,7 @@ export async function DELETE(
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Delete workflow and all related data in a transaction
await db.transaction(async (tx) => {
// Delete from normalized tables first (foreign key constraints)
await tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId))
await tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId))
await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId))
// Delete the main workflow record
await tx.delete(workflow).where(eq(workflow.id, workflowId))
})
await db.delete(workflow).where(eq(workflow.id, workflowId))
const elapsed = Date.now() - startTime
logger.info(`[${requestId}] Successfully deleted workflow ${workflowId} in ${elapsed}ms`)
+5 -21
View File
@@ -2,13 +2,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import {
workflow,
workflowBlocks,
workflowEdges,
workflowSubflows,
workspaceMember,
} from '@/db/schema'
import { workflow, workspaceMember } from '@/db/schema'
const logger = createLogger('WorkspaceByIdAPI')
@@ -126,20 +120,10 @@ export async function DELETE(
// Delete workspace and all related data in a transaction
await db.transaction(async (tx) => {
// Get all workflows in this workspace
const workspaceWorkflows = await tx
.select({ id: workflow.id })
.from(workflow)
.where(eq(workflow.workspaceId, workspaceId))
// Delete all workflow-related data for each workflow
for (const wf of workspaceWorkflows) {
await tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, wf.id))
await tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, wf.id))
await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, wf.id))
}
// Delete all workflows in the workspace
// Delete all workflows in the workspace - database cascade will handle all workflow-related data
// The database cascade will handle deleting related workflow_blocks, workflow_edges, workflow_subflows,
// workflow_logs, workflow_execution_snapshots, workflow_execution_logs, workflow_execution_trace_spans,
// workflow_schedule, webhook, marketplace, chat, and memory records
await tx.delete(workflow).where(eq(workflow.workspaceId, workspaceId))
// Delete workspace members
@@ -63,12 +63,17 @@ export function FolderContextMenu({
setShowRenameDialog(true)
}
const handleDelete = () => {
const handleDelete = async () => {
if (onDelete) {
onDelete(folderId)
} else {
// Default delete behavior
deleteFolder(folderId, workspaceId)
// Default delete behavior with proper error handling
try {
await deleteFolder(folderId, workspaceId)
logger.info(`Successfully deleted folder from context menu: ${folderName}`)
} catch (error) {
logger.error('Failed to delete folder from context menu:', { error, folderId, folderName })
}
}
}
+11 -24
View File
@@ -314,6 +314,7 @@ export const useFolderStore = create<FolderState>()(
const responseData = await response.json()
// Remove the folder from local state
get().removeFolder(id)
// Remove from expanded state
@@ -323,33 +324,19 @@ export const useFolderStore = create<FolderState>()(
return { expandedFolders: newExpanded }
})
// Remove subfolders from local state
get().removeSubfoldersRecursively(id)
// The backend has already deleted the workflows, so we just need to refresh
// the workflow registry to sync with the server state
const workflowRegistry = useWorkflowRegistry.getState()
if (responseData.deletedItems) {
try {
const workflows = Object.values(workflowRegistry.workflows)
const workflowsToDelete = workflows.filter(
(workflow) =>
workflow.folderId === id || get().isWorkflowInDeletedSubfolder(workflow, id)
)
workflowsToDelete.forEach((workflow) => {
workflowRegistry.removeWorkflow(workflow.id)
})
get().removeSubfoldersRecursively(id)
logger.info(
`Deleted ${responseData.deletedItems.workflows} workflow(s) and ${responseData.deletedItems.folders} folder(s)`
)
} catch (error) {
logger.error('Error updating local state after folder deletion:', error)
}
}
if (workspaceId) {
// Trigger workflow refresh through registry store
await workflowRegistry.switchToWorkspace(workspaceId)
await workflowRegistry.loadWorkflows(workspaceId)
}
logger.info(
`Deleted ${responseData.deletedItems.workflows} workflow(s) and ${responseData.deletedItems.folders} folder(s)`
)
},
isWorkflowInDeletedSubfolder: (workflow: Workflow, deletedFolderId: string) => {