/* 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); }, }); } }