mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
This commit is contained in:
committed by
GitHub
parent
e1d8eaa170
commit
62f4361f46
@@ -28,25 +28,6 @@ export interface ExpressionErrorOptions {
|
||||
/**
|
||||
* Class for instantiating an expression error
|
||||
*/
|
||||
// Expression error constants
|
||||
export const EXPRESSION_ERROR_MESSAGES = {
|
||||
NODE_NOT_FOUND: 'Error finding the referenced node',
|
||||
NODE_REFERENCE_TEMPLATE:
|
||||
'Make sure the node you referenced is spelled correctly and is a parent of this node',
|
||||
NO_EXECUTION_DATA: 'No execution data available',
|
||||
} as const;
|
||||
|
||||
export const EXPRESSION_ERROR_TYPES = {
|
||||
PAIRED_ITEM_NO_CONNECTION: 'paired_item_no_connection',
|
||||
} as const;
|
||||
|
||||
export const EXPRESSION_DESCRIPTION_KEYS = {
|
||||
NODE_NOT_FOUND: 'nodeNotFound',
|
||||
NO_NODE_EXECUTION_DATA: 'noNodeExecutionData',
|
||||
PAIRED_ITEM_NO_CONNECTION: 'pairedItemNoConnection',
|
||||
PAIRED_ITEM_NO_CONNECTION_CODE_NODE: 'pairedItemNoConnectionCodeNode',
|
||||
} as const;
|
||||
|
||||
export class ExpressionError extends ExecutionBaseError {
|
||||
constructor(message: string, options?: ExpressionErrorOptions) {
|
||||
super(message, { cause: options?.cause, level: 'warning' });
|
||||
|
||||
@@ -8,13 +8,7 @@ import { DateTime, Duration, Interval, Settings } from 'luxon';
|
||||
import { augmentArray, augmentObject } from './augment-object';
|
||||
import { AGENT_LANGCHAIN_NODE_TYPE, SCRIPTING_NODE_TYPES } from './constants';
|
||||
import { ApplicationError } from '@n8n/errors';
|
||||
import {
|
||||
ExpressionError,
|
||||
type ExpressionErrorOptions,
|
||||
EXPRESSION_ERROR_MESSAGES,
|
||||
EXPRESSION_ERROR_TYPES,
|
||||
EXPRESSION_DESCRIPTION_KEYS,
|
||||
} from './errors/expression.error';
|
||||
import { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error';
|
||||
import { getGlobalState } from './global-state';
|
||||
import { NodeConnectionTypes } from './interfaces';
|
||||
import type {
|
||||
@@ -396,13 +390,11 @@ export class WorkflowDataProxy {
|
||||
}
|
||||
|
||||
if (!that.workflow.getNode(nodeName)) {
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NODE_NOT_FOUND, {
|
||||
messageTemplate: EXPRESSION_ERROR_MESSAGES.NODE_REFERENCE_TEMPLATE,
|
||||
throw new ExpressionError("Referenced node doesn't exist", {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
nodeCause: nodeName,
|
||||
descriptionKey: EXPRESSION_DESCRIPTION_KEYS.NODE_NOT_FOUND,
|
||||
type: EXPRESSION_ERROR_TYPES.PAIRED_ITEM_NO_CONNECTION,
|
||||
descriptionKey: 'nodeNotFound',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -410,12 +402,11 @@ export class WorkflowDataProxy {
|
||||
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
|
||||
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||
) {
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NODE_NOT_FOUND, {
|
||||
messageTemplate: EXPRESSION_ERROR_MESSAGES.NODE_REFERENCE_TEMPLATE,
|
||||
throw new ExpressionError('Referenced node is unexecuted', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: EXPRESSION_ERROR_TYPES.PAIRED_ITEM_NO_CONNECTION,
|
||||
descriptionKey: EXPRESSION_DESCRIPTION_KEYS.NO_NODE_EXECUTION_DATA,
|
||||
type: 'no_node_execution_data',
|
||||
descriptionKey: 'noNodeExecutionData',
|
||||
nodeCause: nodeName,
|
||||
});
|
||||
}
|
||||
@@ -505,16 +496,11 @@ export class WorkflowDataProxy {
|
||||
name = name.toString();
|
||||
|
||||
if (!node) {
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NODE_NOT_FOUND, {
|
||||
messageTemplate: EXPRESSION_ERROR_MESSAGES.NODE_REFERENCE_TEMPLATE,
|
||||
functionality: 'pairedItem',
|
||||
descriptionKey: isScriptingNode(nodeName, that.workflow)
|
||||
? EXPRESSION_DESCRIPTION_KEYS.PAIRED_ITEM_NO_CONNECTION_CODE_NODE
|
||||
: EXPRESSION_DESCRIPTION_KEYS.PAIRED_ITEM_NO_CONNECTION,
|
||||
type: EXPRESSION_ERROR_TYPES.PAIRED_ITEM_NO_CONNECTION,
|
||||
nodeCause: nodeName,
|
||||
throw new ExpressionError("Referenced node doesn't exist", {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
nodeCause: nodeName,
|
||||
descriptionKey: 'nodeNotFound',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -530,7 +516,7 @@ export class WorkflowDataProxy {
|
||||
|
||||
if (executionData.length === 0) {
|
||||
if (that.workflow.getParentNodes(nodeName).length === 0) {
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NO_EXECUTION_DATA, {
|
||||
throw new ExpressionError('No execution data available', {
|
||||
messageTemplate:
|
||||
'No execution data available to expression under ‘%%PARAMETER%%’',
|
||||
descriptionKey: 'noInputConnection',
|
||||
@@ -541,7 +527,7 @@ export class WorkflowDataProxy {
|
||||
});
|
||||
}
|
||||
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NO_EXECUTION_DATA, {
|
||||
throw new ExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
@@ -707,16 +693,11 @@ export class WorkflowDataProxy {
|
||||
const nodeName = name.toString();
|
||||
|
||||
if (that.workflow.getNode(nodeName) === null) {
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NODE_NOT_FOUND, {
|
||||
messageTemplate: EXPRESSION_ERROR_MESSAGES.NODE_REFERENCE_TEMPLATE,
|
||||
functionality: 'pairedItem',
|
||||
descriptionKey: isScriptingNode(nodeName, that.workflow)
|
||||
? EXPRESSION_DESCRIPTION_KEYS.PAIRED_ITEM_NO_CONNECTION_CODE_NODE
|
||||
: EXPRESSION_DESCRIPTION_KEYS.PAIRED_ITEM_NO_CONNECTION,
|
||||
type: EXPRESSION_ERROR_TYPES.PAIRED_ITEM_NO_CONNECTION,
|
||||
nodeCause: nodeName,
|
||||
throw new ExpressionError("Referenced node doesn't exist", {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
nodeCause: nodeName,
|
||||
descriptionKey: 'nodeNotFound',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -833,14 +814,14 @@ export class WorkflowDataProxy {
|
||||
});
|
||||
};
|
||||
|
||||
const createNodeReferenceError = (nodeCause: string) => {
|
||||
return createExpressionError(EXPRESSION_ERROR_MESSAGES.NODE_NOT_FOUND, {
|
||||
messageTemplate: EXPRESSION_ERROR_MESSAGES.NODE_REFERENCE_TEMPLATE,
|
||||
const createNoConnectionError = (nodeCause: string) => {
|
||||
return createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'No path back to referenced node',
|
||||
functionality: 'pairedItem',
|
||||
descriptionKey: isScriptingNode(nodeCause, that.workflow)
|
||||
? EXPRESSION_DESCRIPTION_KEYS.PAIRED_ITEM_NO_CONNECTION_CODE_NODE
|
||||
: EXPRESSION_DESCRIPTION_KEYS.PAIRED_ITEM_NO_CONNECTION,
|
||||
type: EXPRESSION_ERROR_TYPES.PAIRED_ITEM_NO_CONNECTION,
|
||||
? 'pairedItemNoConnectionCodeNode'
|
||||
: 'pairedItemNoConnection',
|
||||
type: 'paired_item_no_connection',
|
||||
moreInfoLink: true,
|
||||
nodeCause,
|
||||
});
|
||||
@@ -1009,7 +990,7 @@ export class WorkflowDataProxy {
|
||||
const matchedItems = results.filter((result) => result.ok).map((result) => result.result);
|
||||
|
||||
if (matchedItems.length === 0) {
|
||||
if (sourceArray.length === 0) throw createNodeReferenceError(destinationNodeName);
|
||||
if (sourceArray.length === 0) throw createNoConnectionError(destinationNodeName);
|
||||
throw createBranchNotFoundError(sourceData.previousNode, pairedItem.item, nodeBeforeLast);
|
||||
}
|
||||
|
||||
@@ -1050,7 +1031,7 @@ export class WorkflowDataProxy {
|
||||
inputData?.[NodeConnectionTypes.AiTool]?.[0]?.[itemIndex].json;
|
||||
|
||||
if (!placeholdersDataInputData) {
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NO_EXECUTION_DATA, {
|
||||
throw new ExpressionError('No execution data available', {
|
||||
runIndex,
|
||||
itemIndex,
|
||||
type: 'no_execution_data',
|
||||
@@ -1072,7 +1053,12 @@ export class WorkflowDataProxy {
|
||||
|
||||
const referencedNode = that.workflow.getNode(nodeName);
|
||||
if (referencedNode === null) {
|
||||
throw createNodeReferenceError(nodeName);
|
||||
throw createExpressionError("Referenced node doesn't exist", {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
nodeCause: nodeName,
|
||||
descriptionKey: 'nodeNotFound',
|
||||
});
|
||||
}
|
||||
|
||||
const ensureNodeExecutionData = () => {
|
||||
@@ -1080,38 +1066,16 @@ export class WorkflowDataProxy {
|
||||
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
||||
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||
) {
|
||||
// Always show helpful "Execute node for preview" message
|
||||
throw new ExpressionError(EXPRESSION_ERROR_MESSAGES.NO_EXECUTION_DATA, {
|
||||
messageTemplate: `Execute node "${nodeName}" for preview`,
|
||||
nodeCause: nodeName,
|
||||
throw createExpressionError('Referenced node is unexecuted', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_node_execution_data',
|
||||
descriptionKey: 'noNodeExecutionData',
|
||||
nodeCause: nodeName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ensureValidPath = () => {
|
||||
// Check path before execution data
|
||||
const referencedNode = that.workflow.getNode(nodeName);
|
||||
if (!referencedNode) {
|
||||
throw createNodeReferenceError(nodeName);
|
||||
}
|
||||
|
||||
const activeNode = that.workflow.getNode(that.activeNodeName);
|
||||
let contextNode = that.contextNodeName;
|
||||
if (activeNode) {
|
||||
const parentMainInputNode = that.workflow.getParentMainInputNode(activeNode);
|
||||
contextNode = parentMainInputNode?.name ?? contextNode;
|
||||
}
|
||||
|
||||
// For .first(), .last(), .all() methods, use unidirectional path checking
|
||||
// (forward only) to maintain traditional paired item behavior
|
||||
const hasForwardPath = that.workflow.getChildNodes(nodeName).includes(contextNode);
|
||||
if (!hasForwardPath) {
|
||||
throw createNodeReferenceError(nodeName);
|
||||
}
|
||||
};
|
||||
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
@@ -1144,24 +1108,17 @@ export class WorkflowDataProxy {
|
||||
property === PAIRED_ITEM_METHOD.ITEM
|
||||
) {
|
||||
// Before resolving the pairedItem make sure that the requested node comes in the
|
||||
// graph before the current one or exists in the workflow
|
||||
const referencedNode = that.workflow.getNode(nodeName);
|
||||
if (!referencedNode) {
|
||||
// Node doesn't exist in the workflow (could be trimmed manual execution)
|
||||
throw createNodeReferenceError(nodeName);
|
||||
}
|
||||
|
||||
// graph before the current one
|
||||
const activeNode = that.workflow.getNode(that.activeNodeName);
|
||||
|
||||
let contextNode = that.contextNodeName;
|
||||
if (activeNode) {
|
||||
const parentMainInputNode = that.workflow.getParentMainInputNode(activeNode);
|
||||
contextNode = parentMainInputNode?.name ?? contextNode;
|
||||
contextNode = parentMainInputNode.name ?? contextNode;
|
||||
}
|
||||
|
||||
// Use bidirectional path checking to handle AI/tool nodes properly
|
||||
if (!that.workflow.hasPath(nodeName, contextNode)) {
|
||||
throw createNodeReferenceError(nodeName);
|
||||
const parentNodes = that.workflow.getParentNodes(contextNode);
|
||||
if (!parentNodes.includes(nodeName)) {
|
||||
throw createNoConnectionError(nodeName);
|
||||
}
|
||||
|
||||
ensureNodeExecutionData();
|
||||
@@ -1242,7 +1199,6 @@ export class WorkflowDataProxy {
|
||||
}
|
||||
|
||||
if (property === 'first') {
|
||||
ensureValidPath();
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
branchIndex =
|
||||
@@ -1261,7 +1217,6 @@ export class WorkflowDataProxy {
|
||||
};
|
||||
}
|
||||
if (property === 'last') {
|
||||
ensureValidPath();
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
branchIndex =
|
||||
@@ -1283,7 +1238,6 @@ export class WorkflowDataProxy {
|
||||
};
|
||||
}
|
||||
if (property === 'all') {
|
||||
ensureValidPath();
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
branchIndex =
|
||||
@@ -1322,7 +1276,7 @@ export class WorkflowDataProxy {
|
||||
if (property === 'isProxy') return true;
|
||||
|
||||
if (that.connectionInputData.length === 0) {
|
||||
throw createExpressionError(EXPRESSION_ERROR_MESSAGES.NO_EXECUTION_DATA, {
|
||||
throw createExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
INodeConnection,
|
||||
IObservableObject,
|
||||
NodeParameterValueType,
|
||||
INodeOutputConfiguration,
|
||||
NodeConnectionType,
|
||||
} from './interfaces';
|
||||
import { NodeConnectionTypes } from './interfaces';
|
||||
@@ -688,39 +689,40 @@ export class Workflow {
|
||||
return returnConns;
|
||||
}
|
||||
|
||||
getParentMainInputNode(
|
||||
node: INode | null | undefined,
|
||||
visitedNodes: Set<string> = new Set(),
|
||||
): INode | null | undefined {
|
||||
if (!node) return node;
|
||||
getParentMainInputNode(node: INode): INode {
|
||||
if (node) {
|
||||
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const outputs = NodeHelpers.getNodeOutputs(this, node, nodeType.description);
|
||||
|
||||
// Prevent infinite recursion by tracking visited nodes
|
||||
if (visitedNodes.has(node.name)) {
|
||||
return node;
|
||||
}
|
||||
visitedNodes.add(node.name);
|
||||
|
||||
const nodeConnections = this.connectionsBySourceNode[node.name];
|
||||
if (!nodeConnections) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Get non-main connection types that this node connects TO (outgoing connections)
|
||||
const nonMainConnectionTypes = Object.keys(nodeConnections).filter(
|
||||
(type) => type !== NodeConnectionTypes.Main,
|
||||
);
|
||||
|
||||
for (const connectionType of nonMainConnectionTypes) {
|
||||
const connections = nodeConnections[connectionType] ?? [];
|
||||
for (const connectionGroup of connections) {
|
||||
for (const connection of connectionGroup ?? []) {
|
||||
if (connection?.node) {
|
||||
const returnNode = this.getNode(connection.node);
|
||||
if (!returnNode) {
|
||||
throw new ApplicationError(`Node "${connection.node}" not found`);
|
||||
}
|
||||
return this.getParentMainInputNode(returnNode, visitedNodes);
|
||||
if (
|
||||
outputs.find(
|
||||
(output) =>
|
||||
((output as INodeOutputConfiguration)?.type ?? output) !== NodeConnectionTypes.Main,
|
||||
)
|
||||
) {
|
||||
// Get the first node which is connected to a non-main output
|
||||
const nonMainNodesConnected = outputs?.reduce((acc, outputName) => {
|
||||
const parentNodes = this.getChildNodes(
|
||||
node.name,
|
||||
(outputName as INodeOutputConfiguration)?.type ?? outputName,
|
||||
);
|
||||
if (parentNodes.length > 0) {
|
||||
acc.push(...parentNodes);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
if (nonMainNodesConnected.length) {
|
||||
const returnNode = this.getNode(nonMainNodesConnected[0]);
|
||||
if (returnNode === null) {
|
||||
// This should theoretically never happen as the node is connected
|
||||
// but who knows and it makes TS happy
|
||||
throw new ApplicationError(`Node "${nonMainNodesConnected[0]}" not found`);
|
||||
}
|
||||
|
||||
// The chain of non-main nodes is potentially not finished yet so
|
||||
// keep on going
|
||||
return this.getParentMainInputNode(returnNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,70 +931,4 @@ export class Workflow {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a bidirectional path between two nodes.
|
||||
* This handles AI/tool nodes that have complex connection patterns
|
||||
* where simple parent-child traversal doesn't work.
|
||||
*
|
||||
* @param fromNodeName The starting node name
|
||||
* @param toNodeName The target node name
|
||||
* @param maxDepth Maximum depth to search (default: 50)
|
||||
* @returns true if there's a path between the nodes
|
||||
*/
|
||||
hasPath(fromNodeName: string, toNodeName: string, maxDepth = 50): boolean {
|
||||
if (fromNodeName === toNodeName) return true;
|
||||
|
||||
// Special case: If the source node has pinned data, consider it as having a valid path
|
||||
// This is important for single node execution scenarios where pinned data creates virtual paths
|
||||
if (this.getPinDataOfNode(fromNodeName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get connection types that actually exist in this workflow
|
||||
// We need both source and destination connection types for bidirectional search
|
||||
const connectionTypes = new Set<NodeConnectionType>();
|
||||
for (const nodeConnections of Object.values(this.connectionsBySourceNode).concat(
|
||||
Object.values(this.connectionsByDestinationNode),
|
||||
)) {
|
||||
for (const type of Object.keys(nodeConnections)) {
|
||||
connectionTypes.add(type as NodeConnectionType);
|
||||
}
|
||||
}
|
||||
|
||||
const visited = new Set<string>();
|
||||
const queue: Array<{ nodeName: string; depth: number }> = [
|
||||
{ nodeName: fromNodeName, depth: 0 },
|
||||
];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { nodeName, depth } = queue.shift()!;
|
||||
|
||||
if (depth > maxDepth) continue;
|
||||
if (visited.has(nodeName)) continue;
|
||||
if (nodeName === toNodeName) return true;
|
||||
|
||||
visited.add(nodeName);
|
||||
|
||||
for (const connectionType of connectionTypes) {
|
||||
// Get children (forward direction)
|
||||
const children = this.getChildNodes(nodeName, connectionType);
|
||||
for (const childName of children) {
|
||||
if (!visited.has(childName)) {
|
||||
queue.push({ nodeName: childName, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// Get parents (backward direction)
|
||||
const parents = this.getParentNodes(nodeName, connectionType);
|
||||
for (const parentName of parents) {
|
||||
if (!visited.has(parentName)) {
|
||||
queue.push({ nodeName: parentName, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -235,7 +235,7 @@ describe('WorkflowDataProxy', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Error finding the referenced node');
|
||||
expect(exprError.message).toEqual("Referenced node doesn't exist");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -246,7 +246,7 @@ describe('WorkflowDataProxy', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Error finding the referenced node');
|
||||
expect(exprError.message).toEqual('Invalid expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_connection');
|
||||
}
|
||||
});
|
||||
@@ -262,8 +262,8 @@ describe('WorkflowDataProxy', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Error finding the referenced node');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_connection');
|
||||
expect(exprError.message).toEqual('Referenced node is unexecuted');
|
||||
expect(exprError.context.type).toEqual('no_node_execution_data');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -286,10 +286,8 @@ describe('WorkflowDataProxy', () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('No execution data available');
|
||||
expect(exprError.context.messageTemplate).toEqual(
|
||||
'Execute node "Impossible if" for preview',
|
||||
);
|
||||
expect(exprError.message).toEqual('Referenced node is unexecuted');
|
||||
expect(exprError.context.type).toEqual('no_node_execution_data');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -828,207 +826,4 @@ describe('WorkflowDataProxy', () => {
|
||||
expect(proxy.$('Set main variable').item.json.main_variable).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Improved error messages for missing execution data', () => {
|
||||
test('should show helpful error message when accessing node without execution data', () => {
|
||||
// Create a simple workflow with two connected nodes
|
||||
const workflow: IWorkflowBase = {
|
||||
id: '1',
|
||||
name: 'test-workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Telegram Trigger',
|
||||
type: 'n8n-nodes-base.telegramTrigger',
|
||||
typeVersion: 1.2,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Send a text message',
|
||||
type: 'n8n-nodes-base.telegram',
|
||||
typeVersion: 1.2,
|
||||
position: [576, 0],
|
||||
parameters: {
|
||||
chatId: "={{ $('Telegram Trigger').item.json.message.chat.id }}",
|
||||
text: 'Test message',
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Telegram Trigger': {
|
||||
main: [[{ node: 'Send a text message', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Create run data without execution data for Telegram Trigger
|
||||
const run = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}, // Empty - no nodes have executed
|
||||
},
|
||||
},
|
||||
mode: 'manual' as const,
|
||||
startedAt: new Date(),
|
||||
status: 'success' as const,
|
||||
};
|
||||
|
||||
const proxy = getProxyFromFixture(workflow, run, 'Send a text message');
|
||||
|
||||
// Should throw helpful error when trying to access Telegram Trigger data
|
||||
let error: ExpressionError | undefined;
|
||||
try {
|
||||
proxy.$('Telegram Trigger').item;
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
expect(error!.message).toBe('No execution data available');
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Telegram Trigger" for preview');
|
||||
expect(error!.context.nodeCause).toBe('Telegram Trigger');
|
||||
});
|
||||
|
||||
test('should show helpful error message for different node names', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
id: '1',
|
||||
name: 'test-workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'HTTP Request',
|
||||
type: 'n8n-nodes-base.httpRequest',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Process Data',
|
||||
type: 'n8n-nodes-base.code',
|
||||
typeVersion: 2,
|
||||
position: [300, 0],
|
||||
parameters: {
|
||||
jsCode: "return $('HTTP Request').all();",
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'HTTP Request': {
|
||||
main: [[{ node: 'Process Data', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const run = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}, // Empty - no nodes have executed
|
||||
},
|
||||
},
|
||||
mode: 'manual' as const,
|
||||
startedAt: new Date(),
|
||||
status: 'success' as const,
|
||||
};
|
||||
|
||||
const proxy = getProxyFromFixture(workflow, run, 'Process Data');
|
||||
|
||||
let error: ExpressionError | undefined;
|
||||
try {
|
||||
proxy.$('HTTP Request').item;
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "HTTP Request" for preview');
|
||||
expect(error!.context.nodeCause).toBe('HTTP Request');
|
||||
});
|
||||
|
||||
test('should use improved error for first(), last(), and all() methods', () => {
|
||||
const workflow: IWorkflowBase = {
|
||||
id: '1',
|
||||
name: 'test-workflow',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Start Node',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'End Node',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
position: [300, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'Start Node': {
|
||||
main: [[{ node: 'End Node', type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const run = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {}, // Empty - no nodes have executed
|
||||
},
|
||||
},
|
||||
mode: 'manual' as const,
|
||||
startedAt: new Date(),
|
||||
status: 'success' as const,
|
||||
};
|
||||
|
||||
const proxy = getProxyFromFixture(workflow, run, 'End Node');
|
||||
|
||||
// Test first() method
|
||||
let error: ExpressionError | undefined;
|
||||
try {
|
||||
proxy.$('Start Node').first();
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Start Node" for preview');
|
||||
|
||||
// Test last() method
|
||||
try {
|
||||
proxy.$('Start Node').last();
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Start Node" for preview');
|
||||
|
||||
// Test all() method
|
||||
try {
|
||||
proxy.$('Start Node').all();
|
||||
} catch (e) {
|
||||
error = e as ExpressionError;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.context.messageTemplate).toBe('Execute node "Start Node" for preview');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
IPinData,
|
||||
IRunExecutionData,
|
||||
NodeParameterValueType,
|
||||
} from '../src/interfaces';
|
||||
@@ -2363,479 +2362,6 @@ describe('Workflow', () => {
|
||||
const result = WORKFLOW_WITH_LOOPS.getParentMainInputNode(set1Node);
|
||||
expect(result).toBe(set1Node);
|
||||
});
|
||||
|
||||
describe('nodes with only main outputs', () => {
|
||||
test('should return the same node when it only has main outputs', () => {
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'SimpleNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'TargetNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: {},
|
||||
},
|
||||
];
|
||||
|
||||
const connections = {
|
||||
SimpleNode: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'TargetNode', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes,
|
||||
connections,
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const simpleNode = workflow.getNode('SimpleNode')!;
|
||||
const result = workflow.getParentMainInputNode(simpleNode);
|
||||
|
||||
expect(result).toBe(simpleNode);
|
||||
expect(result!.name).toBe('SimpleNode');
|
||||
});
|
||||
|
||||
test('should return the same node when it has no connections', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'IsolatedNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const isolatedNode = workflow.getNode('IsolatedNode')!;
|
||||
const result = workflow.getParentMainInputNode(isolatedNode);
|
||||
|
||||
expect(result).toBe(isolatedNode);
|
||||
expect(result!.name).toBe('IsolatedNode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodes with non-main outputs (AI/Tool connections)', () => {
|
||||
test('should follow AI tool connection to find main input node', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'ToolNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'AgentNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
ToolNode: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'AgentNode', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const toolNode = workflow.getNode('ToolNode')!;
|
||||
const result = workflow.getParentMainInputNode(toolNode);
|
||||
|
||||
expect(result!.name).toBe('AgentNode');
|
||||
});
|
||||
|
||||
test('should follow AI memory connection to find main input node', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'MemoryNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'ChatNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
MemoryNode: {
|
||||
[NodeConnectionTypes.AiMemory]: [
|
||||
[{ node: 'ChatNode', type: NodeConnectionTypes.AiMemory, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const memoryNode = workflow.getNode('MemoryNode')!;
|
||||
const result = workflow.getParentMainInputNode(memoryNode);
|
||||
|
||||
expect(result!.name).toBe('ChatNode');
|
||||
});
|
||||
|
||||
test('should handle mixed main and non-main outputs', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'MixedNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'MainTarget',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'ToolTarget',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 200],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
MixedNode: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'MainTarget', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'ToolTarget', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const mixedNode = workflow.getNode('MixedNode')!;
|
||||
const result = workflow.getParentMainInputNode(mixedNode);
|
||||
|
||||
// Should follow the first non-main connection (AiTool)
|
||||
expect(result!.name).toBe('ToolTarget');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chain traversal scenarios', () => {
|
||||
test('should follow a chain of AI connections until reaching main input node', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'StartTool',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'MiddleTool',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'FinalAgent',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [300, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
StartTool: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'MiddleTool', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
MiddleTool: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'FinalAgent', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const startTool = workflow.getNode('StartTool')!;
|
||||
const result = workflow.getParentMainInputNode(startTool);
|
||||
|
||||
expect(result!.name).toBe('FinalAgent');
|
||||
});
|
||||
|
||||
test('should handle chain that ends with a node having only main outputs', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'ToolNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'IntermediateNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'EndNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [300, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
ToolNode: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'IntermediateNode', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
IntermediateNode: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'EndNode', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const toolNode = workflow.getNode('ToolNode')!;
|
||||
const result = workflow.getParentMainInputNode(toolNode);
|
||||
|
||||
expect(result!.name).toBe('IntermediateNode');
|
||||
});
|
||||
|
||||
test('should handle complex multi-branch AI connections', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'MultiTool',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Agent1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 50],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Agent2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 150],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
MultiTool: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[
|
||||
{ node: 'Agent1', type: NodeConnectionTypes.AiTool, index: 0 },
|
||||
{ node: 'Agent2', type: NodeConnectionTypes.AiTool, index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const multiTool = workflow.getNode('MultiTool')!;
|
||||
const result = workflow.getParentMainInputNode(multiTool);
|
||||
|
||||
// Should follow the first connection in the array
|
||||
expect(result!.name).toBe('Agent1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('should handle null node input', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const result = workflow.getParentMainInputNode(null as any);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle undefined node input', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const result = workflow.getParentMainInputNode(undefined as any);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should throw error when connected node does not exist in workflow', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'ToolNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
ToolNode: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'NonExistentNode', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const toolNode = workflow.getNode('ToolNode')!;
|
||||
|
||||
expect(() => {
|
||||
workflow.getParentMainInputNode(toolNode);
|
||||
}).toThrow('Node "NonExistentNode" not found');
|
||||
});
|
||||
|
||||
test('should handle empty connection arrays', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'EmptyConnectionNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
EmptyConnectionNode: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[], // Empty connection array
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const emptyConnectionNode = workflow.getNode('EmptyConnectionNode')!;
|
||||
const result = workflow.getParentMainInputNode(emptyConnectionNode);
|
||||
|
||||
expect(result).toBe(emptyConnectionNode);
|
||||
});
|
||||
|
||||
test('should handle null connections in connection array', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'NullConnectionNode',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 100],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
NullConnectionNode: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: '', type: NodeConnectionTypes.AiTool, index: 0 }], // Connection with empty node name
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const nullConnectionNode = workflow.getNode('NullConnectionNode')!;
|
||||
const result = workflow.getParentMainInputNode(nullConnectionNode);
|
||||
|
||||
expect(result).toBe(nullConnectionNode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeConnectionIndexes', () => {
|
||||
@@ -3243,397 +2769,4 @@ describe('Workflow', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPath method', () => {
|
||||
test('should return true for self-reference', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
name: 'Node1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Node1', 'Node1')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when nodes are not connected', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
name: 'Node1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Node2',
|
||||
name: 'Node2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Node1', 'Node2')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for directly connected nodes', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
name: 'Node1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Node2',
|
||||
name: 'Node2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Node1: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Node1', 'Node2')).toBe(true);
|
||||
expect(workflow.hasPath('Node2', 'Node1')).toBe(true);
|
||||
});
|
||||
|
||||
test('should respect maximum depth limit', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
name: 'Node1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Node2',
|
||||
name: 'Node2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Node1: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
// Should find path with sufficient depth
|
||||
expect(workflow.hasPath('Node1', 'Node2', 5)).toBe(true);
|
||||
expect(workflow.hasPath('Node1', 'Node2', 1)).toBe(true);
|
||||
|
||||
// Should not find path with insufficient depth
|
||||
expect(workflow.hasPath('Node1', 'Node2', 0)).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle AI connection types', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Agent',
|
||||
name: 'Agent',
|
||||
type: 'test.ai.agent',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Tool1',
|
||||
name: 'Tool1',
|
||||
type: 'test.ai.tool',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Memory',
|
||||
name: 'Memory',
|
||||
type: 'test.ai.memory',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Tool1: {
|
||||
[NodeConnectionTypes.AiTool]: [
|
||||
[{ node: 'Agent', type: NodeConnectionTypes.AiTool, index: 0 }],
|
||||
],
|
||||
},
|
||||
Memory: {
|
||||
[NodeConnectionTypes.AiMemory]: [
|
||||
[{ node: 'Agent', type: NodeConnectionTypes.AiMemory, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Tool1', 'Agent')).toBe(true);
|
||||
expect(workflow.hasPath('Memory', 'Agent')).toBe(true);
|
||||
expect(workflow.hasPath('Tool1', 'Memory')).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle complex paths with multiple connection types', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Start',
|
||||
name: 'Start',
|
||||
type: 'test.start',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'VectorStore',
|
||||
name: 'VectorStore',
|
||||
type: 'test.vectorstore',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Document',
|
||||
name: 'Document',
|
||||
type: 'test.document',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'End',
|
||||
name: 'End',
|
||||
type: 'test.end',
|
||||
typeVersion: 1,
|
||||
position: [300, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Start: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'VectorStore', type: NodeConnectionTypes.AiVectorStore, index: 0 }],
|
||||
],
|
||||
},
|
||||
Document: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'VectorStore', type: NodeConnectionTypes.AiDocument, index: 0 }],
|
||||
],
|
||||
},
|
||||
VectorStore: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'End', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Start', 'End')).toBe(true);
|
||||
expect(workflow.hasPath('Document', 'End')).toBe(true);
|
||||
expect(workflow.hasPath('Start', 'Document')).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle cyclic graphs without infinite loops', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
name: 'Node1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Node2',
|
||||
name: 'Node2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Node3',
|
||||
name: 'Node3',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Node1: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Node2', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
Node2: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Node3', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
Node3: {
|
||||
[NodeConnectionTypes.Main]: [
|
||||
[{ node: 'Node1', type: NodeConnectionTypes.Main, index: 0 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Node1', 'Node3')).toBe(true);
|
||||
expect(workflow.hasPath('Node2', 'Node1')).toBe(true);
|
||||
expect(workflow.hasPath('Node3', 'Node2')).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle empty workflow', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('NonExistent1', 'NonExistent2')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle nodes with no outgoing connections', () => {
|
||||
const workflow = new Workflow({
|
||||
id: 'test',
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
name: 'Node1',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: 'Node2',
|
||||
name: 'Node2',
|
||||
type: 'test.set',
|
||||
typeVersion: 1,
|
||||
position: [100, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Node1: {
|
||||
[NodeConnectionTypes.Main]: [[]],
|
||||
},
|
||||
},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
expect(workflow.hasPath('Node1', 'Node2')).toBe(false);
|
||||
expect(workflow.hasPath('Node2', 'Node1')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when source node has pinned data (virtual path)', () => {
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Trigger',
|
||||
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'EditFields',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 1,
|
||||
position: [200, 0],
|
||||
parameters: {},
|
||||
},
|
||||
];
|
||||
|
||||
const connections: IConnections = {
|
||||
Trigger: {
|
||||
main: [[{ node: 'EditFields', type: 'main', index: 0 }]],
|
||||
},
|
||||
};
|
||||
|
||||
const pinData: IPinData = {
|
||||
Trigger: [
|
||||
{
|
||||
json: {
|
||||
name: 'Test item',
|
||||
value: 123,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const workflow = new Workflow({
|
||||
nodes,
|
||||
connections,
|
||||
active: false,
|
||||
nodeTypes,
|
||||
pinData,
|
||||
});
|
||||
|
||||
// Should return true because Trigger has pinned data, creating a virtual path
|
||||
expect(workflow.hasPath('Trigger', 'EditFields')).toBe(true);
|
||||
// Should also work for self-reference with pinned data
|
||||
expect(workflow.hasPath('Trigger', 'Trigger')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user