/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import * as jmespath from 'jmespath';
import { DateTime, Duration, Interval, Settings } from 'luxon';
import { augmentArray, augmentObject } from './augment-object';
import { AGENT_LANGCHAIN_NODE_TYPE, SCRIPTING_NODE_TYPES } from './constants';
import { ApplicationError } from '@n8n/errors';
import { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error';
import { getGlobalState } from './global-state';
import { NodeConnectionTypes } from './interfaces';
import type {
IDataObject,
IExecuteData,
INodeExecutionData,
INodeParameters,
IPairedItemData,
IRunExecutionData,
ISourceData,
ITaskData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
NodeParameterValueType,
WorkflowExecuteMode,
ProxyInput,
INode,
} from './interfaces';
import * as NodeHelpers from './node-helpers';
import { createResultError, createResultOk } from './result';
import { isResourceLocatorValue } from './type-guards';
import { deepCopy, isObjectEmpty } from './utils';
import type { Workflow } from './workflow';
import type { EnvProviderState } from './workflow-data-proxy-env-provider';
import { createEnvProvider, createEnvProviderState } from './workflow-data-proxy-env-provider';
import { getPinDataIfManualExecution } from './workflow-data-proxy-helpers';
const isScriptingNode = (nodeName: string, workflow: Workflow) => {
const node = workflow.getNode(nodeName);
return node && SCRIPTING_NODE_TYPES.includes(node.type);
};
const PAIRED_ITEM_METHOD = {
PAIRED_ITEM: 'pairedItem',
ITEM_MATCHING: 'itemMatching',
ITEM: 'item',
$GET_PAIRED_ITEM: '$getPairedItem',
} as const;
type PairedItemMethod = (typeof PAIRED_ITEM_METHOD)[keyof typeof PAIRED_ITEM_METHOD];
export class WorkflowDataProxy {
private runExecutionData: IRunExecutionData | null;
private connectionInputData: INodeExecutionData[];
private timezone: string;
// TODO: Clean that up at some point and move all the options into an options object
constructor(
private workflow: Workflow,
runExecutionData: IRunExecutionData | null,
private runIndex: number,
private itemIndex: number,
private activeNodeName: string,
connectionInputData: INodeExecutionData[],
private siblingParameters: INodeParameters,
private mode: WorkflowExecuteMode,
private additionalKeys: IWorkflowDataProxyAdditionalKeys,
private executeData?: IExecuteData,
private defaultReturnRunIndex = -1,
private selfData: IDataObject = {},
private contextNodeName: string = activeNodeName,
private envProviderState?: EnvProviderState,
) {
this.runExecutionData = isScriptingNode(this.contextNodeName, workflow)
? runExecutionData !== null
? augmentObject(runExecutionData)
: null
: runExecutionData;
this.connectionInputData = isScriptingNode(this.contextNodeName, workflow)
? augmentArray(connectionInputData)
: connectionInputData;
this.timezone = workflow.settings?.timezone ?? getGlobalState().defaultTimezone;
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,
type: 'no_execution_data',
},
);
}
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() {
return {
enumerable: true,
configurable: true,
};
},
get(_, name) {
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(_, name) {
if (name === 'isProxy') return true;
name = name.toString();
return that.selfData[name];
},
},
);
}
private buildAgentToolInfo(node: INode) {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
const type = nodeType.description.displayName;
const params = NodeHelpers.getNodeParameters(
nodeType.description.properties,
node.parameters,
true,
false,
node,
nodeType.description,
);
const resourceKey = params?.resource;
const operationKey = params?.operation;
const resource =
nodeType.description.properties
.find((nodeProperties) => nodeProperties.name === 'resource')
?.options?.find((option) => 'value' in option && option.value === resourceKey)?.name ??
null;
const operation =
nodeType.description.properties
.find(
(nodeProperty) =>
nodeProperty.name === 'operation' &&
nodeProperty.displayOptions?.show?.resource?.some((y) => y === resourceKey),
)
?.options?.find((y) => 'value' in y && y.value === operationKey)?.name ?? null;
const hasCredentials = !isObjectEmpty(node.credentials ?? {});
const hasValidCalendar = nodeType.description.name.includes('googleCalendar')
? isResourceLocatorValue(node.parameters.calendar) && node.parameters.calendar.value !== ''
: undefined;
const aiDefinedFields = Object.entries(node.parameters)
.map(([key, value]) => [key, isResourceLocatorValue(value) ? value.value : value] as const)
.filter(([_, value]) => value?.toString().toLowerCase().includes('$fromai'))
.map(
([key]) =>
nodeType.description.properties.find((property) => property.name === key)?.displayName,
);
return {
name: node.name,
type,
resource,
operation,
hasCredentials,
hasValidCalendar,
aiDefinedFields,
};
}
private agentInfo() {
const agentNode = this.workflow.getNode(this.activeNodeName);
if (!agentNode || agentNode.type !== AGENT_LANGCHAIN_NODE_TYPE) return undefined;
const connectedTools = this.workflow
.getParentNodes(this.activeNodeName, NodeConnectionTypes.AiTool)
.map((nodeName) => this.workflow.getNode(nodeName))
.filter((node) => node) as INode[];
const memoryConnectedToAgent =
this.workflow.getParentNodes(this.activeNodeName, NodeConnectionTypes.AiMemory).length > 0;
const allTools = this.workflow.queryNodes((nodeType) => {
return nodeType.description.name.toLowerCase().includes('tool');
});
const unconnectedTools = allTools
.filter(
(node) =>
this.workflow.getChildNodes(node.name, NodeConnectionTypes.AiTool, 1).length === 0,
)
.filter((node) => !connectedTools.includes(node));
return {
memoryConnectedToAgent,
tools: [
...connectedTools.map((node) => ({ connected: true, ...this.buildAgentToolInfo(node) })),
...unconnectedTools.map((node) => ({ connected: false, ...this.buildAgentToolInfo(node) })),
],
};
}
/**
* 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
* @param {boolean} [resolveValue=true] If the expression value should get resolved
*/
private nodeParameterGetter(nodeName: string, resolveValue = true) {
const that = this;
const node = this.workflow.nodes[nodeName];
// `node` is `undefined` only in expressions in credentials
return new Proxy(node?.parameters ?? {}, {
has: () => true,
ownKeys(target) {
return Reflect.ownKeys(target);
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
get(target, name) {
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 ApplicationError('Could not find sibling parameter on node', {
extra: { nodeName, parameter: key },
});
}
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 (resolveValue && 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.additionalKeys,
that.executeData,
false,
{},
that.contextNodeName,
);
}
return returnValue;
},
});
}
private getNodeExecutionOrPinnedData({
nodeName,
branchIndex,
runIndex,
shortSyntax = false,
}: {
nodeName: string;
branchIndex?: number;
runIndex?: number;
shortSyntax?: boolean;
}) {
try {
return this.getNodeExecutionData(nodeName, shortSyntax, branchIndex, runIndex);
} catch (e) {
const pinData = getPinDataIfManualExecution(this.workflow, nodeName, this.mode);
if (pinData) {
return pinData;
}
throw e;
}
}
/**
* 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.workflow.getNode(nodeName)) {
throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
if (
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
) {
throw new ExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_node_execution_data',
descriptionKey: 'noNodeExecutionData',
nodeCause: nodeName,
});
}
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 ApplicationError('No data found for item-index', { extra: { 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,
NodeConnectionTypes.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
* @param {boolean} [throwOnMissingExecutionData=true] If an error should get thrown if no execution data is available
*/
private nodeDataGetter(
nodeName: string,
shortSyntax = false,
throwOnMissingExecutionData = true,
) {
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("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
if (['binary', 'data', 'json'].includes(name)) {
const executionData = that.getNodeExecutionOrPinnedData({
nodeName,
shortSyntax,
});
if (executionData.length === 0 && !throwOnMissingExecutionData) {
return undefined;
}
if (executionData.length === 0) {
if (that.workflow.getParentNodes(nodeName).length === 0) {
throw new ExpressionError('No execution data available', {
messageTemplate:
'No execution data available to expression under ‘%%PARAMETER%%’',
descriptionKey: 'noInputConnection',
nodeCause: nodeName,
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_input_connection',
});
}
throw new ExpressionError('No execution data available', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_execution_data',
});
}
if (executionData.length <= that.itemIndex) {
throw new ExpressionError(`No data found for item-index: "${that.itemIndex}"`, {
runIndex: that.runIndex,
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);
},
},
);
}
private prevNodeGetter() {
const allowedValues = ['name', 'outputIndex', 'runIndex'];
const that = this;
return new Proxy(
{},
{
has: () => true,
ownKeys() {
return allowedValues;
},
getOwnPropertyDescriptor() {
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() {
return allowedValues;
},
getOwnPropertyDescriptor() {
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(_, name) {
if (name === 'isProxy') return true;
const nodeName = name.toString();
if (that.workflow.getNode(nodeName) === null) {
throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
return that.nodeDataGetter(nodeName);
},
},
);
}
/**
* Returns the data proxy object which allows to query data from current run
*
*/
getDataProxy(opts?: { throwOnMissingExecutionData: boolean }): IWorkflowDataProxyData {
const that = this;
// 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?: ExpressionErrorOptions & {
moreInfoLink?: boolean;
functionOverrides?: {
// Custom data to display for Function-Nodes
message?: string;
description?: 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 = getPinDataIfManualExecution(that.workflow, nodeName, that.mode);
if (pinData) {
if (!context) {
context = {};
}
message = `Unpin '${nodeName}' to execute`;
context.messageTemplate = undefined;
context.descriptionKey = 'pairedItemPinned';
}
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 createInvalidPairedItemError = ({ nodeName }: { nodeName: string }) => {
return createExpressionError("Can't get data for expression", {
messageTemplate: 'Expression info invalid',
functionality: 'pairedItem',
functionOverrides: {
message: "Can't get data",
},
nodeCause: nodeName,
descriptionKey: 'pairedItemInvalidInfo',
type: 'paired_item_invalid_info',
});
};
const createMissingPairedItemError = (
nodeCause: string,
usedMethodName: PairedItemMethod = PAIRED_ITEM_METHOD.PAIRED_ITEM,
) => {
const pinData = getPinDataIfManualExecution(that.workflow, nodeCause, that.mode);
const message = pinData
? `Using the ${usedMethodName} method doesn't work with pinned data in this scenario. Please unpin '${nodeCause}' and try again.`
: `Paired item data for ${usedMethodName} from node '${nodeCause}' is unavailable. Ensure '${nodeCause}' is providing the required output.`;
return new ExpressionError(message, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
functionality: 'pairedItem',
descriptionKey: isScriptingNode(nodeCause, that.workflow)
? 'pairedItemNoInfoCodeNode'
: 'pairedItemNoInfo',
nodeCause,
causeDetailed: `Missing pairedItem data (node '${nodeCause}' probably didn't supply it)`,
type: 'paired_item_no_info',
});
};
const createNoConnectionError = (nodeCause: string) => {
return createExpressionError('Invalid expression', {
messageTemplate: 'No path back to referenced node',
functionality: 'pairedItem',
descriptionKey: isScriptingNode(nodeCause, that.workflow)
? 'pairedItemNoConnectionCodeNode'
: 'pairedItemNoConnection',
type: 'paired_item_no_connection',
moreInfoLink: true,
nodeCause,
});
};
function createBranchNotFoundError(node: string, item: number, cause?: string) {
return createExpressionError('Branch not found', {
messageTemplate: 'Paired item references non-existent branch',
functionality: 'pairedItem',
nodeCause: cause,
functionOverrides: { message: 'Invalid branch reference' },
description: `Item ${item} in node ${node} references a branch that doesn't exist.`,
type: 'paired_item_invalid_info',
});
}
function createPairedItemNotFound(destNode: string, cause?: string) {
return createExpressionError('Paired item resolution failed', {
messageTemplate: 'Unable to find paired item source',
functionality: 'pairedItem',
nodeCause: cause,
functionOverrides: { message: 'Data not found' },
description: `Could not trace back to node '${destNode}'`,
type: 'paired_item_no_info',
moreInfoLink: true,
});
}
function createPairedItemMultipleItemsFound(destNode: string, itemIndex: number) {
return createExpressionError('Multiple matches found', {
messageTemplate: `Multiple matching items for item [${itemIndex}]`,
functionality: 'pairedItem',
functionOverrides: { message: 'Multiple matches' },
nodeCause: destNode,
descriptionKey: isScriptingNode(destNode, that.workflow)
? 'pairedItemMultipleMatchesCodeNode'
: 'pairedItemMultipleMatches',
type: 'paired_item_multiple_matches',
});
}
function normalizeInputs(
pairedItem: IPairedItemData,
sourceData: ISourceData | null,
): [IPairedItemData, ISourceData | null] {
if (typeof pairedItem === 'number') {
pairedItem = { item: pairedItem };
}
const finalSource = pairedItem.sourceOverwrite || sourceData;
return [pairedItem, finalSource];
}
function pinDataToTask(pinData: INodeExecutionData[] | undefined): ITaskData | undefined {
if (!pinData) return undefined;
return {
data: { main: [pinData] },
startTime: 0,
executionTime: 0,
executionIndex: 0,
source: [],
};
}
function getTaskData(source: ISourceData): ITaskData | undefined {
return (
that.runExecutionData?.resultData?.runData?.[source.previousNode]?.[
source.previousNodeRun || 0
] ??
pinDataToTask(getPinDataIfManualExecution(that.workflow, source.previousNode, that.mode))
);
}
function getNodeOutput(
taskData: ITaskData | undefined,
source: ISourceData,
nodeCause?: string,
): INodeExecutionData[] {
const outputIndex = source.previousNodeOutput || 0;
const outputs = taskData?.data?.main?.[outputIndex];
if (!outputs) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Missing output data',
functionOverrides: { message: 'Missing output' },
nodeCause,
description: `Expected output #${outputIndex} from node ${source.previousNode}`,
type: 'internal',
});
}
return outputs;
}
const normalizePairedItem = (
paired: number | IPairedItemData | Array | null | undefined,
): IPairedItemData[] => {
if (paired === null || paired === undefined) {
return [];
}
const pairedItems = Array.isArray(paired) ? paired : [paired];
return pairedItems.map((p) => (typeof p === 'number' ? { item: p } : p));
};
const getPairedItem = (
destinationNodeName: string,
incomingSourceData: ISourceData | null,
initialPairedItem: IPairedItemData,
usedMethodName: PairedItemMethod = PAIRED_ITEM_METHOD.$GET_PAIRED_ITEM,
nodeBeforeLast?: string,
): INodeExecutionData => {
// Normalize inputs
const [pairedItem, sourceData] = normalizeInputs(initialPairedItem, incomingSourceData);
if (!sourceData) {
throw createPairedItemNotFound(destinationNodeName, nodeBeforeLast);
}
const taskData = getTaskData(sourceData);
const outputData = getNodeOutput(taskData, sourceData, nodeBeforeLast);
const item = outputData[pairedItem.item];
const sourceArray = taskData?.source ?? [];
// Done: reached the destination node in the ancestry chain
if (sourceData.previousNode === destinationNodeName) {
if (pairedItem.item >= outputData.length) {
throw createInvalidPairedItemError({ nodeName: sourceData.previousNode });
}
return item;
}
// Normalize paired item to always be IPairedItemData[]
const nextPairedItems = normalizePairedItem(item.pairedItem);
if (nextPairedItems.length === 0) {
throw createMissingPairedItemError(sourceData.previousNode, usedMethodName);
}
// Recursively traverse ancestry to find the destination node + paired item
const results = nextPairedItems.flatMap((nextPairedItem) => {
const inputIndex = nextPairedItem.input ?? 0;
if (inputIndex >= sourceArray.length) return [];
const nextSource = nextPairedItem.sourceOverwrite ?? sourceArray[inputIndex];
try {
return createResultOk(
getPairedItem(
destinationNodeName,
nextSource,
{ ...nextPairedItem, input: inputIndex },
usedMethodName,
sourceData.previousNode,
),
);
} catch (error) {
return createResultError(error);
}
});
if (results.every((result) => !result.ok)) {
throw results[0].error;
}
const matchedItems = results.filter((result) => result.ok).map((result) => result.result);
if (matchedItems.length === 0) {
if (sourceArray.length === 0) throw createNoConnectionError(destinationNodeName);
throw createBranchNotFoundError(sourceData.previousNode, pairedItem.item, nodeBeforeLast);
}
const [first, ...rest] = matchedItems;
if (rest.some((r) => r !== first)) {
throw createPairedItemMultipleItemsFound(destinationNodeName, pairedItem.item);
}
return first;
};
const handleFromAi = (
name: string,
_description?: string,
_type: string = 'string',
defaultValue?: unknown,
) => {
const { itemIndex, runIndex } = that;
if (!name || name === '') {
throw new ExpressionError("Add a key, e.g. $fromAI('placeholder_name')", {
runIndex,
itemIndex,
});
}
const nameValidationRegex = /^[a-zA-Z0-9_-]{0,64}$/;
if (!nameValidationRegex.test(name)) {
throw new ExpressionError(
'Invalid parameter key, must be between 1 and 64 characters long and only contain lowercase letters, uppercase letters, numbers, underscores, and hyphens',
{
runIndex,
itemIndex,
},
);
}
const inputData =
that.runExecutionData?.resultData.runData[that.activeNodeName]?.[runIndex].inputOverride;
const placeholdersDataInputData =
inputData?.[NodeConnectionTypes.AiTool]?.[0]?.[itemIndex].json;
if (!placeholdersDataInputData) {
throw new ExpressionError('No execution data available', {
runIndex,
itemIndex,
type: 'no_execution_data',
});
}
return (
// TS does not know that the key exists, we need to address this in refactor
(placeholdersDataInputData?.query as Record)?.[name] ??
placeholdersDataInputData?.[name] ??
defaultValue
);
};
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("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
const ensureNodeExecutionData = () => {
if (
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
) {
throw createExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_node_execution_data',
descriptionKey: 'noNodeExecutionData',
nodeCause: nodeName,
});
}
};
return new Proxy(
{},
{
has: () => true,
ownKeys() {
return [
PAIRED_ITEM_METHOD.PAIRED_ITEM,
'isExecuted',
PAIRED_ITEM_METHOD.ITEM_MATCHING,
PAIRED_ITEM_METHOD.ITEM,
'first',
'last',
'all',
'context',
'params',
];
},
get(target, property, receiver) {
if (property === 'isProxy') return true;
if (property === 'isExecuted') {
return (
that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) ?? false
);
}
if (
property === PAIRED_ITEM_METHOD.PAIRED_ITEM ||
property === PAIRED_ITEM_METHOD.ITEM_MATCHING ||
property === PAIRED_ITEM_METHOD.ITEM
) {
// Before resolving the pairedItem make sure that the requested node comes in the
// graph before the current one
const activeNode = that.workflow.getNode(that.activeNodeName);
let contextNode = that.contextNodeName;
if (activeNode) {
const parentMainInputNode = that.workflow.getParentMainInputNode(activeNode);
contextNode = parentMainInputNode.name ?? contextNode;
}
const parentNodes = that.workflow.getParentNodes(contextNode);
if (!parentNodes.includes(nodeName)) {
throw createNoConnectionError(nodeName);
}
ensureNodeExecutionData();
const pairedItemMethod = (itemIndex?: number) => {
if (itemIndex === undefined) {
if (property === PAIRED_ITEM_METHOD.ITEM_MATCHING) {
throw createExpressionError('Missing item index for .itemMatching()', {
itemIndex,
});
}
itemIndex = that.itemIndex;
}
if (!that.connectionInputData.length) {
const pinnedData = getPinDataIfManualExecution(
that.workflow,
nodeName,
that.mode,
);
if (pinnedData) {
return pinnedData[itemIndex];
}
}
const executionData = that.connectionInputData;
const input = executionData?.[itemIndex];
if (!input) {
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
functionality: 'pairedItem',
functionOverrides: {
description: `Some intermediate nodes between ‘${nodeName}‘ and ‘${that.activeNodeName}‘ have not executed yet.`,
message: 'Can’t get data',
},
description: `Some intermediate nodes between ‘${nodeName}‘ and ‘${that.activeNodeName}‘ have not executed yet.`,
causeDetailed: `pairedItem can\'t be found when intermediate nodes between ‘${nodeName}‘ and ‘${that.activeNodeName} have not executed yet.`,
itemIndex,
type: 'paired_item_intermediate_nodes',
});
}
// As we operate on the incoming item we can be sure that pairedItem is not an
// array. After all can it only come from exactly one previous node via a certain
// input. For that reason do we not have to consider the array case.
const pairedItem = input.pairedItem as IPairedItemData;
if (pairedItem === undefined) {
throw createMissingPairedItemError(that.activeNodeName, property);
}
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,
});
}
const sourceData: ISourceData | null =
that.executeData.source.main[pairedItem.input || 0] ??
that.executeData.source.main[0];
return getPairedItem(nodeName, sourceData, pairedItem, property);
};
if (property === PAIRED_ITEM_METHOD.ITEM) {
return pairedItemMethod();
}
return pairedItemMethod;
}
if (property === 'first') {
ensureNodeExecutionData();
return (branchIndex?: number, runIndex?: number) => {
branchIndex =
branchIndex ??
// default to the output the active node is connected to
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
?.sourceIndex ??
0;
const executionData = that.getNodeExecutionOrPinnedData({
nodeName,
branchIndex,
runIndex,
});
if (executionData[0]) return executionData[0];
return undefined;
};
}
if (property === 'last') {
ensureNodeExecutionData();
return (branchIndex?: number, runIndex?: number) => {
branchIndex =
branchIndex ??
// default to the output the active node is connected to
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
?.sourceIndex ??
0;
const executionData = that.getNodeExecutionOrPinnedData({
nodeName,
branchIndex,
runIndex,
});
if (!executionData.length) return undefined;
if (executionData[executionData.length - 1]) {
return executionData[executionData.length - 1];
}
return undefined;
};
}
if (property === 'all') {
ensureNodeExecutionData();
return (branchIndex?: number, runIndex?: number) => {
branchIndex =
branchIndex ??
// default to the output the active node is connected to
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
?.sourceIndex ??
0;
return that.getNodeExecutionOrPinnedData({ 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() {
return ['all', 'context', 'first', 'item', 'last', 'params'];
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
get(target, property, receiver) {
if (property === 'isProxy') return true;
if (that.connectionInputData.length === 0) {
throw createExpressionError('No execution data available', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_execution_data',
});
}
if (property === 'item') {
return that.connectionInputData[that.itemIndex];
}
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: createEnvProvider(
that.runIndex,
that.itemIndex,
that.envProviderState ?? createEnvProviderState(),
),
$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.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.additionalKeys,
that.executeData,
defaultReturnRunIndex,
{},
that.contextNodeName,
);
return dataProxy.getDataProxy();
},
$fromAI: handleFromAi,
// Make sure mis-capitalized $fromAI is handled correctly even though we don't auto-complete it
$fromai: handleFromAi,
$fromAi: handleFromAi,
$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),
$rawParameter: this.nodeParameterGetter(this.activeNodeName, false),
$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,
DateTime,
Interval,
Duration,
...that.additionalKeys,
$getPairedItem: getPairedItem,
// deprecated
$jmespath: jmespathWrapper,
$position: this.itemIndex,
$thisItem: that.connectionInputData[that.itemIndex],
$thisItemIndex: this.itemIndex,
$thisRunIndex: this.runIndex,
$nodeVersion: that.workflow.getNode(that.activeNodeName)?.typeVersion,
$nodeId: that.workflow.getNode(that.activeNodeName)?.id,
$agentInfo: this.agentInfo(),
$webhookId: that.workflow.getNode(that.activeNodeName)?.webhookId,
};
const throwOnMissingExecutionData = opts?.throwOnMissingExecutionData ?? true;
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, throwOnMissingExecutionData)?.json;
}
if (name === '$binary') {
return that.nodeDataGetter(that.contextNodeName, true, throwOnMissingExecutionData)
?.binary;
}
return Reflect.get(target, name, receiver);
},
});
}
}