mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 22:19:41 -08:00
ec141416e2
https://linear.app/n8n/issue/PAY-630 Minimal repro: https://internal.users.n8n.cloud/workflow/9HSkIy4T1LqXbm1H
1279 lines
39 KiB
TypeScript
1279 lines
39 KiB
TypeScript
/* 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 <strong>'${nodeName}'</strong> and execute the workflow again.`;
|
||
context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node <strong>'${nodeName}'</strong> and execute the workflow again.`;
|
||
}
|
||
|
||
if (context.moreInfoLink && (pinData || isScriptingNode(nodeName, that.workflow))) {
|
||
const moreInfoLink =
|
||
' <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/">More info</a>';
|
||
|
||
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 ‘<strong>${nodeBeforeLast!}</strong>’, output item ${
|
||
currentPairedItem.item || 0
|
||
} ${
|
||
sourceData.previousNodeRun
|
||
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
|
||
: ''
|
||
}points to an input item on node ‘<strong>${
|
||
sourceData.previousNode
|
||
}</strong>‘ 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 ‘<strong>${sourceData.previousNode}</strong>’`,
|
||
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 ‘<strong>${destinationNodeName}</strong>’ but there is more than one matching item in that node`,
|
||
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',
|
||
});
|
||
}
|
||
|
||
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 ‘<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).`,
|
||
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',
|
||
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 ‘<strong>${sourceData.previousNode}</strong>’, 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 ‘<strong>${nodeBeforeLast!}</strong>’, output item ${
|
||
currentPairedItem.item || 0
|
||
} ${
|
||
sourceData.previousNodeRun
|
||
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
|
||
: ''
|
||
}points to an input item on node ‘<strong>${
|
||
sourceData.previousNode
|
||
}</strong>‘ 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 ‘<strong>${that.activeNodeName}</strong>‘`,
|
||
message: 'Can’t get data',
|
||
},
|
||
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘<strong>${that.activeNodeName}</strong>‘`,
|
||
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 <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];
|
||
|
||
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);
|
||
},
|
||
});
|
||
}
|
||
}
|