/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { DateTime, Duration, Interval, Settings } from 'luxon';
import * as jmespath from 'jmespath';
import type {
IDataObject,
IExecuteData,
INodeExecutionData,
INodeParameters,
IPairedItemData,
IRunExecutionData,
ISourceData,
ITaskData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
INodeParameterResourceLocator,
NodeParameterValueType,
WorkflowExecuteMode,
ProxyInput,
} from './Interfaces';
import * as NodeHelpers from './NodeHelpers';
import { ExpressionError } from './ExpressionError';
import type { Workflow } from './Workflow';
import { augmentArray, augmentObject } from './AugmentObject';
import { deepCopy } from './utils';
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
return Boolean(
typeof value === 'object' && value && 'mode' in value && 'value' in value && '__rl' in value,
);
}
const SCRIPTING_NODE_TYPES = [
'n8n-nodes-base.function',
'n8n-nodes-base.functionItem',
'n8n-nodes-base.code',
];
const isScriptingNode = (nodeName: string, workflow: Workflow) => {
const node = workflow.getNode(nodeName);
return node && SCRIPTING_NODE_TYPES.includes(node.type);
};
export class WorkflowDataProxy {
private workflow: Workflow;
private runExecutionData: IRunExecutionData | null;
private defaultReturnRunIndex: number;
private runIndex: number;
private itemIndex: number;
private activeNodeName: string;
private contextNodeName: string;
private connectionInputData: INodeExecutionData[];
private siblingParameters: INodeParameters;
private mode: WorkflowExecuteMode;
private selfData: IDataObject;
private additionalKeys: IWorkflowDataProxyAdditionalKeys;
private executeData: IExecuteData | undefined;
private defaultTimezone: string;
private timezone: string;
// TODO: Clean that up at some point and move all the options into an options object
constructor(
workflow: Workflow,
runExecutionData: IRunExecutionData | null,
runIndex: number,
itemIndex: number,
activeNodeName: string,
connectionInputData: INodeExecutionData[],
siblingParameters: INodeParameters,
mode: WorkflowExecuteMode,
defaultTimezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultReturnRunIndex = -1,
selfData = {},
contextNodeName?: string,
) {
this.activeNodeName = activeNodeName;
this.contextNodeName = contextNodeName || activeNodeName;
this.workflow = workflow;
this.runExecutionData = isScriptingNode(this.contextNodeName, workflow)
? runExecutionData !== null
? augmentObject(runExecutionData)
: null
: runExecutionData;
this.connectionInputData = isScriptingNode(this.contextNodeName, workflow)
? augmentArray(connectionInputData)
: connectionInputData;
this.defaultReturnRunIndex = defaultReturnRunIndex;
this.runIndex = runIndex;
this.itemIndex = itemIndex;
this.siblingParameters = siblingParameters;
this.mode = mode;
this.defaultTimezone = defaultTimezone;
this.timezone = workflow.settings?.timezone ?? defaultTimezone;
this.selfData = selfData;
this.additionalKeys = additionalKeys;
this.executeData = executeData;
Settings.defaultZone = this.timezone;
}
/**
* Returns a proxy which allows to query context data of a given node
*
* @private
* @param {string} nodeName The name of the node to get the context from
*/
private nodeContextGetter(nodeName: string) {
const that = this;
const node = this.workflow.nodes[nodeName];
if (!that.runExecutionData?.executionData && that.connectionInputData.length > 0) {
return {}; // incoming connection has pinned data, so stub context object
}
if (!that.runExecutionData?.executionData && !that.runExecutionData?.resultData) {
throw new ExpressionError(
"The workflow hasn't been executed yet, so you can't reference any context data",
{
runIndex: that.runIndex,
itemIndex: that.itemIndex,
},
);
}
return new Proxy(
{},
{
has: () => true,
ownKeys(target) {
if (Reflect.ownKeys(target).length === 0) {
// Target object did not get set yet
Object.assign(target, NodeHelpers.getContext(that.runExecutionData!, 'node', node));
}
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) {
if (name === 'isProxy') return true;
name = name.toString();
const contextData = NodeHelpers.getContext(that.runExecutionData!, 'node', node);
return contextData[name];
},
},
);
}
private selfGetter() {
const that = this;
return new Proxy(
{},
{
has: () => true,
ownKeys(target) {
return Reflect.ownKeys(target);
},
get(target, name, receiver) {
if (name === 'isProxy') return true;
name = name.toString();
return that.selfData[name];
},
},
);
}
/**
* Returns a proxy which allows to query parameter data of a given node
*
* @private
* @param {string} nodeName The name of the node to query data from
*/
private nodeParameterGetter(nodeName: string) {
const that = this;
const node = this.workflow.nodes[nodeName];
return new Proxy(node.parameters, {
has: () => true,
ownKeys(target) {
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) {
if (name === 'isProxy') return true;
if (name === 'toJSON') return () => deepCopy(target);
name = name.toString();
let returnValue: NodeParameterValueType;
if (name[0] === '&') {
const key = name.slice(1);
if (!that.siblingParameters.hasOwnProperty(key)) {
throw new Error(`Could not find sibling parameter "${key}" on node "${nodeName}"`);
}
returnValue = that.siblingParameters[key];
} else {
if (!node.parameters.hasOwnProperty(name)) {
// Parameter does not exist on node
return undefined;
}
returnValue = node.parameters[name];
}
// Avoid recursion
if (returnValue === `={{ $parameter.${name} }}`) return undefined;
if (isResourceLocatorValue(returnValue)) {
if (returnValue.__regex && typeof returnValue.value === 'string') {
const expr = new RegExp(returnValue.__regex);
const extracted = expr.exec(returnValue.value);
if (extracted && extracted.length >= 2) {
returnValue = extracted[1];
} else {
return returnValue.value;
}
} else {
returnValue = returnValue.value;
}
}
if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') {
// The found value is an expression so resolve it
return that.workflow.expression.getParameterValue(
returnValue,
that.runExecutionData,
that.runIndex,
that.itemIndex,
that.activeNodeName,
that.connectionInputData,
that.mode,
that.timezone,
that.additionalKeys,
that.executeData,
false,
{},
that.contextNodeName,
);
}
return returnValue;
},
});
}
/**
* Returns the node ExecutionData
*
* @private
* @param {string} nodeName The name of the node query data from
* @param {boolean} [shortSyntax=false] If short syntax got used
* @param {number} [outputIndex] The index of the output, if not given the first one gets used
* @param {number} [runIndex] The index of the run, if not given the current one does get used
*/
private getNodeExecutionData(
nodeName: string,
shortSyntax = false,
outputIndex?: number,
runIndex?: number,
): INodeExecutionData[] {
const that = this;
let executionData: INodeExecutionData[];
if (!shortSyntax) {
// Long syntax got used to return data from node in path
if (that.runExecutionData === null) {
throw new ExpressionError(
"The workflow hasn't been executed yet, so you can't reference any output data",
{
runIndex: that.runIndex,
itemIndex: that.itemIndex,
},
);
}
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,
});
}
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
runIndex = runIndex === undefined ? that.defaultReturnRunIndex : runIndex;
runIndex =
runIndex === -1 ? that.runExecutionData.resultData.runData[nodeName].length - 1 : runIndex;
if (that.runExecutionData.resultData.runData[nodeName].length <= runIndex) {
throw new ExpressionError(`Run ${runIndex} of node "${nodeName}" not found`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
const taskData = that.runExecutionData.resultData.runData[nodeName][runIndex].data!;
if (!taskData.main?.length || taskData.main[0] === null) {
// throw new Error(`No data found for item-index: "${itemIndex}"`);
throw new ExpressionError('No data found from "main" input.', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
// Check from which output to read the data.
// Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output)
if (outputIndex === undefined) {
const nodeConnection = that.workflow.getNodeConnectionIndexes(
that.contextNodeName,
nodeName,
'main',
);
if (nodeConnection === undefined) {
throw new ExpressionError(`connect "${that.contextNodeName}" to "${nodeName}"`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
outputIndex = nodeConnection.sourceIndex;
}
if (outputIndex === undefined) {
outputIndex = 0;
}
if (taskData.main.length <= outputIndex) {
throw new ExpressionError(`Node "${nodeName}" has no branch with index ${outputIndex}.`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
executionData = taskData.main[outputIndex] as INodeExecutionData[];
} else {
// Short syntax got used to return data from active node
executionData = that.connectionInputData;
}
return executionData;
}
/**
* Returns a proxy which allows to query data of a given node
*
* @private
* @param {string} nodeName The name of the node query data from
* @param {boolean} [shortSyntax=false] If short syntax got used
*/
private nodeDataGetter(nodeName: string, shortSyntax = false) {
const that = this;
const node = this.workflow.nodes[nodeName];
return new Proxy(
{ binary: undefined, data: undefined, json: undefined },
{
has: () => true,
get(target, name, receiver) {
if (name === 'isProxy') return true;
name = name.toString();
if (!node) {
throw new ExpressionError(`"${nodeName}" node doesn't exist`);
}
if (['binary', 'data', 'json'].includes(name)) {
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
if (executionData.length <= that.itemIndex) {
throw new ExpressionError(`No data found for item-index: "${that.itemIndex}"`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
if (['data', 'json'].includes(name)) {
// JSON-Data
return executionData[that.itemIndex].json;
}
if (name === 'binary') {
// Binary-Data
const returnData: IDataObject = {};
if (!executionData[that.itemIndex].binary) {
return returnData;
}
const binaryKeyData = executionData[that.itemIndex].binary!;
for (const keyName of Object.keys(binaryKeyData)) {
returnData[keyName] = {};
const binaryData = binaryKeyData[keyName];
for (const propertyName in binaryData) {
if (propertyName === 'data') {
// Skip the data property
continue;
}
(returnData[keyName] as IDataObject)[propertyName] = binaryData[propertyName];
}
}
return returnData;
}
} else if (name === 'context') {
return that.nodeContextGetter(nodeName);
} else if (name === 'parameter') {
// Get node parameter data
return that.nodeParameterGetter(nodeName);
} else if (name === 'runIndex') {
if (!that.runExecutionData?.resultData.runData[nodeName]) {
return -1;
}
return that.runExecutionData.resultData.runData[nodeName].length - 1;
}
return Reflect.get(target, name, receiver);
},
},
);
}
/**
* Returns a proxy to query data from the environment
*
* @private
*/
private envGetter() {
const that = this;
return new Proxy(
{},
{
has: () => true,
get(target, name, receiver) {
if (name === 'isProxy') return true;
if (typeof process === 'undefined') {
throw new ExpressionError('not accessible via UI, please run node', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
if (process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true') {
throw new ExpressionError('access to env vars denied', {
causeDetailed:
'If you need access please contact the administrator to remove the environment variable ‘N8N_BLOCK_ENV_ACCESS_IN_NODE‘',
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
return process.env[name.toString()];
},
},
);
}
private prevNodeGetter() {
const allowedValues = ['name', 'outputIndex', 'runIndex'];
const that = this;
return new Proxy(
{},
{
has: () => true,
ownKeys(target) {
return allowedValues;
},
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) {
if (name === 'isProxy') return true;
if (!that.executeData?.source) {
// Means the previous node did not get executed yet
return undefined;
}
const sourceData: ISourceData = that.executeData.source.main[0] as ISourceData;
if (name === 'name') {
return sourceData.previousNode;
}
if (name === 'outputIndex') {
return sourceData.previousNodeOutput || 0;
}
if (name === 'runIndex') {
return sourceData.previousNodeRun || 0;
}
return Reflect.get(target, name, receiver);
},
},
);
}
/**
* Returns a proxy to query data from the workflow
*
* @private
*/
private workflowGetter() {
const allowedValues = ['active', 'id', 'name'];
const that = this;
return new Proxy(
{},
{
has: () => true,
ownKeys(target) {
return allowedValues;
},
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) {
if (name === 'isProxy') return true;
if (allowedValues.includes(name.toString())) {
const value = that.workflow[name as keyof typeof target];
if (value === undefined && name === 'id') {
throw new ExpressionError('save workflow to view', {
description: 'Please save the workflow first to use $workflow',
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
return value;
}
return Reflect.get(target, name, receiver);
},
},
);
}
/**
* Returns a proxy to query data of all nodes
*
* @private
*/
private nodeGetter() {
const that = this;
return new Proxy(
{},
{
has: () => true,
get(target, name, receiver) {
if (name === 'isProxy') return true;
const nodeName = name.toString();
if (that.workflow.getNode(nodeName) === null) {
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
return that.nodeDataGetter(nodeName);
},
},
);
}
/**
* Returns the data proxy object which allows to query data from current run
*
*/
getDataProxy(): IWorkflowDataProxyData {
const that = this;
const getNodeOutput = (nodeName?: string, branchIndex?: number, runIndex?: number) => {
let executionData: INodeExecutionData[];
if (nodeName === undefined) {
executionData = that.connectionInputData;
} else {
branchIndex = branchIndex || 0;
runIndex = runIndex === undefined ? -1 : runIndex;
executionData = that.getNodeExecutionData(nodeName, false, branchIndex, runIndex);
}
return executionData;
};
// replacing proxies with the actual data.
const jmespathWrapper = (data: IDataObject | IDataObject[], query: string) => {
if (typeof data !== 'object' || typeof query !== 'string') {
throw new ExpressionError('expected two arguments (Object, string) for this function', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
}
if (!Array.isArray(data) && typeof data === 'object') {
return jmespath.search({ ...data }, query);
}
return jmespath.search(data, query);
};
const createExpressionError = (
message: string,
context?: {
causeDetailed?: string;
description?: string;
descriptionTemplate?: string;
functionality?: 'pairedItem';
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) {
// If the node in which the error is thrown is a function node,
// display a different error message in case there is one defined
message = context.functionOverrides.message || message;
context.description = context.functionOverrides.description || context.description;
// The error will be in the code and not on an expression on a parameter
// so remove the messageTemplate as it would overwrite the message
context.messageTemplate = undefined;
}
if (context?.nodeCause) {
const nodeName = context.nodeCause;
const pinData = this.workflow.getPinDataOfNode(nodeName);
if (pinData) {
if (!context) {
context = {};
}
message = `‘Node ${nodeName}‘ must be unpinned to execute`;
context.messageTemplate = undefined;
context.description = `To fetch the data for the expression, you must unpin the node '${nodeName}' and execute the workflow again.`;
context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node '${nodeName}' and execute the workflow again.`;
}
if (context.moreInfoLink && (pinData || isScriptingNode(nodeName, that.workflow))) {
const moreInfoLink =
' More info';
context.description += moreInfoLink;
if (context.descriptionTemplate) context.descriptionTemplate += moreInfoLink;
}
}
return new ExpressionError(message, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
...context,
});
};
const getPairedItem = (
destinationNodeName: string,
incomingSourceData: ISourceData | null,
pairedItem: IPairedItemData,
): INodeExecutionData | null => {
let taskData: ITaskData;
let sourceData: ISourceData | null = incomingSourceData;
if (pairedItem.sourceOverwrite) {
sourceData = pairedItem.sourceOverwrite;
}
if (typeof pairedItem === 'number') {
pairedItem = {
item: pairedItem,
};
}
let currentPairedItem = pairedItem;
let nodeBeforeLast: string | undefined;
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
taskData =
that.runExecutionData!.resultData.runData[sourceData.previousNode][
sourceData?.previousNodeRun || 0
];
const previousNodeOutput = sourceData.previousNodeOutput || 0;
if (previousNodeOutput >= taskData.data!.main.length) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionOverrides: {
message: 'Can’t get data',
},
nodeCause: nodeBeforeLast,
description: 'Apologies, this is an internal error. See details for more information',
causeDetailed: 'Referencing a non-existent output on a node, problem with source data',
type: 'internal',
});
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
nodeCause: nodeBeforeLast,
description: `In node ‘${nodeBeforeLast!}’, output item ${
currentPairedItem.item || 0
} ${
sourceData.previousNodeRun
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
: ''
}points to an input item on node ‘${
sourceData.previousNode
}‘ that doesn’t exist.`,
type: 'invalid pairing info',
moreInfoLink: true,
});
}
const itemPreviousNode: INodeExecutionData =
taskData.data!.main[previousNodeOutput]![pairedItem.item];
if (itemPreviousNode.pairedItem === undefined) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
nodeCause: sourceData.previousNode,
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘${sourceData.previousNode}’`,
causeDetailed: `Missing pairedItem data (node ‘${sourceData.previousNode}’ probably didn’t supply it)`,
type: 'no pairing info',
moreInfoLink: true,
});
}
if (Array.isArray(itemPreviousNode.pairedItem)) {
// Item is based on multiple items so check all of them
const results = itemPreviousNode.pairedItem
// eslint-disable-next-line @typescript-eslint/no-loop-func
.map((item) => {
try {
const itemInput = item.input || 0;
if (itemInput >= taskData.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 Error('Not found');
}
return getPairedItem(destinationNodeName, taskData.source[itemInput], item);
} catch (error) {
// Means pairedItem could not be found
return null;
}
})
.filter((result) => result !== null);
if (results.length !== 1) {
// Check if the results are all the same
const firstResult = results[0];
if (results.every((result) => result === firstResult)) {
// All results are the same so return the first one
return firstResult;
}
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
functionality: 'pairedItem',
functionOverrides: {
description: `The code uses data in the node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
message: 'Invalid code',
},
description: `The expression uses data in the node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
type: 'multiple matches',
});
}
return results[0];
}
currentPairedItem = pairedItem;
// pairedItem is not an array
if (typeof itemPreviousNode.pairedItem === 'number') {
pairedItem = {
item: itemPreviousNode.pairedItem,
};
} else {
pairedItem = itemPreviousNode.pairedItem;
}
const itemInput = pairedItem.input || 0;
if (itemInput >= taskData.source.length) {
if (taskData.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%%’',
functionality: 'pairedItem',
functionOverrides: {
description: `The code uses data in the node ‘${destinationNodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
message: 'Invalid code',
},
description: `The expression uses data in the node ‘${destinationNodeName}’ 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',
moreInfoLink: true,
});
}
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
nodeCause: nodeBeforeLast,
description: `In node ‘${sourceData.previousNode}’, output item ${
currentPairedItem.item || 0
} of ${
sourceData.previousNodeRun
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
: ''
}points to a branch that doesn’t exist.`,
type: 'invalid pairing info',
});
}
nodeBeforeLast = sourceData.previousNode;
sourceData = taskData.source[pairedItem.input || 0] || null;
if (pairedItem.sourceOverwrite) {
sourceData = pairedItem.sourceOverwrite;
}
}
if (sourceData === null) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
nodeCause: nodeBeforeLast,
description: 'Could not resolve, probably no pairedItem exists',
type: 'no pairing info',
moreInfoLink: true,
});
}
taskData =
that.runExecutionData!.resultData.runData[sourceData.previousNode][
sourceData?.previousNodeRun || 0
];
const previousNodeOutput = sourceData.previousNodeOutput || 0;
if (previousNodeOutput >= taskData.data!.main.length) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
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',
});
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
nodeCause: nodeBeforeLast,
description: `In node ‘${nodeBeforeLast!}’, output item ${
currentPairedItem.item || 0
} ${
sourceData.previousNodeRun
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
: ''
}points to an input item on node ‘${
sourceData.previousNode
}‘ that doesn’t exist.`,
type: 'invalid pairing info',
moreInfoLink: true,
});
}
return taskData.data!.main[previousNodeOutput]![pairedItem.item];
};
const base = {
$: (nodeName: string) => {
if (!nodeName) {
throw createExpressionError('When calling $(), please specify a node');
}
const referencedNode = that.workflow.getNode(nodeName);
if (referencedNode === null) {
throw createExpressionError(`"${nodeName}" node doesn't exist`);
}
if (!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName)) {
throw createExpressionError(`no data, execute "${nodeName}" node first`);
}
return new Proxy(
{},
{
has: () => true,
ownKeys(target) {
return [
'pairedItem',
'itemMatching',
'item',
'first',
'last',
'all',
'context',
'params',
];
},
get(target, property, receiver) {
if (property === 'isProxy') return true;
if (['pairedItem', 'itemMatching', 'item'].includes(property as string)) {
const pairedItemMethod = (itemIndex?: number) => {
if (itemIndex === undefined) {
if (property === 'itemMatching') {
throw createExpressionError('Missing item index for .itemMatching()', {
itemIndex,
});
}
itemIndex = that.itemIndex;
}
const executionData = that.connectionInputData;
// 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;
if (pairedItem === undefined) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
description: `To fetch the data from other nodes that this code needs, more information is needed from the node ‘${that.activeNodeName}‘`,
message: 'Can’t get data',
},
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘${that.activeNodeName}‘`,
causeDetailed: `Missing pairedItem data (node ‘${that.activeNodeName}‘ probably didn’t supply it)`,
itemIndex,
});
}
if (!that.executeData?.source) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Can’t get data',
},
description:
'Apologies, this is an internal error. See details for more information',
causeDetailed: 'Missing sourceData (probably an internal error)',
itemIndex,
});
}
// Before resolving the pairedItem make sure that the requested node comes in the
// graph before the current one
const parentNodes = that.workflow.getParentNodes(that.contextNodeName);
if (!parentNodes.includes(nodeName)) {
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
functionality: 'pairedItem',
functionOverrides: {
description: `The code uses data in the node ‘${nodeName}’ 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 ‘${nodeName}’ 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];
return getPairedItem(nodeName, sourceData, pairedItem);
};
if (property === 'item') {
return pairedItemMethod();
}
return pairedItemMethod;
}
if (property === 'first') {
return (branchIndex?: number, runIndex?: number) => {
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
if (executionData[0]) return executionData[0];
return undefined;
};
}
if (property === 'last') {
return (branchIndex?: number, runIndex?: number) => {
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
if (!executionData.length) return undefined;
if (executionData[executionData.length - 1]) {
return executionData[executionData.length - 1];
}
return undefined;
};
}
if (property === 'all') {
return (branchIndex?: number, runIndex?: number) =>
getNodeOutput(nodeName, branchIndex, runIndex);
}
if (property === 'context') {
return that.nodeContextGetter(nodeName);
}
if (property === 'params') {
return that.workflow.getNode(nodeName)?.parameters;
}
return Reflect.get(target, property, receiver);
},
},
);
},
$input: new Proxy({} as ProxyInput, {
has: () => true,
ownKeys(target) {
return ['all', 'context', 'first', 'item', 'last', 'params'];
},
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, property, receiver) {
if (property === 'isProxy') return true;
if (property === 'item') {
return that.connectionInputData[that.itemIndex];
}
if (property === 'first') {
return (...args: unknown[]) => {
if (args.length) {
throw createExpressionError('$input.first() should have no arguments');
}
const result = that.connectionInputData;
if (result[0]) {
return result[0];
}
return undefined;
};
}
if (property === 'last') {
return (...args: unknown[]) => {
if (args.length) {
throw createExpressionError('$input.last() should have no arguments');
}
const result = that.connectionInputData;
if (result.length && result[result.length - 1]) {
return result[result.length - 1];
}
return undefined;
};
}
if (property === 'all') {
return () => {
const result = that.connectionInputData;
if (result.length) {
return result;
}
return [];
};
}
if (['context', 'params'].includes(property as string)) {
// For the following properties we need the source data so fail in case it is missing
// for some reason (even though that should actually never happen)
if (!that.executeData?.source) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionOverrides: {
message: 'Can’t get data',
},
description:
'Apologies, this is an internal error. See details for more information',
causeDetailed: 'Missing sourceData (probably an internal error)',
runIndex: that.runIndex,
});
}
const sourceData: ISourceData = that.executeData.source.main[0] as ISourceData;
if (property === 'context') {
return that.nodeContextGetter(sourceData.previousNode);
}
if (property === 'params') {
return that.workflow.getNode(sourceData.previousNode)?.parameters;
}
}
return Reflect.get(target, property, receiver);
},
}),
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
$evaluateExpression: (expression: string, itemIndex?: number) => {
itemIndex = itemIndex || that.itemIndex;
return that.workflow.expression.getParameterValue(
`=${expression}`,
that.runExecutionData,
that.runIndex,
itemIndex,
that.activeNodeName,
that.connectionInputData,
that.mode,
that.timezone,
that.additionalKeys,
that.executeData,
false,
{},
that.contextNodeName,
);
},
$item: (itemIndex: number, runIndex?: number) => {
const defaultReturnRunIndex = runIndex === undefined ? -1 : runIndex;
const dataProxy = new WorkflowDataProxy(
this.workflow,
this.runExecutionData,
this.runIndex,
itemIndex,
this.activeNodeName,
this.connectionInputData,
that.siblingParameters,
that.mode,
that.defaultTimezone,
that.additionalKeys,
that.executeData,
defaultReturnRunIndex,
that.contextNodeName,
);
return dataProxy.getDataProxy();
},
$items: (nodeName?: string, outputIndex?: number, runIndex?: number) => {
if (nodeName === undefined) {
nodeName = (that.prevNodeGetter() as { name: string }).name;
const node = this.workflow.nodes[nodeName];
let result = that.connectionInputData;
if (node.executeOnce === true) {
result = result.slice(0, 1);
}
if (result.length) {
return result;
}
return [];
}
outputIndex = outputIndex || 0;
runIndex = runIndex === undefined ? -1 : runIndex;
return that.getNodeExecutionData(nodeName, false, outputIndex, runIndex);
},
$json: {}, // Placeholder
$node: this.nodeGetter(),
$self: this.selfGetter(),
$parameter: this.nodeParameterGetter(this.activeNodeName),
$prevNode: this.prevNodeGetter(),
$runIndex: this.runIndex,
$mode: this.mode,
$workflow: this.workflowGetter(),
$itemIndex: this.itemIndex,
$now: DateTime.now(),
$today: DateTime.now().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }),
$jmesPath: jmespathWrapper,
// eslint-disable-next-line @typescript-eslint/naming-convention
DateTime,
// eslint-disable-next-line @typescript-eslint/naming-convention
Interval,
// eslint-disable-next-line @typescript-eslint/naming-convention
Duration,
...that.additionalKeys,
// deprecated
$jmespath: jmespathWrapper,
$position: this.itemIndex,
$thisItem: that.connectionInputData[that.itemIndex],
$thisItemIndex: this.itemIndex,
$thisRunIndex: this.runIndex,
};
return new Proxy(base, {
has: () => true,
get(target, name, receiver) {
if (name === 'isProxy') return true;
if (['$data', '$json'].includes(name as string)) {
return that.nodeDataGetter(that.contextNodeName, true)?.json;
}
if (name === '$binary') {
return that.nodeDataGetter(that.contextNodeName, true)?.binary;
}
return Reflect.get(target, name, receiver);
},
});
}
}