diff --git a/src/app/api/workflows/[id]/nodes/route.ts b/src/app/api/workflows/[id]/nodes/route.ts index 6fb96c3..af11249 100644 --- a/src/app/api/workflows/[id]/nodes/route.ts +++ b/src/app/api/workflows/[id]/nodes/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server" import { withWorkspace } from "@/lib/api/with-workspace" -import { addWorkflowNode } from "@/lib/db/workflows" +import { addWorkflowNode, deleteWorkflowNodes } from "@/lib/db/workflows" export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) { const result = await withWorkspace() @@ -20,3 +20,25 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: return NextResponse.json({ error: message }, { status }) } } + +export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { + const result = await withWorkspace() + if (result.error) return result.error + + const { id } = await params + const body = await request.json() + const nodeIds = Array.isArray(body?.nodeIds) + ? body.nodeIds.filter((nodeId: unknown): nodeId is string => typeof nodeId === "string") + : [] + + if (nodeIds.length === 0) { + return NextResponse.json({ error: "nodeIds must be a non-empty string array" }, { status: 400 }) + } + + try { + await deleteWorkflowNodes(id, nodeIds, result.workspace.id) + return NextResponse.json({ success: true }) + } catch { + return NextResponse.json({ error: "Workflow not found" }, { status: 404 }) + } +} diff --git a/src/components/builder/hooks/use-builder-graph-mutations.ts b/src/components/builder/hooks/use-builder-graph-mutations.ts index 4eda492..5a11f0f 100644 --- a/src/components/builder/hooks/use-builder-graph-mutations.ts +++ b/src/components/builder/hooks/use-builder-graph-mutations.ts @@ -302,12 +302,15 @@ export function useBuilderGraphMutations(options: { const ids = Array.isArray(nodeIds) ? nodeIds : nodeIds ? [nodeIds] : [] if (!ids.length || !workflowId || !workflow) return const previous = workflow - const results = await Promise.all( - ids.map((nodeId) => - safeFetch(`/api/workflows/${workflowId}/nodes/${nodeId}`, { method: "DELETE" }), - ), - ) - if (results.every((response) => response.ok)) { + const response = + ids.length === 1 + ? await safeFetch(`/api/workflows/${workflowId}/nodes/${ids[0]}`, { method: "DELETE" }) + : await safeFetch(`/api/workflows/${workflowId}/nodes`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ nodeIds: ids }), + }) + if (response.ok) { saveToHistory(previous) } mutateWorkflow(workflowId) diff --git a/src/lib/db/workflows.ts b/src/lib/db/workflows.ts index 3db67ee..3409fd1 100644 --- a/src/lib/db/workflows.ts +++ b/src/lib/db/workflows.ts @@ -123,17 +123,22 @@ export async function updateWorkflowNode( return await updateWorkflow(workflowId, { nodes: updatedNodes }, workspaceId) } -export async function deleteWorkflowNode( +export async function deleteWorkflowNodes( workflowId: string, - nodeId: string, + nodeIds: string[], workspaceId?: string, ): Promise { + if (nodeIds.length === 0) { + throw new Error("At least one node id is required") + } + const workflow = await getWorkflow(workflowId, workspaceId) if (!workflow) throw new Error("Workflow not found") - const updatedNodes = workflow.nodes.filter((node) => node.id !== nodeId) + const nodeIdsSet = new Set(nodeIds) + const updatedNodes = workflow.nodes.filter((node) => !nodeIdsSet.has(node.id)) const updatedConnections = workflow.connections.filter( - (conn) => conn.sourceId !== nodeId && conn.targetId !== nodeId, + (conn) => !nodeIdsSet.has(conn.sourceId) && !nodeIdsSet.has(conn.targetId), ) return await updateWorkflow( @@ -143,6 +148,14 @@ export async function deleteWorkflowNode( ) } +export async function deleteWorkflowNode( + workflowId: string, + nodeId: string, + workspaceId?: string, +): Promise { + return deleteWorkflowNodes(workflowId, [nodeId], workspaceId) +} + export async function addWorkflowConnection( workflowId: string, connection: Omit, diff --git a/tests/api/workflows.test.ts b/tests/api/workflows.test.ts index 3110dbf..e9a347c 100644 --- a/tests/api/workflows.test.ts +++ b/tests/api/workflows.test.ts @@ -9,6 +9,7 @@ import { addWorkflowNode, updateWorkflowNode, deleteWorkflowNode, + deleteWorkflowNodes, addWorkflowConnection, deleteWorkflowConnection, } from "@/lib/db/workflows" @@ -295,6 +296,36 @@ describe("lib/db/workflows", () => { }) }) + describe("deleteWorkflowNodes", () => { + it("removes multiple nodes and their connections in one update", async () => { + const existingRow = workflowRow({ + nodes: [ + { id: "n1", type: "agent", position: { x: 0, y: 0 }, data: { label: "A" } }, + { id: "n2", type: "agent", position: { x: 100, y: 0 }, data: { label: "B" } }, + { id: "n3", type: "end", position: { x: 200, y: 0 }, data: { label: "C" } }, + ], + connections: [ + { id: "c1", sourceId: "n1", targetId: "n2" }, + { id: "c2", sourceId: "n2", targetId: "n3" }, + ], + }) + const updatedRow = workflowRow({ + nodes: [{ id: "n3", type: "end", position: { x: 200, y: 0 }, data: { label: "C" } }], + connections: [], + }) + + mockFrom + .mockReturnValueOnce(createMockChain({ data: existingRow, error: null })) + .mockReturnValueOnce(createMockChain({ data: updatedRow, error: null })) + + const result = await deleteWorkflowNodes(WORKFLOW_ID, ["n1", "n2"]) + + expect(result.nodes).toHaveLength(1) + expect(result.nodes[0].id).toBe("n3") + expect(result.connections).toHaveLength(0) + }) + }) + describe("addWorkflowConnection", () => { it("adds connection between nodes", async () => { const existingRow = workflowRow({