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:
Elias Meire
2024-02-27 10:29:16 +01:00
committed by GitHub
parent 737170893d
commit f9a99ec029
29 changed files with 2818 additions and 558 deletions

View File

@@ -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 };

View File

@@ -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('Cant get data for expression', {
messageTemplate: 'Cant 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('Cant get data for expression', {
messageTemplate: 'Cant 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 doesnt 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('Cant 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 didnt 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 doesnt 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 doesnt 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('Cant get data for expression', {
messageTemplate: 'Cant 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: 'Cant 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('Cant 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];
}

View File

@@ -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) {

View File

@@ -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);
}
}
});
}

View File

@@ -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 },

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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('Cant 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('Cant get data for expression');
expect(exprError.context.type).toEqual('paired_item_invalid_info');
done();
}
});
});
});

View 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"
}

View 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
}
]
]
}
}
}

File diff suppressed because it is too large Load Diff

View 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": []
}