Skip to main content
Glama
operations.ts12.6 kB
import { N8nWorkflow, N8nNode, N8nConnections, PatchOperation, AddNodeOperation, DeleteNodeOperation, UpdateNodeOperation, SetParamOperation, UnsetParamOperation, ConnectOperation, DisconnectOperation, SetWorkflowPropertyOperation, AddTagOperation, RemoveTagOperation, OperationError, ApplyOpsResponse } from './types.js'; export class WorkflowOperationsProcessor { /** * Apply a batch of operations to a workflow atomically */ async applyOperations(workflow: N8nWorkflow, operations: PatchOperation[] | undefined | null): Promise<ApplyOpsResponse> { // Create a deep copy of the workflow to work with const workflowCopy = JSON.parse(JSON.stringify(workflow)) as N8nWorkflow; const errors: OperationError[] = []; try { // Normalize operations to an empty array if undefined/null const ops = Array.isArray(operations) ? operations : []; // Apply each operation for (let i = 0; i < ops.length; i++) { const operation = ops[i]; try { this.applyOperation(workflowCopy, operation); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; errors.push({ operationIndex: i, operation, error: errorMessage, details: error instanceof Error ? error.stack : undefined }); // Atomic behavior: if any operation fails, return errors without applying changes return { success: false, errors }; } } // All operations succeeded return { success: true, workflow: workflowCopy }; } catch (error) { // Unexpected error during processing const errorMessage = error instanceof Error ? error.message : 'Unknown error during batch processing'; return { success: false, errors: [{ operationIndex: -1, operation: (Array.isArray(operations) && operations.length > 0 ? operations[0] : ({ type: 'unknown' } as any)), error: errorMessage }] }; } } /** * Apply a single operation to the workflow */ private applyOperation(workflow: N8nWorkflow, operation: PatchOperation): void { switch (operation.type) { case 'addNode': this.applyAddNode(workflow, operation); break; case 'deleteNode': this.applyDeleteNode(workflow, operation); break; case 'updateNode': this.applyUpdateNode(workflow, operation); break; case 'setParam': this.applySetParam(workflow, operation); break; case 'unsetParam': this.applyUnsetParam(workflow, operation); break; case 'connect': this.applyConnect(workflow, operation); break; case 'disconnect': this.applyDisconnect(workflow, operation); break; case 'setWorkflowProperty': this.applySetWorkflowProperty(workflow, operation); break; case 'addTag': this.applyAddTag(workflow, operation); break; case 'removeTag': this.applyRemoveTag(workflow, operation); break; default: throw new Error(`Unknown operation type: ${(operation as any).type}`); } } private applyAddNode(workflow: N8nWorkflow, operation: AddNodeOperation): void { // Check if node ID already exists if (workflow.nodes.some(node => node.id === operation.node.id)) { throw new Error(`Node with ID "${operation.node.id}" already exists`); } // Add the node workflow.nodes.push(operation.node); } private applyDeleteNode(workflow: N8nWorkflow, operation: DeleteNodeOperation): void { const nodeIndex = workflow.nodes.findIndex(node => node.id === operation.nodeId); if (nodeIndex === -1) { throw new Error(`Node with ID "${operation.nodeId}" not found`); } // Get the node name before deleting it const nodeName = workflow.nodes[nodeIndex].name; // Remove the node workflow.nodes.splice(nodeIndex, 1); // Remove all connections involving this node this.removeNodeConnections(workflow, nodeName); } private applyUpdateNode(workflow: N8nWorkflow, operation: UpdateNodeOperation): void { const node = workflow.nodes.find(node => node.id === operation.nodeId); if (!node) { throw new Error(`Node with ID "${operation.nodeId}" not found`); } // Apply updates Object.assign(node, operation.updates); } private applySetParam(workflow: N8nWorkflow, operation: SetParamOperation): void { const node = workflow.nodes.find(node => node.id === operation.nodeId); if (!node) { throw new Error(`Node with ID "${operation.nodeId}" not found`); } // Initialize parameters if they don't exist if (!node.parameters) { node.parameters = {}; } // Set the parameter using dot notation this.setNestedProperty(node.parameters, operation.paramPath, operation.value); } private applyUnsetParam(workflow: N8nWorkflow, operation: UnsetParamOperation): void { const node = workflow.nodes.find(node => node.id === operation.nodeId); if (!node) { throw new Error(`Node with ID "${operation.nodeId}" not found`); } if (!node.parameters) { return; // Nothing to unset } // Unset the parameter using dot notation this.unsetNestedProperty(node.parameters, operation.paramPath); } private applyConnect(workflow: N8nWorkflow, operation: ConnectOperation): void { // Validate that both nodes exist const fromNode = workflow.nodes.find(node => node.name === operation.from.nodeName); const toNode = workflow.nodes.find(node => node.name === operation.to.nodeName); if (!fromNode) { throw new Error(`Source node with name "${operation.from.nodeName}" not found`); } if (!toNode) { throw new Error(`Target node with name "${operation.to.nodeName}" not found`); } // Initialize connections if they don't exist if (!workflow.connections) { workflow.connections = {}; } const fromNodeName = operation.from.nodeName; const outputType = operation.from.outputType; if (!workflow.connections[fromNodeName]) { workflow.connections[fromNodeName] = {}; } if (!workflow.connections[fromNodeName][outputType]) { workflow.connections[fromNodeName][outputType] = []; } // Ensure the output index array exists while (workflow.connections[fromNodeName][outputType].length <= operation.from.outputIndex) { workflow.connections[fromNodeName][outputType].push([]); } // Check if connection already exists const existingConnection = workflow.connections[fromNodeName][outputType][operation.from.outputIndex] .find(conn => conn.node === operation.to.nodeName && conn.type === operation.to.inputType && conn.index === operation.to.inputIndex ); if (existingConnection) { throw new Error(`Connection already exists from ${operation.from.nodeName}[${operation.from.outputIndex}] to ${operation.to.nodeName}[${operation.to.inputIndex}]`); } // Add the connection workflow.connections[fromNodeName][outputType][operation.from.outputIndex].push({ node: operation.to.nodeName, type: operation.to.inputType, index: operation.to.inputIndex }); } private applyDisconnect(workflow: N8nWorkflow, operation: DisconnectOperation): void { if (!workflow.connections) { throw new Error('No connections exist in workflow'); } const fromNodeName = operation.from.nodeName; const outputType = operation.from.outputType; if (!workflow.connections[fromNodeName] || !workflow.connections[fromNodeName][outputType] || !workflow.connections[fromNodeName][outputType][operation.from.outputIndex]) { throw new Error(`No connection found from ${operation.from.nodeName}[${operation.from.outputIndex}]`); } const connections = workflow.connections[fromNodeName][outputType][operation.from.outputIndex]; const connectionIndex = connections.findIndex(conn => conn.node === operation.to.nodeName && conn.type === operation.to.inputType && conn.index === operation.to.inputIndex ); if (connectionIndex === -1) { throw new Error(`Connection not found from ${operation.from.nodeName}[${operation.from.outputIndex}] to ${operation.to.nodeName}[${operation.to.inputIndex}]`); } // Remove the connection connections.splice(connectionIndex, 1); // Clean up empty arrays if (connections.length === 0) { workflow.connections[fromNodeName][outputType].splice(operation.from.outputIndex, 1); // Clean up empty output type if (workflow.connections[fromNodeName][outputType].length === 0) { delete workflow.connections[fromNodeName][outputType]; // Clean up empty node if (Object.keys(workflow.connections[fromNodeName]).length === 0) { delete workflow.connections[fromNodeName]; } } } } private applySetWorkflowProperty(workflow: N8nWorkflow, operation: SetWorkflowPropertyOperation): void { // Validate that the property exists on the workflow type if (!(operation.property in workflow)) { throw new Error(`Property "${operation.property}" does not exist on workflow`); } // Set the property (workflow as any)[operation.property] = operation.value; } private applyAddTag(workflow: N8nWorkflow, operation: AddTagOperation): void { if (!workflow.tags) { workflow.tags = []; } if (workflow.tags.includes(operation.tag)) { throw new Error(`Tag "${operation.tag}" already exists`); } workflow.tags.push(operation.tag); } private applyRemoveTag(workflow: N8nWorkflow, operation: RemoveTagOperation): void { if (!workflow.tags) { throw new Error('No tags exist on workflow'); } const tagIndex = workflow.tags.indexOf(operation.tag); if (tagIndex === -1) { throw new Error(`Tag "${operation.tag}" not found`); } workflow.tags.splice(tagIndex, 1); } /** * Remove all connections involving a specific node by name */ private removeNodeConnections(workflow: N8nWorkflow, nodeName: string): void { if (!workflow.connections) { return; } // Remove outgoing connections (where this node is the source) delete workflow.connections[nodeName]; // Remove incoming connections (where this node is the target) for (const fromNodeName of Object.keys(workflow.connections)) { for (const outputType of Object.keys(workflow.connections[fromNodeName])) { for (let outputIndex = 0; outputIndex < workflow.connections[fromNodeName][outputType].length; outputIndex++) { workflow.connections[fromNodeName][outputType][outputIndex] = workflow.connections[fromNodeName][outputType][outputIndex].filter( conn => conn.node !== nodeName ); } // Clean up empty arrays - need to filter from the end to avoid index issues workflow.connections[fromNodeName][outputType] = workflow.connections[fromNodeName][outputType].filter(arr => arr.length > 0); if (workflow.connections[fromNodeName][outputType].length === 0) { delete workflow.connections[fromNodeName][outputType]; } } // Clean up empty nodes if (Object.keys(workflow.connections[fromNodeName]).length === 0) { delete workflow.connections[fromNodeName]; } } } /** * Set a nested property using dot notation */ private setNestedProperty(obj: any, path: string, value: any): void { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current) || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } current[keys[keys.length - 1]] = value; } /** * Unset a nested property using dot notation */ private unsetNestedProperty(obj: any, path: string): void { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current) || typeof current[key] !== 'object') { return; // Path doesn't exist } current = current[key]; } delete current[keys[keys.length - 1]]; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get2knowio/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server