Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/sim/app/api/folders/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 6 additions & 18 deletions apps/sim/app/api/workflows/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -291,7 +283,9 @@ describe('Workflow By ID API Route', () => {
}),
}),
}),
transaction: mockTransaction,
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
},
}))

Expand Down Expand Up @@ -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({
Expand All @@ -343,7 +329,9 @@ describe('Workflow By ID API Route', () => {
}),
}),
}),
transaction: mockTransaction,
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
},
}))

Expand Down
13 changes: 2 additions & 11 deletions apps/sim/app/api/workflows/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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`)
Expand Down
26 changes: 5 additions & 21 deletions apps/sim/app/api/workspaces/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}
}

Expand Down
35 changes: 11 additions & 24 deletions apps/sim/stores/folders/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -323,33 +324,19 @@ export const useFolderStore = create<FolderState>()(
return { expandedFolders: newExpanded }
})

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)
}
}
// 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 (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) => {
Expand Down