var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "./common", "./constants", "./errors", "@n8n/errors", "./expression", "./global-state", "./interfaces", "./node-helpers", "./node-parameters/rename-node-utils", "./node-reference-parser-utils", "./observable-object"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Workflow = void 0; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-for-in-array */ const common_1 = require("./common"); const constants_1 = require("./constants"); const errors_1 = require("./errors"); const errors_2 = require("@n8n/errors"); const expression_1 = require("./expression"); const global_state_1 = require("./global-state"); const interfaces_1 = require("./interfaces"); const NodeHelpers = __importStar(require("./node-helpers")); const rename_node_utils_1 = require("./node-parameters/rename-node-utils"); const node_reference_parser_utils_1 = require("./node-reference-parser-utils"); const ObservableObject = __importStar(require("./observable-object")); function dedupe(arr) { return [...new Set(arr)]; } class Workflow { id; name; nodes = {}; connectionsBySourceNode = {}; connectionsByDestinationNode = {}; nodeTypes; expression; active; settings = {}; timezone; // To save workflow specific static data like for example // ids of registered webhooks of nodes staticData; testStaticData; pinData; constructor(parameters) { this.id = parameters.id; // @tech_debt Ensure this is not optional this.name = parameters.name; this.nodeTypes = parameters.nodeTypes; let nodeType; for (const node of parameters.nodes) { nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); if (nodeType === undefined) { // Go on to next node when its type is not known. // For now do not error because that causes problems with // expression resolution also then when the unknown node // does not get used. continue; // throw new ApplicationError(`Node with unknown node type`, { // tags: { nodeType: node.type }, // extra: { node }, // }); } // Add default values const nodeParameters = NodeHelpers.getNodeParameters(nodeType.description.properties, node.parameters, true, false, node, nodeType.description); node.parameters = nodeParameters !== null ? nodeParameters : {}; } this.setNodes(parameters.nodes); this.setConnections(parameters.connections); this.setPinData(parameters.pinData); this.setSettings(parameters.settings ?? {}); this.active = parameters.active || false; this.staticData = ObservableObject.create(parameters.staticData || {}, undefined, { ignoreEmptyOnFirstChild: true, }); this.timezone = this.settings.timezone ?? (0, global_state_1.getGlobalState)().defaultTimezone; this.expression = new expression_1.Expression(this); } // Save nodes in workflow as object to be able to get the nodes easily by their name. setNodes(nodes) { this.nodes = {}; for (const node of nodes) { this.nodes[node.name] = node; } } setConnections(connections) { this.connectionsBySourceNode = connections; this.connectionsByDestinationNode = (0, common_1.mapConnectionsByDestination)(this.connectionsBySourceNode); } setPinData(pinData) { this.pinData = pinData; } setSettings(settings) { this.settings = settings; } overrideStaticData(staticData) { this.staticData = ObservableObject.create(staticData || {}, undefined, { ignoreEmptyOnFirstChild: true, }); this.staticData.__dataChanged = true; } static getConnectionsByDestination(connections) { const returnConnection = {}; let connectionInfo; let maxIndex; for (const sourceNode in connections) { if (!connections.hasOwnProperty(sourceNode)) { continue; } for (const type of Object.keys(connections[sourceNode])) { if (!connections[sourceNode].hasOwnProperty(type)) { continue; } for (const inputIndex in connections[sourceNode][type]) { if (!connections[sourceNode][type].hasOwnProperty(inputIndex)) { continue; } for (connectionInfo of connections[sourceNode][type][inputIndex] ?? []) { if (!returnConnection.hasOwnProperty(connectionInfo.node)) { returnConnection[connectionInfo.node] = {}; } if (!returnConnection[connectionInfo.node].hasOwnProperty(connectionInfo.type)) { returnConnection[connectionInfo.node][connectionInfo.type] = []; } maxIndex = returnConnection[connectionInfo.node][connectionInfo.type].length - 1; for (let j = maxIndex; j < connectionInfo.index; j++) { returnConnection[connectionInfo.node][connectionInfo.type].push([]); } returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index]?.push({ node: sourceNode, type, index: parseInt(inputIndex, 10), }); } } } } return returnConnection; } /** * Returns the static data of the workflow. * It gets saved with the workflow and will be the same for * all workflow-executions. * * @param {string} type The type of data to return ("global"|"node") * @param {INode} [node] If type is set to "node" then the node has to be provided */ getStaticData(type, node) { let key; if (type === 'global') { key = 'global'; } else if (type === 'node') { if (node === undefined) { throw new errors_2.ApplicationError('The request data of context type "node" the node parameter has to be set!'); } key = `node:${node.name}`; } else { throw new errors_2.ApplicationError('Unknown context type. Only `global` and `node` are supported.', { extra: { contextType: type }, }); } if (this.testStaticData?.[key]) return this.testStaticData[key]; if (this.staticData[key] === undefined) { // Create it as ObservableObject that we can easily check if the data changed // to know if the workflow with its data has to be saved afterwards or not. this.staticData[key] = ObservableObject.create({}, this.staticData); } return this.staticData[key]; } setTestStaticData(testStaticData) { this.testStaticData = testStaticData; } /** * Returns all the trigger nodes in the workflow. * */ getTriggerNodes() { return this.queryNodes((nodeType) => !!nodeType.trigger); } /** * Returns all the poll nodes in the workflow * */ getPollNodes() { return this.queryNodes((nodeType) => !!nodeType.poll); } /** * Returns all the nodes in the workflow for which the given * checkFunction return true * * @param {(nodeType: INodeType) => boolean} checkFunction */ queryNodes(checkFunction) { const returnNodes = []; // Check if it has any of them let node; let nodeType; for (const nodeName of Object.keys(this.nodes)) { node = this.nodes[nodeName]; if (node.disabled === true) { continue; } nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); if (nodeType !== undefined && checkFunction(nodeType)) { returnNodes.push(node); } } return returnNodes; } /** * Returns the node with the given name if it exists else null * * @param {string} nodeName Name of the node to return */ getNode(nodeName) { return this.nodes[nodeName] ?? null; } /** * Returns the nodes with the given names if they exist. * If a node cannot be found it will be ignored, meaning the returned array * of nodes can be smaller than the array of names. */ getNodes(nodeNames) { const nodes = []; for (const name of nodeNames) { const node = this.getNode(name); if (!node) { console.warn(`Could not find a node with the name ${name} in the workflow. This was passed in as a dirty node name.`); continue; } nodes.push(node); } return nodes; } /** * Returns the pinData of the node with the given name if it exists * * @param {string} nodeName Name of the node to return the pinData of */ getPinDataOfNode(nodeName) { return this.pinData ? this.pinData[nodeName] : undefined; } renameNodeInParameterValue(parameterValue, currentName, newName, { hasRenamableContent } = { hasRenamableContent: false }) { if (typeof parameterValue !== 'object') { // Reached the actual value if (typeof parameterValue === 'string' && (parameterValue.charAt(0) === '=' || hasRenamableContent)) { parameterValue = (0, node_reference_parser_utils_1.applyAccessPatterns)(parameterValue, currentName, newName); } return parameterValue; } if (Array.isArray(parameterValue)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const returnArray = []; for (const currentValue of parameterValue) { returnArray.push(this.renameNodeInParameterValue(currentValue, currentName, newName)); } return returnArray; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const returnData = {}; for (const parameterName of Object.keys(parameterValue || {})) { returnData[parameterName] = this.renameNodeInParameterValue(parameterValue[parameterName], currentName, newName, { hasRenamableContent }); } return returnData; } /** * Rename a node in the workflow * * @param {string} currentName The current name of the node * @param {string} newName The new name */ renameNode(currentName, newName) { // These keys are excluded to prevent accidental modification of inherited properties and // to avoid any issues related to JavaScript's built-in methods that can cause unexpected behavior const restrictedKeys = [ 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf', 'constructor', 'prototype', '__proto__', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', ]; if (restrictedKeys.map((k) => k.toLowerCase()).includes(newName.toLowerCase())) { throw new errors_1.UserError(`Node name "${newName}" is a restricted name.`, { description: `Node names cannot be any of the following: ${restrictedKeys.join(', ')}`, }); } // Rename the node itself if (this.nodes[currentName] !== undefined) { this.nodes[newName] = this.nodes[currentName]; this.nodes[newName].name = newName; delete this.nodes[currentName]; } // Update the expressions which reference the node // with its old name for (const node of Object.values(this.nodes)) { node.parameters = this.renameNodeInParameterValue(node.parameters, currentName, newName); if (constants_1.NODES_WITH_RENAMABLE_CONTENT.has(node.type)) { node.parameters.jsCode = this.renameNodeInParameterValue(node.parameters.jsCode, currentName, newName, { hasRenamableContent: true }); } if (constants_1.NODES_WITH_RENAMEABLE_TOPLEVEL_HTML_CONTENT.has(node.type)) { node.parameters.html = this.renameNodeInParameterValue(node.parameters.html, currentName, newName, { hasRenamableContent: true }); } if (constants_1.NODES_WITH_RENAMABLE_FORM_HTML_CONTENT.has(node.type)) { (0, rename_node_utils_1.renameFormFields)(node, (p) => this.renameNodeInParameterValue(p, currentName, newName, { hasRenamableContent: true, })); } } // Change all source connections if (this.connectionsBySourceNode.hasOwnProperty(currentName)) { this.connectionsBySourceNode[newName] = this.connectionsBySourceNode[currentName]; delete this.connectionsBySourceNode[currentName]; } // Change all destination connections let sourceNode; let type; let sourceIndex; let connectionIndex; let connectionData; for (sourceNode of Object.keys(this.connectionsBySourceNode)) { for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) { for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) { for (connectionIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)] || [])) { connectionData = this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)]?.[parseInt(connectionIndex, 10)]; if (connectionData?.node === currentName) { connectionData.node = newName; } } } } } } /** * Finds the highest parent nodes of the node with the given name * * @param {NodeConnectionType} [type='main'] */ getHighestNode(nodeName, nodeConnectionIndex, checkedNodes) { const currentHighest = []; if (this.nodes[nodeName].disabled === false) { // If the current node is not disabled itself is the highest currentHighest.push(nodeName); } if (!this.connectionsByDestinationNode.hasOwnProperty(nodeName)) { // Node does not have incoming connections return currentHighest; } if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(interfaces_1.NodeConnectionTypes.Main)) { // Node does not have incoming connections of given type return currentHighest; } checkedNodes = checkedNodes || []; if (checkedNodes.includes(nodeName)) { // Node got checked already before return currentHighest; } checkedNodes.push(nodeName); const returnNodes = []; let addNodes; let connectionsByIndex; for (let connectionIndex = 0; connectionIndex < this.connectionsByDestinationNode[nodeName][interfaces_1.NodeConnectionTypes.Main].length; connectionIndex++) { if (nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex) { // If a connection-index is given ignore all other ones continue; } connectionsByIndex = this.connectionsByDestinationNode[nodeName][interfaces_1.NodeConnectionTypes.Main][connectionIndex]; connectionsByIndex?.forEach((connection) => { if (checkedNodes.includes(connection.node)) { // Node got checked already before return; } // Ignore connections for nodes that don't exist in this workflow if (!(connection.node in this.nodes)) return; addNodes = this.getHighestNode(connection.node, undefined, checkedNodes); if (addNodes.length === 0) { // The checked node does not have any further parents so add it // if it is not disabled if (this.nodes[connection.node].disabled !== true) { addNodes = [connection.node]; } } addNodes.forEach((name) => { // Only add if node is not on the list already anyway if (returnNodes.indexOf(name) === -1) { returnNodes.push(name); } }); }); } return returnNodes; } /** * Returns all the after the given one * * @param {string} [type='main'] * @param {*} [depth=-1] */ getChildNodes(nodeName, type = interfaces_1.NodeConnectionTypes.Main, depth = -1) { return (0, common_1.getChildNodes)(this.connectionsBySourceNode, nodeName, type, depth); } /** * Returns all the nodes before the given one * * @param {NodeConnectionType} [type='main'] * @param {*} [depth=-1] */ getParentNodes(nodeName, type = interfaces_1.NodeConnectionTypes.Main, depth = -1) { return (0, common_1.getParentNodes)(this.connectionsByDestinationNode, nodeName, type, depth); } /** * Gets all the nodes which are connected nodes starting from * the given one * * @param {NodeConnectionType} [type='main'] * @param {*} [depth=-1] */ getConnectedNodes(connections, nodeName, connectionType = interfaces_1.NodeConnectionTypes.Main, depth = -1, checkedNodesIncoming) { return (0, common_1.getConnectedNodes)(connections, nodeName, connectionType, depth, checkedNodesIncoming); } /** * Returns all the nodes before the given one * * @param {*} [maxDepth=-1] */ getParentNodesByDepth(nodeName, maxDepth = -1) { return this.searchNodesBFS(this.connectionsByDestinationNode, nodeName, maxDepth); } /** * Gets all the nodes which are connected nodes starting from * the given one * Uses BFS traversal * * @param {*} [maxDepth=-1] */ searchNodesBFS(connections, sourceNode, maxDepth = -1) { const returnConns = []; const type = interfaces_1.NodeConnectionTypes.Main; let queue = []; queue.push({ name: sourceNode, depth: 0, indicies: [], }); const visited = {}; let depth = 0; while (queue.length > 0) { if (maxDepth !== -1 && depth > maxDepth) { break; } depth++; const toAdd = [...queue]; queue = []; toAdd.forEach((curr) => { if (visited[curr.name]) { visited[curr.name].indicies = dedupe(visited[curr.name].indicies.concat(curr.indicies)); return; } visited[curr.name] = curr; if (curr.name !== sourceNode) { returnConns.push(curr); } if (!connections.hasOwnProperty(curr.name) || !connections[curr.name].hasOwnProperty(type)) { return; } connections[curr.name][type].forEach((connectionsByIndex) => { connectionsByIndex?.forEach((connection) => { queue.push({ name: connection.node, indicies: [connection.index], depth, }); }); }); }); } return returnConns; } getParentMainInputNode(node) { if (node) { const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); if (!nodeType?.description.outputs) { return node; } const outputs = NodeHelpers.getNodeOutputs(this, node, nodeType.description); const nonMainConnectionTypes = []; for (const output of outputs) { const type = typeof output === 'string' ? output : output.type; if (type !== interfaces_1.NodeConnectionTypes.Main) { nonMainConnectionTypes.push(type); } } // Sort for deterministic behavior: prevents non-deterministic selection when multiple // non-main outputs exist (AI agents with multiple tools). Object.keys() ordering // can vary across runs, causing inconsistent first-choice selection. nonMainConnectionTypes.sort(); if (nonMainConnectionTypes.length > 0) { const nonMainNodesConnected = []; const nodeConnections = this.connectionsBySourceNode[node.name]; for (const type of nonMainConnectionTypes) { // Only include connection types that exist in actual execution data if (nodeConnections?.[type]) { const childNodes = this.getChildNodes(node.name, type); if (childNodes.length > 0) { nonMainNodesConnected.push(...childNodes); } } } if (nonMainNodesConnected.length) { // Sort for deterministic behavior, then get first node nonMainNodesConnected.sort(); const returnNode = this.getNode(nonMainNodesConnected[0]); if (!returnNode) { throw new errors_2.ApplicationError(`Node "${nonMainNodesConnected[0]}" not found`); } return this.getParentMainInputNode(returnNode); } } } return node; } /** * Returns via which output of the parent-node and index the current node * they are connected * * @param {string} nodeName The node to check how it is connected with parent node * @param {string} parentNodeName The parent node to get the output index of * @param {string} [type='main'] */ getNodeConnectionIndexes(nodeName, parentNodeName, type = interfaces_1.NodeConnectionTypes.Main) { // This method has been optimized for performance. If you make any changes to it, // make sure the performance is not degraded. const parentNode = this.getNode(parentNodeName); if (parentNode === null) { return undefined; } const visitedNodes = new Set(); const queue = [nodeName]; // Cache the connections by destination node to avoid reference lookups const connectionsByDest = this.connectionsByDestinationNode; while (queue.length > 0) { const currentNodeName = queue.shift(); if (visitedNodes.has(currentNodeName)) { continue; } visitedNodes.add(currentNodeName); const typeConnections = connectionsByDest[currentNodeName]?.[type]; if (!typeConnections) { continue; } for (let typedConnectionIdx = 0; typedConnectionIdx < typeConnections.length; typedConnectionIdx++) { const connectionsByIndex = typeConnections[typedConnectionIdx]; if (!connectionsByIndex) { continue; } for (let destinationIndex = 0; destinationIndex < connectionsByIndex.length; destinationIndex++) { const connection = connectionsByIndex[destinationIndex]; if (parentNodeName === connection.node) { return { sourceIndex: connection.index, destinationIndex, }; } if (!visitedNodes.has(connection.node)) { queue.push(connection.node); } } } } return undefined; } /** * Returns from which of the given nodes the workflow should get started from * * @param {string[]} nodeNames The potential start nodes */ __getStartNode(nodeNames) { // Check if there are any trigger or poll nodes and then return the first one let node; let nodeType; if (nodeNames.length === 1) { node = this.nodes[nodeNames[0]]; if (node && !node.disabled) { return node; } } for (const nodeName of nodeNames) { node = this.nodes[nodeName]; nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); // TODO: Identify later differently if (nodeType.description.name === constants_1.MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE) { continue; } if (nodeType && (nodeType.trigger !== undefined || nodeType.poll !== undefined)) { if (node.disabled === true) { continue; } return node; } } const sortedNodeNames = Object.values(this.nodes) .sort((a, b) => constants_1.STARTING_NODE_TYPES.indexOf(a.type) - constants_1.STARTING_NODE_TYPES.indexOf(b.type)) .map((n) => n.name); for (const nodeName of sortedNodeNames) { node = this.nodes[nodeName]; if (constants_1.STARTING_NODE_TYPES.includes(node.type)) { if (node.disabled === true) { continue; } return node; } } return undefined; } /** * Returns the start node to start the workflow from * */ getStartNode(destinationNode) { if (destinationNode) { // Find the highest parent nodes of the given one const nodeNames = this.getHighestNode(destinationNode); if (nodeNames.length === 0) { // If no parent nodes have been found then only the destination-node // is in the tree so add that one nodeNames.push(destinationNode); } // Check which node to return as start node const node = this.__getStartNode(nodeNames); if (node !== undefined) { return node; } // If none of the above did find anything simply return the // first parent node in the list return this.nodes[nodeNames[0]]; } return this.__getStartNode(Object.keys(this.nodes)); } getConnectionsBetweenNodes(sources, targets) { const result = []; for (const source of sources) { for (const type of Object.keys(this.connectionsBySourceNode[source] ?? {})) { for (const sourceIndex of Object.keys(this.connectionsBySourceNode[source][type])) { for (const connectionIndex of Object.keys(this.connectionsBySourceNode[source][type][parseInt(sourceIndex, 10)] ?? [])) { const targetConnectionData = this.connectionsBySourceNode[source][type][parseInt(sourceIndex, 10)]?.[parseInt(connectionIndex, 10)]; if (targetConnectionData && targets.includes(targetConnectionData?.node)) { result.push([ { node: source, index: parseInt(sourceIndex, 10), type: type, }, targetConnectionData, ]); } } } } } return result; } } exports.Workflow = Workflow; }); //# sourceMappingURL=workflow.js.map