mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat: No expression error when node hasn’t executed (#8448)
Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -2584,6 +2584,6 @@ export type BannerName =
|
||||
| 'NON_PRODUCTION_LICENSE'
|
||||
| 'EMAIL_CONFIRMATION';
|
||||
|
||||
export type Functionality = 'regular' | 'configuration-node';
|
||||
export type Functionality = 'regular' | 'configuration-node' | 'pairedItem';
|
||||
|
||||
export type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
ProxyInput,
|
||||
} from './Interfaces';
|
||||
import * as NodeHelpers from './NodeHelpers';
|
||||
import { ExpressionError } from './errors/expression.error';
|
||||
import { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error';
|
||||
import type { Workflow } from './Workflow';
|
||||
import { augmentArray, augmentObject } from './AugmentObject';
|
||||
import { deepCopy } from './utils';
|
||||
@@ -104,6 +104,7 @@ export class WorkflowDataProxy {
|
||||
{
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -274,19 +275,25 @@ export class WorkflowDataProxy {
|
||||
);
|
||||
}
|
||||
|
||||
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
||||
if (that.workflow.getNode(nodeName)) {
|
||||
throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
if (!that.workflow.getNode(nodeName)) {
|
||||
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
|
||||
!that.workflow.getPinDataOfNode(nodeName)
|
||||
) {
|
||||
throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_node_execution_data',
|
||||
nodeCause: nodeName,
|
||||
});
|
||||
}
|
||||
|
||||
runIndex = runIndex === undefined ? that.defaultReturnRunIndex : runIndex;
|
||||
runIndex =
|
||||
runIndex === -1 ? that.runExecutionData.resultData.runData[nodeName].length - 1 : runIndex;
|
||||
@@ -372,6 +379,28 @@ export class WorkflowDataProxy {
|
||||
|
||||
if (['binary', 'data', 'json'].includes(name)) {
|
||||
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
|
||||
|
||||
if (executionData.length === 0) {
|
||||
if (that.workflow.getParentNodes(nodeName).length === 0) {
|
||||
throw new ExpressionError('No execution data available', {
|
||||
messageTemplate:
|
||||
'No execution data available to expression under ‘%%PARAMETER%%’',
|
||||
description:
|
||||
'This node has no input data. Please make sure this node is connected to another node.',
|
||||
nodeCause: nodeName,
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_input_connection',
|
||||
});
|
||||
}
|
||||
|
||||
throw new ExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
});
|
||||
}
|
||||
|
||||
if (executionData.length <= that.itemIndex) {
|
||||
throw new ExpressionError(`No data found for item-index: "${that.itemIndex}"`, {
|
||||
runIndex: that.runIndex,
|
||||
@@ -615,22 +644,13 @@ export class WorkflowDataProxy {
|
||||
|
||||
const createExpressionError = (
|
||||
message: string,
|
||||
context?: {
|
||||
causeDetailed?: string;
|
||||
description?: string;
|
||||
descriptionTemplate?: string;
|
||||
functionality?: 'pairedItem';
|
||||
context?: ExpressionErrorOptions & {
|
||||
moreInfoLink?: boolean;
|
||||
functionOverrides?: {
|
||||
// Custom data to display for Function-Nodes
|
||||
message?: string;
|
||||
description?: string;
|
||||
};
|
||||
itemIndex?: number;
|
||||
messageTemplate?: string;
|
||||
moreInfoLink?: boolean;
|
||||
nodeCause?: string;
|
||||
runIndex?: number;
|
||||
type?: string;
|
||||
},
|
||||
) => {
|
||||
if (isScriptingNode(that.activeNodeName, that.workflow) && context?.functionOverrides) {
|
||||
@@ -678,7 +698,7 @@ export class WorkflowDataProxy {
|
||||
incomingSourceData: ISourceData | null,
|
||||
pairedItem: IPairedItemData,
|
||||
): INodeExecutionData | null => {
|
||||
let taskData: ITaskData;
|
||||
let taskData: ITaskData | undefined;
|
||||
|
||||
let sourceData: ISourceData | null = incomingSourceData;
|
||||
|
||||
@@ -697,13 +717,12 @@ export class WorkflowDataProxy {
|
||||
let nodeBeforeLast: string | undefined;
|
||||
|
||||
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
|
||||
taskData =
|
||||
that.runExecutionData!.resultData.runData[sourceData.previousNode][
|
||||
sourceData?.previousNodeRun || 0
|
||||
];
|
||||
|
||||
const runIndex = sourceData?.previousNodeRun || 0;
|
||||
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
||||
if (previousNodeOutput >= taskData.data!.main.length) {
|
||||
taskData =
|
||||
that.runExecutionData?.resultData?.runData?.[sourceData.previousNode]?.[runIndex];
|
||||
|
||||
if (taskData?.data?.main && previousNodeOutput >= taskData.data.main.length) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
functionOverrides: {
|
||||
@@ -716,7 +735,12 @@ export class WorkflowDataProxy {
|
||||
});
|
||||
}
|
||||
|
||||
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
|
||||
const previousNodeOutputData =
|
||||
taskData?.data?.main?.[previousNodeOutput] ??
|
||||
(that.workflow.getPinDataOfNode(sourceData.previousNode) as INodeExecutionData[]);
|
||||
const source = taskData?.source ?? [];
|
||||
|
||||
if (pairedItem.item >= previousNodeOutputData.length) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
functionality: 'pairedItem',
|
||||
@@ -733,13 +757,12 @@ export class WorkflowDataProxy {
|
||||
}points to an input item on node ‘<strong>${
|
||||
sourceData.previousNode
|
||||
}</strong>‘ that doesn’t exist.`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
|
||||
const itemPreviousNode: INodeExecutionData =
|
||||
taskData.data!.main[previousNodeOutput]![pairedItem.item];
|
||||
const itemPreviousNode: INodeExecutionData = previousNodeOutputData[pairedItem.item];
|
||||
|
||||
if (itemPreviousNode.pairedItem === undefined) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
@@ -751,7 +774,7 @@ export class WorkflowDataProxy {
|
||||
nodeCause: sourceData.previousNode,
|
||||
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘<strong>${sourceData.previousNode}</strong>’`,
|
||||
causeDetailed: `Missing pairedItem data (node ‘${sourceData.previousNode}’ probably didn’t supply it)`,
|
||||
type: 'no pairing info',
|
||||
type: 'paired_item_no_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
@@ -763,13 +786,13 @@ export class WorkflowDataProxy {
|
||||
.map((item) => {
|
||||
try {
|
||||
const itemInput = item.input || 0;
|
||||
if (itemInput >= taskData.source.length) {
|
||||
if (itemInput >= source.length) {
|
||||
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData!.previousNode}'.`
|
||||
// Actual error does not matter as it gets caught below and `null` will be returned
|
||||
throw new ApplicationError('Not found');
|
||||
}
|
||||
|
||||
return getPairedItem(destinationNodeName, taskData.source[itemInput], item);
|
||||
return getPairedItem(destinationNodeName, source[itemInput], item);
|
||||
} catch (error) {
|
||||
// Means pairedItem could not be found
|
||||
return null;
|
||||
@@ -793,7 +816,7 @@ export class WorkflowDataProxy {
|
||||
message: 'Invalid code',
|
||||
},
|
||||
description: `The expression uses data in the node ‘<strong>${destinationNodeName}</strong>’ but there is more than one matching item in that node`,
|
||||
type: 'multiple matches',
|
||||
type: 'paired_item_multiple_matches',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -812,8 +835,8 @@ export class WorkflowDataProxy {
|
||||
}
|
||||
|
||||
const itemInput = pairedItem.input || 0;
|
||||
if (itemInput >= taskData.source.length) {
|
||||
if (taskData.source.length === 0) {
|
||||
if (itemInput >= source.length) {
|
||||
if (source.length === 0) {
|
||||
// A trigger node got reached, so looks like that that item can not be resolved
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
@@ -823,7 +846,7 @@ export class WorkflowDataProxy {
|
||||
message: 'Invalid code',
|
||||
},
|
||||
description: `The expression uses data in the node ‘<strong>${destinationNodeName}</strong>’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
type: 'no connection',
|
||||
type: 'paired_item_no_connection',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
@@ -841,12 +864,12 @@ export class WorkflowDataProxy {
|
||||
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
|
||||
: ''
|
||||
}points to a branch that doesn’t exist.`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
});
|
||||
}
|
||||
|
||||
nodeBeforeLast = sourceData.previousNode;
|
||||
sourceData = taskData.source[pairedItem.input || 0] || null;
|
||||
sourceData = source[pairedItem.input || 0] || null;
|
||||
|
||||
if (pairedItem.sourceOverwrite) {
|
||||
sourceData = pairedItem.sourceOverwrite;
|
||||
@@ -862,7 +885,7 @@ export class WorkflowDataProxy {
|
||||
},
|
||||
nodeCause: nodeBeforeLast,
|
||||
description: 'Could not resolve, probably no pairedItem exists',
|
||||
type: 'no pairing info',
|
||||
type: 'paired_item_no_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
@@ -882,7 +905,7 @@ export class WorkflowDataProxy {
|
||||
},
|
||||
description: 'Item points to a node output which does not exist',
|
||||
causeDetailed: `The sourceData points to a node output ‘${previousNodeOutput}‘ which does not exist on node ‘${sourceData.previousNode}‘ (output node did probably supply a wrong one)`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -903,7 +926,7 @@ export class WorkflowDataProxy {
|
||||
}points to an input item on node ‘<strong>${
|
||||
sourceData.previousNode
|
||||
}</strong>‘ that doesn’t exist.`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
@@ -922,6 +945,18 @@ export class WorkflowDataProxy {
|
||||
throw createExpressionError(`"${nodeName}" node doesn't exist`);
|
||||
}
|
||||
|
||||
const ensureNodeExecutionData = () => {
|
||||
if (
|
||||
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
||||
!that.workflow.getPinDataOfNode(nodeName)
|
||||
) {
|
||||
throw createExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
type: 'no_node_execution_data',
|
||||
nodeCause: nodeName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
@@ -942,13 +977,38 @@ export class WorkflowDataProxy {
|
||||
get(target, property, receiver) {
|
||||
if (property === 'isProxy') return true;
|
||||
|
||||
if (!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName)) {
|
||||
if (property === 'isExecuted') return false;
|
||||
throw createExpressionError(`no data, execute "${nodeName}" node first`);
|
||||
if (property === 'isExecuted') {
|
||||
return (
|
||||
that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) ?? false
|
||||
);
|
||||
}
|
||||
if (property === 'isExecuted') return true;
|
||||
|
||||
if (['pairedItem', 'itemMatching', 'item'].includes(property as string)) {
|
||||
// Before resolving the pairedItem make sure that the requested node comes in the
|
||||
// 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;
|
||||
}
|
||||
const parentNodes = that.workflow.getParentNodes(contextNode);
|
||||
if (!parentNodes.includes(nodeName)) {
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
functionality: 'pairedItem',
|
||||
functionOverrides: {
|
||||
description: `The code uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
message: `No path back to node ‘${nodeName}’`,
|
||||
},
|
||||
description: `The expression uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
nodeCause: nodeName,
|
||||
type: 'paired_item_no_connection',
|
||||
});
|
||||
}
|
||||
|
||||
ensureNodeExecutionData();
|
||||
|
||||
const pairedItemMethod = (itemIndex?: number) => {
|
||||
if (itemIndex === undefined) {
|
||||
if (property === 'itemMatching') {
|
||||
@@ -960,11 +1020,26 @@ export class WorkflowDataProxy {
|
||||
}
|
||||
|
||||
const executionData = that.connectionInputData;
|
||||
const input = executionData[itemIndex];
|
||||
if (!input) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
functionality: 'pairedItem',
|
||||
functionOverrides: {
|
||||
description: `Some intermediate nodes between ‘<strong>${nodeName}</strong>‘ and ‘<strong>${that.activeNodeName}</strong>‘ have not executed yet.`,
|
||||
message: 'Can’t get data',
|
||||
},
|
||||
description: `Some intermediate nodes between ‘<strong>${nodeName}</strong>‘ and ‘<strong>${that.activeNodeName}</strong>‘ have not executed yet.`,
|
||||
causeDetailed: `pairedItem can\'t be found when intermediate nodes between ‘<strong>${nodeName}</strong>‘ and ‘<strong>${that.activeNodeName}</strong> have not executed yet.`,
|
||||
itemIndex,
|
||||
type: 'paired_item_intermediate_nodes',
|
||||
});
|
||||
}
|
||||
|
||||
// As we operate on the incoming item we can be sure that pairedItem is not an
|
||||
// array. After all can it only come from exactly one previous node via a certain
|
||||
// input. For that reason do we not have to consider the array case.
|
||||
const pairedItem = executionData[itemIndex].pairedItem as IPairedItemData;
|
||||
const pairedItem = input.pairedItem as IPairedItemData;
|
||||
|
||||
if (pairedItem === undefined) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
@@ -994,28 +1069,6 @@ export class WorkflowDataProxy {
|
||||
});
|
||||
}
|
||||
|
||||
// Before resolving the pairedItem make sure that the requested node comes in the
|
||||
// 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;
|
||||
}
|
||||
const parentNodes = that.workflow.getParentNodes(contextNode);
|
||||
if (!parentNodes.includes(nodeName)) {
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
functionality: 'pairedItem',
|
||||
functionOverrides: {
|
||||
description: `The code uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
message: `No path back to node ‘${nodeName}’`,
|
||||
},
|
||||
description: `The expression uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
const sourceData: ISourceData | null =
|
||||
that.executeData.source.main[pairedItem.input || 0] ??
|
||||
that.executeData.source.main[0];
|
||||
@@ -1029,6 +1082,7 @@ export class WorkflowDataProxy {
|
||||
return pairedItemMethod;
|
||||
}
|
||||
if (property === 'first') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
if (executionData[0]) return executionData[0];
|
||||
@@ -1036,6 +1090,7 @@ export class WorkflowDataProxy {
|
||||
};
|
||||
}
|
||||
if (property === 'last') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
if (!executionData.length) return undefined;
|
||||
@@ -1046,6 +1101,7 @@ export class WorkflowDataProxy {
|
||||
};
|
||||
}
|
||||
if (property === 'all') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) =>
|
||||
getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
}
|
||||
@@ -1075,6 +1131,14 @@ export class WorkflowDataProxy {
|
||||
get(target, property, receiver) {
|
||||
if (property === 'isProxy') return true;
|
||||
|
||||
if (that.connectionInputData.length === 0) {
|
||||
throw createExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
});
|
||||
}
|
||||
|
||||
if (property === 'item') {
|
||||
return that.connectionInputData[that.itemIndex];
|
||||
}
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import type { IDataObject } from '../Interfaces';
|
||||
import { ExecutionBaseError } from './abstract/execution-base.error';
|
||||
|
||||
export interface ExpressionErrorOptions {
|
||||
cause?: Error;
|
||||
causeDetailed?: string;
|
||||
description?: string;
|
||||
descriptionTemplate?: string;
|
||||
functionality?: 'pairedItem';
|
||||
itemIndex?: number;
|
||||
messageTemplate?: string;
|
||||
nodeCause?: string;
|
||||
parameter?: string;
|
||||
runIndex?: number;
|
||||
type?:
|
||||
| 'no_execution_data'
|
||||
| 'no_node_execution_data'
|
||||
| 'no_input_connection'
|
||||
| 'internal'
|
||||
| 'paired_item_invalid_info'
|
||||
| 'paired_item_no_info'
|
||||
| 'paired_item_multiple_matches'
|
||||
| 'paired_item_no_connection'
|
||||
| 'paired_item_intermediate_nodes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for instantiating an expression error
|
||||
*/
|
||||
export class ExpressionError extends ExecutionBaseError {
|
||||
constructor(
|
||||
message: string,
|
||||
options?: {
|
||||
cause?: Error;
|
||||
causeDetailed?: string;
|
||||
description?: string;
|
||||
descriptionTemplate?: string;
|
||||
functionality?: 'pairedItem';
|
||||
itemIndex?: number;
|
||||
messageTemplate?: string;
|
||||
nodeCause?: string;
|
||||
parameter?: string;
|
||||
runIndex?: number;
|
||||
type?: string;
|
||||
},
|
||||
) {
|
||||
constructor(message: string, options?: ExpressionErrorOptions) {
|
||||
super(message, { cause: options?.cause });
|
||||
|
||||
if (options?.description !== undefined) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { INodeExecutionData } from '@/Interfaces';
|
||||
import { extendSyntax } from '@/Extensions/ExpressionExtension';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy';
|
||||
import { workflow } from './ExpressionExtensions/Helpers';
|
||||
|
||||
setDifferEnabled(true);
|
||||
|
||||
@@ -172,24 +173,6 @@ for (const evaluator of ['tmpl', 'tournament'] as const) {
|
||||
});
|
||||
|
||||
describe('Test all expression value fixtures', () => {
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({
|
||||
id: '1',
|
||||
nodes: [
|
||||
{
|
||||
name: 'node',
|
||||
typeVersion: 1,
|
||||
type: 'test.set',
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const expression = workflow.expression;
|
||||
|
||||
const evaluate = (value: string, data: INodeExecutionData[]) => {
|
||||
@@ -202,12 +185,18 @@ for (const evaluator of ['tmpl', 'tournament'] as const) {
|
||||
continue;
|
||||
}
|
||||
test(t.expression, () => {
|
||||
for (const test of t.tests.filter(
|
||||
const evaluationTests = t.tests.filter(
|
||||
(test) => test.type === 'evaluation',
|
||||
) as ExpressionTestEvaluation[]) {
|
||||
expect(
|
||||
evaluate(t.expression, test.input.map((d) => ({ json: d })) as any),
|
||||
).toStrictEqual(test.output);
|
||||
) as ExpressionTestEvaluation[];
|
||||
|
||||
for (const test of evaluationTests) {
|
||||
const input = test.input.map((d) => ({ json: d })) as any;
|
||||
|
||||
if ('error' in test) {
|
||||
expect(() => evaluate(t.expression, input)).toThrowError(test.error);
|
||||
} else {
|
||||
expect(evaluate(t.expression, input)).toStrictEqual(test.output);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import type { GenericValue, IDataObject } from '@/Interfaces';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
|
||||
export interface ExpressionTestBase {
|
||||
type: string;
|
||||
interface ExpressionTestBase {
|
||||
type: 'evaluation' | 'transform';
|
||||
}
|
||||
|
||||
export interface ExpressionTestEvaluation extends ExpressionTestBase {
|
||||
interface ExpressionTestSuccess extends ExpressionTestBase {
|
||||
type: 'evaluation';
|
||||
input: Array<IDataObject | GenericValue>;
|
||||
output: IDataObject | GenericValue;
|
||||
}
|
||||
|
||||
interface ExpressionTestFailure extends ExpressionTestBase {
|
||||
type: 'evaluation';
|
||||
input: Array<IDataObject | GenericValue>;
|
||||
error: ExpressionError;
|
||||
}
|
||||
|
||||
export interface ExpressionTestTransform extends ExpressionTestBase {
|
||||
type: 'transform';
|
||||
// If we don't specify a result we expect it to be the same as the input
|
||||
@@ -17,6 +24,7 @@ export interface ExpressionTestTransform extends ExpressionTestBase {
|
||||
forceTransform?: boolean;
|
||||
}
|
||||
|
||||
export type ExpressionTestEvaluation = ExpressionTestSuccess | ExpressionTestFailure;
|
||||
export type ExpressionTests = ExpressionTestEvaluation | ExpressionTestTransform;
|
||||
|
||||
export interface ExpressionTestFixture {
|
||||
@@ -265,7 +273,11 @@ export const baseFixtures: ExpressionTestFixture[] = [
|
||||
{
|
||||
type: 'evaluation',
|
||||
input: [],
|
||||
output: undefined,
|
||||
error: new ExpressionError('No execution data available', {
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
type: 'no_execution_data',
|
||||
}),
|
||||
},
|
||||
{ type: 'transform' },
|
||||
{ type: 'transform', forceTransform: true },
|
||||
|
||||
@@ -38,6 +38,8 @@ import { deepCopy } from '@/utils';
|
||||
import { getGlobalState } from '@/GlobalState';
|
||||
import { ApplicationError } from '@/errors/application.error';
|
||||
import { NodeTypes as NodeTypesClass } from './NodeTypes';
|
||||
import { readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface INodeTypesObject {
|
||||
[key: string]: INodeType;
|
||||
@@ -558,3 +560,7 @@ export function WorkflowExecuteAdditionalData(): IWorkflowExecuteAdditionalData
|
||||
userId: '123',
|
||||
};
|
||||
}
|
||||
|
||||
const BASE_DIR = path.resolve(__dirname, '..');
|
||||
export const readJsonFileSync = <T>(filePath: string) =>
|
||||
JSON.parse(readFileSync(path.join(BASE_DIR, filePath), 'utf-8')) as T;
|
||||
|
||||
@@ -536,42 +536,45 @@ const googleSheetsNode: LoadedClass<IVersionedNodeType> = {
|
||||
},
|
||||
};
|
||||
|
||||
const setNode: LoadedClass<INodeType> = {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class NodeTypes implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {
|
||||
'n8n-nodes-base.stickyNote': stickyNode,
|
||||
'n8n-nodes-base.set': setNode,
|
||||
'test.googleSheets': googleSheetsNode,
|
||||
'test.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.set': setNode,
|
||||
'test.setMulti': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
|
||||
@@ -1,344 +1,144 @@
|
||||
import type { IConnections, IExecuteData, INode, IRunExecutionData } from '@/Interfaces';
|
||||
import type { IExecuteData, INode, IRun, IWorkflowBase } from '@/Interfaces';
|
||||
import { Workflow } from '@/Workflow';
|
||||
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
||||
import * as Helpers from './Helpers';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
import * as Helpers from './Helpers';
|
||||
|
||||
describe('WorkflowDataProxy', () => {
|
||||
describe('test data proxy', () => {
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
name: 'Start',
|
||||
type: 'test.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
name: 'Function',
|
||||
type: 'test.set',
|
||||
parameters: {
|
||||
functionCode:
|
||||
'// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require("luxon");\n\nconst data = [\n {\n "length": 105\n },\n {\n "length": 160\n },\n {\n "length": 121\n },\n {\n "length": 275\n },\n {\n "length": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));',
|
||||
},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [280, 200],
|
||||
},
|
||||
{
|
||||
name: 'Rename',
|
||||
type: 'test.set',
|
||||
parameters: {
|
||||
value1: 'data',
|
||||
value2: 'initialName',
|
||||
},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [460, 200],
|
||||
},
|
||||
{
|
||||
name: 'Set',
|
||||
type: 'test.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [640, 200],
|
||||
},
|
||||
{
|
||||
name: 'End',
|
||||
type: 'test.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-5',
|
||||
position: [640, 200],
|
||||
},
|
||||
];
|
||||
const loadFixture = (fixture: string) => {
|
||||
const workflow = Helpers.readJsonFileSync<IWorkflowBase>(
|
||||
`test/fixtures/WorkflowDataProxy/${fixture}_workflow.json`,
|
||||
);
|
||||
const run = Helpers.readJsonFileSync<IRun>(`test/fixtures/WorkflowDataProxy/${fixture}_run.json`);
|
||||
|
||||
const connections: IConnections = {
|
||||
Start: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Function',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Function: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Rename',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Rename: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'End',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
return { workflow, run };
|
||||
};
|
||||
|
||||
const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNode: string) => {
|
||||
const taskData = run?.data.resultData.runData[activeNode]?.[0];
|
||||
const lastNodeConnectionInputData = taskData?.data?.main[0];
|
||||
|
||||
let executeData: IExecuteData | undefined;
|
||||
|
||||
if (taskData) {
|
||||
executeData = {
|
||||
data: taskData.data!,
|
||||
node: workflow.nodes.find((node) => node.name === activeNode) as INode,
|
||||
source: {
|
||||
main: taskData.source,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
resultData: {
|
||||
runData: {
|
||||
Start: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
Function: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { initialName: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 160 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 121 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 275 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 950 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Start',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
Rename: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { data: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { data: 160 },
|
||||
pairedItem: { item: 1 },
|
||||
},
|
||||
{
|
||||
json: { data: 121 },
|
||||
pairedItem: { item: 2 },
|
||||
},
|
||||
{
|
||||
json: { data: 275 },
|
||||
pairedItem: { item: 3 },
|
||||
},
|
||||
{
|
||||
json: { data: 950 },
|
||||
pairedItem: { item: 4 },
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Function',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
End: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { data: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { data: 160 },
|
||||
pairedItem: { item: 1 },
|
||||
},
|
||||
{
|
||||
json: { data: 121 },
|
||||
pairedItem: { item: 2 },
|
||||
},
|
||||
{
|
||||
json: { data: 275 },
|
||||
pairedItem: { item: 3 },
|
||||
},
|
||||
{
|
||||
json: { data: 950 },
|
||||
pairedItem: { item: 4 },
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Rename',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({
|
||||
const dataProxy = new WorkflowDataProxy(
|
||||
new Workflow({
|
||||
id: '123',
|
||||
name: 'test workflow',
|
||||
nodes,
|
||||
connections,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
const nameLastNode = 'End';
|
||||
nodeTypes: Helpers.NodeTypes(),
|
||||
}),
|
||||
run?.data ?? null,
|
||||
0,
|
||||
0,
|
||||
activeNode,
|
||||
lastNodeConnectionInputData ?? [],
|
||||
{},
|
||||
'manual',
|
||||
{},
|
||||
executeData,
|
||||
);
|
||||
|
||||
const lastNodeConnectionInputData =
|
||||
runExecutionData.resultData.runData[nameLastNode][0].data!.main[0];
|
||||
return dataProxy.getDataProxy();
|
||||
};
|
||||
|
||||
const executeData: IExecuteData = {
|
||||
data: runExecutionData.resultData.runData[nameLastNode][0].data!,
|
||||
node: nodes.find((node) => node.name === nameLastNode) as INode,
|
||||
source: {
|
||||
main: runExecutionData.resultData.runData[nameLastNode][0].source,
|
||||
},
|
||||
};
|
||||
describe('WorkflowDataProxy', () => {
|
||||
describe('Base', () => {
|
||||
const fixture = loadFixture('base');
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||
|
||||
const dataProxy = new WorkflowDataProxy(
|
||||
workflow,
|
||||
runExecutionData,
|
||||
0,
|
||||
0,
|
||||
nameLastNode,
|
||||
lastNodeConnectionInputData ?? [],
|
||||
{},
|
||||
'manual',
|
||||
{},
|
||||
executeData,
|
||||
);
|
||||
const proxy = dataProxy.getDataProxy();
|
||||
|
||||
test('test $("NodeName").all()', () => {
|
||||
test('$("NodeName").all()', () => {
|
||||
expect(proxy.$('Rename').all()[1].json.data).toEqual(160);
|
||||
});
|
||||
test('test $("NodeName").all() length', () => {
|
||||
test('$("NodeName").all() length', () => {
|
||||
expect(proxy.$('Rename').all().length).toEqual(5);
|
||||
});
|
||||
test('test $("NodeName").item', () => {
|
||||
test('$("NodeName").item', () => {
|
||||
expect(proxy.$('Rename').item).toEqual({ json: { data: 105 }, pairedItem: { item: 0 } });
|
||||
});
|
||||
test('test $("NodeNameEarlier").item', () => {
|
||||
test('$("NodeNameEarlier").item', () => {
|
||||
expect(proxy.$('Function').item).toEqual({
|
||||
json: { initialName: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
});
|
||||
});
|
||||
test('test $("NodeName").itemMatching(2)', () => {
|
||||
test('$("NodeName").itemMatching(2)', () => {
|
||||
expect(proxy.$('Rename').itemMatching(2).json.data).toEqual(121);
|
||||
});
|
||||
test('test $("NodeName").first()', () => {
|
||||
test('$("NodeName").first()', () => {
|
||||
expect(proxy.$('Rename').first().json.data).toEqual(105);
|
||||
});
|
||||
test('test $("NodeName").last()', () => {
|
||||
test('$("NodeName").last()', () => {
|
||||
expect(proxy.$('Rename').last().json.data).toEqual(950);
|
||||
});
|
||||
|
||||
test('test $("NodeName").params', () => {
|
||||
test('$("NodeName").params', () => {
|
||||
expect(proxy.$('Rename').params).toEqual({ value1: 'data', value2: 'initialName' });
|
||||
});
|
||||
|
||||
test('$("NodeName")', () => {
|
||||
test('$("NodeName") not in workflow should throw', () => {
|
||||
expect(() => proxy.$('doNotExist')).toThrowError(ExpressionError);
|
||||
});
|
||||
|
||||
test('test $("NodeName").isExecuted', () => {
|
||||
test('$("NodeName").item on Node that has not executed', () => {
|
||||
expect(() => proxy.$('Set').item).toThrowError(ExpressionError);
|
||||
});
|
||||
|
||||
test('$("NodeName").isExecuted', () => {
|
||||
expect(proxy.$('Function').isExecuted).toEqual(true);
|
||||
expect(proxy.$('Set').isExecuted).toEqual(false);
|
||||
});
|
||||
|
||||
test('test $input.all()', () => {
|
||||
test('$input.all()', () => {
|
||||
expect(proxy.$input.all()[1].json.data).toEqual(160);
|
||||
});
|
||||
test('test $input.all() length', () => {
|
||||
test('$input.all() length', () => {
|
||||
expect(proxy.$input.all().length).toEqual(5);
|
||||
});
|
||||
test('test $input.first()', () => {
|
||||
expect(proxy.$input.first().json.data).toEqual(105);
|
||||
test('$input.first()', () => {
|
||||
expect(proxy.$input.first()?.json?.data).toEqual(105);
|
||||
});
|
||||
test('test $input.last()', () => {
|
||||
expect(proxy.$input.last().json.data).toEqual(950);
|
||||
test('$input.last()', () => {
|
||||
expect(proxy.$input.last()?.json?.data).toEqual(950);
|
||||
});
|
||||
test('test $input.item', () => {
|
||||
expect(proxy.$input.item.json.data).toEqual(105);
|
||||
test('$input.item', () => {
|
||||
expect(proxy.$input.item?.json?.data).toEqual(105);
|
||||
});
|
||||
test('test $thisItem', () => {
|
||||
test('$thisItem', () => {
|
||||
expect(proxy.$thisItem.json.data).toEqual(105);
|
||||
});
|
||||
|
||||
test('test $binary', () => {
|
||||
test('$binary', () => {
|
||||
expect(proxy.$binary).toEqual({});
|
||||
});
|
||||
|
||||
test('test $json', () => {
|
||||
test('$json', () => {
|
||||
expect(proxy.$json).toEqual({ data: 105 });
|
||||
});
|
||||
|
||||
test('test $itemIndex', () => {
|
||||
test('$itemIndex', () => {
|
||||
expect(proxy.$itemIndex).toEqual(0);
|
||||
});
|
||||
|
||||
test('test $prevNode', () => {
|
||||
test('$prevNode', () => {
|
||||
expect(proxy.$prevNode).toEqual({ name: 'Rename', outputIndex: 0, runIndex: 0 });
|
||||
});
|
||||
|
||||
test('test $runIndex', () => {
|
||||
test('$runIndex', () => {
|
||||
expect(proxy.$runIndex).toEqual(0);
|
||||
});
|
||||
|
||||
test('test $workflow', () => {
|
||||
test('$workflow', () => {
|
||||
expect(proxy.$workflow).toEqual({
|
||||
active: false,
|
||||
id: '123',
|
||||
@@ -346,4 +146,141 @@ describe('WorkflowDataProxy', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Errors', () => {
|
||||
const fixture = loadFixture('errors');
|
||||
|
||||
test('$("NodeName").item, Node does not exist', (done) => {
|
||||
const proxy = getProxyFromFixture(
|
||||
fixture.workflow,
|
||||
fixture.run,
|
||||
'Reference non-existent node',
|
||||
);
|
||||
try {
|
||||
proxy.$('does not exist').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('"does not exist" node doesn\'t exist');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, node has no connection to referenced node', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoPathBack');
|
||||
try {
|
||||
proxy.$('Customer Datastore (n8n training)').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Invalid expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_connection');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").first(), node has no connection to referenced node', (done) => {
|
||||
const proxy = getProxyFromFixture(
|
||||
fixture.workflow,
|
||||
fixture.run,
|
||||
'Reference impossible with .first()',
|
||||
);
|
||||
try {
|
||||
proxy.$('Impossible').first().json.name;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('no data, execute "Impossible" node first');
|
||||
expect(exprError.context.type).toEqual('no_node_execution_data');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$json, Node has no connections', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoInputConnection');
|
||||
try {
|
||||
proxy.$json.email;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('No execution data available');
|
||||
expect(exprError.context.type).toEqual('no_input_connection');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, Node has not run', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible');
|
||||
try {
|
||||
proxy.$('Impossible if').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('no data, execute "Impossible if" node first');
|
||||
expect(exprError.context.type).toEqual('no_node_execution_data');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$json, Node has not run', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible');
|
||||
try {
|
||||
proxy.$json.email;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('No execution data available');
|
||||
expect(exprError.context.type).toEqual('no_execution_data');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, paired item error: more than 1 matching item', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemMultipleMatches');
|
||||
try {
|
||||
proxy.$('Edit Fields').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Invalid expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_multiple_matches');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, paired item error: missing paired item', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemInfoMissing');
|
||||
try {
|
||||
proxy.$('Edit Fields').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Can’t get data for expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, paired item error: invalid paired item', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'IncorrectPairedItem');
|
||||
try {
|
||||
proxy.$('Edit Fields').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Can’t get data for expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_invalid_info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
140
packages/workflow/test/fixtures/WorkflowDataProxy/base_run.json
vendored
Normal file
140
packages/workflow/test/fixtures/WorkflowDataProxy/base_run.json
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"data": {
|
||||
"startData": {},
|
||||
"resultData": {
|
||||
"runData": {
|
||||
"Start": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"Function": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": { "initialName": 105 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 160 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 121 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 275 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 950 },
|
||||
"pairedItem": { "item": 0 }
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Start"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Rename": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": { "data": 105 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 160 },
|
||||
"pairedItem": { "item": 1 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 121 },
|
||||
"pairedItem": { "item": 2 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 275 },
|
||||
"pairedItem": { "item": 3 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 950 },
|
||||
"pairedItem": { "item": 4 }
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Function"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"End": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": { "data": 105 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 160 },
|
||||
"pairedItem": { "item": 1 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 121 },
|
||||
"pairedItem": { "item": 2 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 275 },
|
||||
"pairedItem": { "item": 3 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 950 },
|
||||
"pairedItem": { "item": 4 }
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Rename"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"mode": "manual",
|
||||
"startedAt": "2024-02-08T15:45:18.848Z",
|
||||
"stoppedAt": "2024-02-08T15:45:18.862Z",
|
||||
"status": "running"
|
||||
}
|
||||
86
packages/workflow/test/fixtures/WorkflowDataProxy/base_workflow.json
vendored
Normal file
86
packages/workflow/test/fixtures/WorkflowDataProxy/base_workflow.json
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Start",
|
||||
"type": "test.set",
|
||||
"parameters": {},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-1",
|
||||
"position": [100, 200]
|
||||
},
|
||||
{
|
||||
"name": "Function",
|
||||
"type": "test.set",
|
||||
"parameters": {
|
||||
"functionCode": "// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require(\"luxon\");\n\nconst data = [\n {\n \"length\": 105\n },\n {\n \"length\": 160\n },\n {\n \"length\": 121\n },\n {\n \"length\": 275\n },\n {\n \"length\": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));"
|
||||
},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-2",
|
||||
"position": [280, 200]
|
||||
},
|
||||
{
|
||||
"name": "Rename",
|
||||
"type": "test.set",
|
||||
"parameters": {
|
||||
"value1": "data",
|
||||
"value2": "initialName"
|
||||
},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-3",
|
||||
"position": [460, 200]
|
||||
},
|
||||
{
|
||||
"name": "Set",
|
||||
"type": "test.set",
|
||||
"parameters": {},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-4",
|
||||
"position": [640, 200]
|
||||
},
|
||||
{
|
||||
"name": "End",
|
||||
"type": "test.set",
|
||||
"parameters": {},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-5",
|
||||
"position": [640, 200]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Function",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Function": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Rename",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Rename": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "End",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1017
packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json
vendored
Normal file
1017
packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
747
packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json
vendored
Normal file
747
packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json
vendored
Normal file
@@ -0,0 +1,747 @@
|
||||
{
|
||||
"name": "WorkflowDataProxy errors",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "b5122d27-4bb5-4100-a69b-03b1dcac76c7",
|
||||
"name": "When clicking \"Test workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAllPeople"
|
||||
},
|
||||
"id": "bf471582-900d-47af-848c-2d4218798775",
|
||||
"name": "Customer Datastore (n8n training)",
|
||||
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
|
||||
"typeVersion": 1,
|
||||
"position": [1180, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "name",
|
||||
"stringValue": "={{ $json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "1de94b04-c87b-4ef1-b5d7-5078f9e33220",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1400, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "These expression should always be red — there is no way of getting the input data even if you execute. Text should be:",
|
||||
"height": 349.2762683040461,
|
||||
"width": 339
|
||||
},
|
||||
"id": "c277f7c6-8a7a-41e9-9484-78e90bd205bf",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 1040]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToAggregate": {
|
||||
"fieldToAggregate": [
|
||||
{
|
||||
"fieldToAggregate": "name"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "f6606ff5-4d66-4efb-8dad-de7662f20867",
|
||||
"name": "Aggregate",
|
||||
"type": "n8n-nodes-base.aggregate",
|
||||
"typeVersion": 1,
|
||||
"position": [1820, 860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "71fbae4a-f5b3-4db1-9684-83c4d2037099",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 760]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "[No path back to node]",
|
||||
"height": 209,
|
||||
"width": 150
|
||||
},
|
||||
"id": "24e878cb-a681-4c00-bec1-83188aa20eb7",
|
||||
"name": "Sticky Note2",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 1132]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "[No input connected]",
|
||||
"height": 201,
|
||||
"width": 150
|
||||
},
|
||||
"id": "4bd26f55-87b5-4ad1-b3f1-ae2786941114",
|
||||
"name": "Sticky Note3",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 1132]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"field\": \"the same\"\n }\n];"
|
||||
},
|
||||
"id": "6538818e-c5b3-422b-920c-d5d52533578b",
|
||||
"name": "Break pairedItem chain",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1120]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "42641e54-60e1-46d7-bcb4-b55a83f89f6b",
|
||||
"name": "Sticky Note4",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1020]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": 99\n }\n];"
|
||||
},
|
||||
"id": "05583883-ab4a-42c2-9edb-8e8cf3c9d074",
|
||||
"name": "Incorrect pairedItem info",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "aea58e9e-5a00-4a86-a0bc-b077a07cd1f4",
|
||||
"name": "Sticky Note5",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "If the pinned node is executed, make grey and use text:\n[For preview, unpin node ‘<node_name>’ and execute]",
|
||||
"height": 255,
|
||||
"width": 237.63786881219818
|
||||
},
|
||||
"id": "3fdf6bdc-8065-421b-9ecf-6453946356a4",
|
||||
"name": "Sticky Note6",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1840]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": [1, 2, 3, 4]\n }\n];"
|
||||
},
|
||||
"id": "f8de7b0a-79c1-4b7a-a183-feb94f2f8625",
|
||||
"name": "Multiple matching items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 2200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "601c050a-7909-4708-be8d-4de248b68392",
|
||||
"name": "Sticky Note7",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 2100]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This should be grey, with text\n\n[For preview, unpin node ‘<node_name>’ and execute]",
|
||||
"height": 291.70186796527776,
|
||||
"width": 177
|
||||
},
|
||||
"id": "dfdcfaf4-a76b-4307-97a6-3fd7772e9fa8",
|
||||
"name": "Sticky Note8",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 2360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": [1, 2, 3, 4]\n }\n];"
|
||||
},
|
||||
"id": "8f2a9642-68e7-4dc6-a6c2-2018919327a3",
|
||||
"name": "Multiple matching items, pinned",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 2500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "If the pinned node isn't executed (e.g. if you execute one of the other code nodes in the same column), the expression is green!",
|
||||
"height": 128.93706220621976,
|
||||
"width": 177
|
||||
},
|
||||
"id": "65cf9b4c-a96d-46f5-b9bb-f6d88d1fbc44",
|
||||
"name": "Sticky Note9",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2220, 1940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": 99\n }\n];"
|
||||
},
|
||||
"id": "0bdfe0d2-7de2-472d-bc0a-2d0eff0e08c7",
|
||||
"name": "Incorrect pairedItem info, pinned1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"field\": \"the same\"\n }\n];"
|
||||
},
|
||||
"id": "b080a98e-d983-414a-b925-bdfc7ab2c3b6",
|
||||
"name": "Break pairedItem chain, pinned",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This should be grey, with text\n\n[For preview, unpin node ‘<node_name>’ and execute]",
|
||||
"height": 291.70186796527776,
|
||||
"width": 177
|
||||
},
|
||||
"id": "ce083193-1944-4c6c-925d-9e23c5194d98",
|
||||
"name": "Sticky Note11",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1280]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "We should also change the output pane error on execution in this case.\n\nERROR: No path back to '<node_name>' node\nDescription: Please make sure it is connected to this node (there can be other nodes in between)",
|
||||
"height": 209,
|
||||
"width": 301.59467203049536
|
||||
},
|
||||
"id": "755e07f0-3f18-4b08-ad30-79221a76507a",
|
||||
"name": "Sticky Note10",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1080, 1360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "1fff886f-3d13-4fbf-b0fb-7e2f845937c0",
|
||||
"leftValue": "={{ false }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "56dd65f0-d67a-42ce-a876-77434f621dc3",
|
||||
"name": "Impossible if",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [1000, 2000]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "test",
|
||||
"stringValue": "xzy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "11eadfc8-d14d-407c-b6d5-6e59b2e427a1",
|
||||
"name": "Impossible",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1180, 1980]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Should be an error when using .item:\n\n[No path back to node]",
|
||||
"height": 237.7232010163043,
|
||||
"width": 150
|
||||
},
|
||||
"id": "c3a3fdc2-66fa-4562-a359-45bdece2f625",
|
||||
"name": "Sticky Note12",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1400, 1880]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "name",
|
||||
"stringValue": "={{ $('Impossible').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "4cbbee96-dd4c-4625-95b9-c68faef3e9a8",
|
||||
"name": "Reference impossible with .item",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1420, 2000]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "name",
|
||||
"stringValue": "={{ $('Impossible').first().json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "6d47bd08-810a-4ade-be57-635adc1df47f",
|
||||
"name": "Reference impossible with .first()",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1420, 2320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "When using .first(), .last() or .all() and the node isn't executed, show grey warning:\n\n[Execute ‘<node_name>’ for preview]",
|
||||
"height": 330.27573762439613,
|
||||
"width": 229.78666948973432
|
||||
},
|
||||
"id": "1fcf2562-0789-41ad-8c92-44bcdd5d44e6",
|
||||
"name": "Sticky Note13",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1400, 2180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('non existent') }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "327d7f7b-61a5-4d60-9542-d61f84e7c83a",
|
||||
"name": "Reference non-existent node",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1000, 2320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Customer Datastore (n8n training)').item.json.email }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "38e3a736-4e13-4c23-af16-e50e605c4fb5",
|
||||
"name": "NoPathBack",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1040, 1184]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $json.email }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "2a7eaf81-6d64-488d-baf6-cc2f962908af",
|
||||
"name": "NoInputConnection",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1220, 1180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "166ee813-1db8-43a6-ace4-990c41dfeaea",
|
||||
"name": "PairedItemInfoMissing",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1120]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "a2dca54c-03ef-4a16-bf29-71eb0012cf0b",
|
||||
"name": "PairedItemInfoMissingPinned",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "0a1f566b-8dcf-4e28-81c4-faeadcdc02fb",
|
||||
"name": "IncorrectPairedItem",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.to }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "4d76b75f-5896-48ba-bb2f-8a2574ec1b8b",
|
||||
"name": "IncorrectPairedItemPinned",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "c4636b5c-c13a-441b-a59c-23962b2757b3",
|
||||
"name": "PairedItemMultipleMatches2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 2200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "6d687cf8-5309-4d44-aab3-aa023a42fa27",
|
||||
"name": "PairedItemMultipleMatches",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "d87a7aa4-b4c7-4fad-897d-a7ce0657bef3",
|
||||
"name": "IncorrectPairedItemPinned2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 2500]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Multiple matching items, pinned": [
|
||||
{
|
||||
"json": {
|
||||
"field": "the same"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Incorrect pairedItem info, pinned1": [
|
||||
{
|
||||
"json": {
|
||||
"field": "the same"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Break pairedItem chain, pinned": [
|
||||
{
|
||||
"json": {
|
||||
"field": "the same"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Test workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Customer Datastore (n8n training)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Impossible if",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Reference non-existent node",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Customer Datastore (n8n training)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Reference impossible with .item",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Reference impossible with .first()",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Aggregate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Break pairedItem chain",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Incorrect pairedItem info",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Multiple matching items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Incorrect pairedItem info, pinned1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Multiple matching items, pinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Break pairedItem chain, pinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Aggregate": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemMultipleMatches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Break pairedItem chain": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemInfoMissing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Incorrect pairedItem info": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IncorrectPairedItem",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Multiple matching items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemMultipleMatches2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Multiple matching items, pinned": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IncorrectPairedItemPinned2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Incorrect pairedItem info, pinned1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IncorrectPairedItemPinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Break pairedItem chain, pinned": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemInfoMissingPinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Impossible if": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Impossible",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "f6276c80-c1d1-485b-9d07-894868bcd701",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "2e88d456a76a9edc44cbcda082bb44ddef9555356ef691b0c6a45099d5095a45"
|
||||
},
|
||||
"id": "BmXv9neCtTggKXuG",
|
||||
"tags": []
|
||||
}
|
||||
Reference in New Issue
Block a user