mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
feat(core): Improve paired item and add additional variables (#3765)
* ⚡ Remove duplicate and old string * ⚡ Add telemetry * ⚡ Futher improvements * ⚡ Change error message and display only name of last parameter * 👕 Fix lint issue * ⚡ Remove not needed comments * ⚡ Rename properties, add new ones and improve error messages * ⚡ Add support for $execution, $prevNode and make it possible to use proxies as object * ⚡ Some small improvements * 🐛 Fix error message * ⚡ Improve some error messages * ⚡ Change resumeUrl variable and display in editor * ⚡ Fix and extend tests * ⚡ Multiple pairedItem improvements * ⚡ Display "More Info" link with error messages if user can fix issue * ⚡ Display different errors in Function Nodes
This commit is contained in:
parent
737cbf9694
commit
5526057efc
|
@ -160,8 +160,14 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
|
|
||||||
if (!properties.success && runData?.data.resultData.error) {
|
if (!properties.success && runData?.data.resultData.error) {
|
||||||
properties.error_message = runData?.data.resultData.error.message;
|
properties.error_message = runData?.data.resultData.error.message;
|
||||||
let errorNodeName = runData?.data.resultData.error.node?.name;
|
let errorNodeName =
|
||||||
properties.error_node_type = runData?.data.resultData.error.node?.type;
|
'node' in runData?.data.resultData.error
|
||||||
|
? runData?.data.resultData.error.node?.name
|
||||||
|
: undefined;
|
||||||
|
properties.error_node_type =
|
||||||
|
'node' in runData?.data.resultData.error
|
||||||
|
? runData?.data.resultData.error.node?.type
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (runData.data.resultData.lastNodeExecuted) {
|
if (runData.data.resultData.lastNodeExecuted) {
|
||||||
const lastNode = TelemetryHelpers.getNodeTypeForName(
|
const lastNode = TelemetryHelpers.getNodeTypeForName(
|
||||||
|
|
|
@ -1487,11 +1487,20 @@ export async function requestWithAuthentication(
|
||||||
*/
|
*/
|
||||||
export function getAdditionalKeys(
|
export function getAdditionalKeys(
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
): IWorkflowDataProxyAdditionalKeys {
|
): IWorkflowDataProxyAdditionalKeys {
|
||||||
const executionId = additionalData.executionId || PLACEHOLDER_EMPTY_EXECUTION_ID;
|
const executionId = additionalData.executionId || PLACEHOLDER_EMPTY_EXECUTION_ID;
|
||||||
|
const resumeUrl = `${additionalData.webhookWaitingBaseUrl}/${executionId}`;
|
||||||
return {
|
return {
|
||||||
|
$execution: {
|
||||||
|
id: executionId,
|
||||||
|
mode: mode === 'manual' ? 'test' : 'production',
|
||||||
|
resumeUrl,
|
||||||
|
},
|
||||||
|
|
||||||
|
// deprecated
|
||||||
$executionId: executionId,
|
$executionId: executionId,
|
||||||
$resumeWebhookUrl: `${additionalData.webhookWaitingBaseUrl}/${executionId}`,
|
$resumeWebhookUrl: resumeUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1601,7 +1610,7 @@ export async function getCredentials(
|
||||||
// TODO: solve using credentials via expression
|
// TODO: solve using credentials via expression
|
||||||
// if (name.charAt(0) === '=') {
|
// if (name.charAt(0) === '=') {
|
||||||
// // If the credential name is an expression resolve it
|
// // If the credential name is an expression resolve it
|
||||||
// const additionalKeys = getAdditionalKeys(additionalData);
|
// const additionalKeys = getAdditionalKeys(additionalData, mode);
|
||||||
// name = workflow.expression.getParameterValue(
|
// name = workflow.expression.getParameterValue(
|
||||||
// name,
|
// name,
|
||||||
// runExecutionData || null,
|
// runExecutionData || null,
|
||||||
|
@ -1638,30 +1647,29 @@ export function getNode(node: INode): INode {
|
||||||
* Clean up parameter data to make sure that only valid data gets returned
|
* Clean up parameter data to make sure that only valid data gets returned
|
||||||
* INFO: Currently only converts Luxon Dates as we know for sure it will not be breaking
|
* INFO: Currently only converts Luxon Dates as we know for sure it will not be breaking
|
||||||
*/
|
*/
|
||||||
function cleanupParameterData(inputData: NodeParameterValueType): NodeParameterValueType {
|
function cleanupParameterData(inputData: NodeParameterValueType): void {
|
||||||
if (inputData === null || inputData === undefined) {
|
if (typeof inputData !== 'object' || inputData === null) {
|
||||||
return inputData;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(inputData)) {
|
if (Array.isArray(inputData)) {
|
||||||
inputData.forEach((value) => cleanupParameterData(value));
|
inputData.forEach((value) => cleanupParameterData(value));
|
||||||
return inputData;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (inputData.constructor.name === 'DateTime') {
|
|
||||||
// Is a special luxon date so convert to string
|
|
||||||
return inputData.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof inputData === 'object') {
|
if (typeof inputData === 'object') {
|
||||||
Object.keys(inputData).forEach((key) => {
|
Object.keys(inputData).forEach((key) => {
|
||||||
inputData[key as keyof typeof inputData] = cleanupParameterData(
|
if (typeof inputData[key as keyof typeof inputData] === 'object') {
|
||||||
inputData[key as keyof typeof inputData],
|
if (inputData[key as keyof typeof inputData]?.constructor.name === 'DateTime') {
|
||||||
);
|
// Is a special luxon date so convert to string
|
||||||
|
inputData[key as keyof typeof inputData] =
|
||||||
|
inputData[key as keyof typeof inputData]?.toString();
|
||||||
|
} else {
|
||||||
|
cleanupParameterData(inputData[key as keyof typeof inputData]);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return inputData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1710,7 +1718,7 @@ export function getNodeParameter(
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
|
|
||||||
returnData = cleanupParameterData(returnData);
|
cleanupParameterData(returnData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.context) e.context.parameter = parameterName;
|
if (e.context) e.context.parameter = parameterName;
|
||||||
e.cause = value;
|
e.cause = value;
|
||||||
|
@ -1883,7 +1891,7 @@ export function getExecutePollFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
undefined,
|
undefined,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2032,7 +2040,7 @@ export function getExecuteTriggerFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
undefined,
|
undefined,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2160,7 +2168,7 @@ export function getExecuteFunctions(
|
||||||
connectionInputData,
|
connectionInputData,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2237,7 +2245,7 @@ export function getExecuteFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
executeData,
|
executeData,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2272,7 +2280,7 @@ export function getExecuteFunctions(
|
||||||
{},
|
{},
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
return dataProxy.getDataProxy();
|
return dataProxy.getDataProxy();
|
||||||
|
@ -2421,7 +2429,7 @@ export function getExecuteSingleFunctions(
|
||||||
connectionInputData,
|
connectionInputData,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2501,7 +2509,7 @@ export function getExecuteSingleFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
executeData,
|
executeData,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2521,7 +2529,7 @@ export function getExecuteSingleFunctions(
|
||||||
{},
|
{},
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
return dataProxy.getDataProxy();
|
return dataProxy.getDataProxy();
|
||||||
|
@ -2658,6 +2666,7 @@ export function getLoadOptionsFunctions(
|
||||||
const runExecutionData: IRunExecutionData | null = null;
|
const runExecutionData: IRunExecutionData | null = null;
|
||||||
const itemIndex = 0;
|
const itemIndex = 0;
|
||||||
const runIndex = 0;
|
const runIndex = 0;
|
||||||
|
const mode = 'internal' as WorkflowExecuteMode;
|
||||||
const connectionInputData: INodeExecutionData[] = [];
|
const connectionInputData: INodeExecutionData[] = [];
|
||||||
|
|
||||||
return getNodeParameter(
|
return getNodeParameter(
|
||||||
|
@ -2668,9 +2677,9 @@ export function getLoadOptionsFunctions(
|
||||||
node,
|
node,
|
||||||
parameterName,
|
parameterName,
|
||||||
itemIndex,
|
itemIndex,
|
||||||
'internal' as WorkflowExecuteMode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
undefined,
|
undefined,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2792,7 +2801,7 @@ export function getExecuteHookFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
undefined,
|
undefined,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2806,7 +2815,7 @@ export function getExecuteHookFunctions(
|
||||||
additionalData,
|
additionalData,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
isTest,
|
isTest,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -2945,7 +2954,7 @@ export function getExecuteWebhookFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
undefined,
|
undefined,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
options,
|
options,
|
||||||
|
@ -2983,7 +2992,7 @@ export function getExecuteWebhookFunctions(
|
||||||
additionalData,
|
additionalData,
|
||||||
mode,
|
mode,
|
||||||
additionalData.timezone,
|
additionalData.timezone,
|
||||||
getAdditionalKeys(additionalData),
|
getAdditionalKeys(additionalData, mode),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getTimezone: (): string => {
|
getTimezone: (): string => {
|
||||||
|
|
|
@ -78,6 +78,13 @@ export default mixins(
|
||||||
const connectionInputData = this.connectionInputData(parentNode, activeNode!.name, inputName, runIndex, nodeConnection);
|
const connectionInputData = this.connectionInputData(parentNode, activeNode!.name, inputName, runIndex, nodeConnection);
|
||||||
|
|
||||||
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
|
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||||
|
$execution: {
|
||||||
|
id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
mode: 'test',
|
||||||
|
resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
},
|
||||||
|
|
||||||
|
// deprecated
|
||||||
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="error-header">
|
<div class="error-header">
|
||||||
<div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ': ' + getErrorMessage() }}</div>
|
<div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ': ' + getErrorMessage() }}</div>
|
||||||
<div class="error-description" v-if="error.description">{{getErrorDescription()}}</div>
|
<div class="error-description" v-if="error.description" v-html="getErrorDescription()"></div>
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary class="error-details__summary">
|
<summary class="error-details__summary">
|
||||||
|
@ -139,28 +139,34 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
replacePlaceholders (parameter: string, message: string): string {
|
||||||
|
const parameterName = this.parameterDisplayName(parameter, false);
|
||||||
|
const parameterFullName = this.parameterDisplayName(parameter, true);
|
||||||
|
return message.replace(/%%PARAMETER%%/g, parameterName).replace(/%%PARAMETER_FULL%%/g, parameterFullName);
|
||||||
|
},
|
||||||
getErrorDescription (): string {
|
getErrorDescription (): string {
|
||||||
if (!this.error.context || !this.error.context.descriptionTemplate) {
|
if (!this.error.context || !this.error.context.descriptionTemplate) {
|
||||||
return this.error.description;
|
return this.error.description;
|
||||||
}
|
}
|
||||||
|
return this.replacePlaceholders(this.error.context.parameter, this.error.context.descriptionTemplate);
|
||||||
const parameterName = this.parameterDisplayName(this.error.context.parameter);
|
|
||||||
return this.error.context.descriptionTemplate.replace(/%%PARAMETER%%/g, parameterName);
|
|
||||||
},
|
},
|
||||||
getErrorMessage (): string {
|
getErrorMessage (): string {
|
||||||
if (!this.error.context || !this.error.context.messageTemplate) {
|
if (!this.error.context || !this.error.context.messageTemplate) {
|
||||||
return this.error.message;
|
return this.error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameterName = this.parameterDisplayName(this.error.context.parameter);
|
return this.replacePlaceholders(this.error.context.parameter, this.error.context.messageTemplate);
|
||||||
return this.error.context.messageTemplate.replace(/%%PARAMETER%%/g, parameterName);
|
|
||||||
},
|
},
|
||||||
parameterDisplayName(path: string) {
|
parameterDisplayName(path: string, fullPath = true) {
|
||||||
try {
|
try {
|
||||||
const parameters = this.parameterName(this.parameters, path.split('.'));
|
const parameters = this.parameterName(this.parameters, path.split('.'));
|
||||||
if (!parameters.length) {
|
if (!parameters.length) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fullPath === false) {
|
||||||
|
return parameters.pop()!.displayName;
|
||||||
|
}
|
||||||
return parameters.map(parameter => parameter.displayName).join(' > ');
|
return parameters.map(parameter => parameter.displayName).join(' > ');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return `Could not find parameter "${path}"`;
|
return `Could not find parameter "${path}"`;
|
||||||
|
|
|
@ -420,6 +420,13 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||||
|
$execution: {
|
||||||
|
id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
mode: 'test',
|
||||||
|
resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
},
|
||||||
|
|
||||||
|
// deprecated
|
||||||
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import {
|
import {
|
||||||
IExecutionsCurrentSummaryExtended,
|
IExecutionsCurrentSummaryExtended,
|
||||||
IPushData,
|
IPushData,
|
||||||
IPushDataConsoleMessage,
|
|
||||||
IPushDataExecutionFinished,
|
|
||||||
IPushDataExecutionStarted,
|
|
||||||
IPushDataNodeExecuteAfter,
|
|
||||||
IPushDataNodeExecuteBefore,
|
|
||||||
IPushDataTestWebhook,
|
|
||||||
} from '../../Interface';
|
} from '../../Interface';
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
@ -16,7 +10,11 @@ import { titleChange } from '@/components/mixins/titleChange';
|
||||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ExpressionError,
|
||||||
|
IDataObject,
|
||||||
INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
|
IWorkflowBase,
|
||||||
|
TelemetryHelpers,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
@ -215,7 +213,7 @@ export const pushConnection = mixins(
|
||||||
|
|
||||||
const runDataExecuted = pushData.data;
|
const runDataExecuted = pushData.data;
|
||||||
|
|
||||||
const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data.resultData.error);
|
const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data);
|
||||||
|
|
||||||
const workflow = this.getCurrentWorkflow();
|
const workflow = this.getCurrentWorkflow();
|
||||||
if (runDataExecuted.waitTill !== undefined) {
|
if (runDataExecuted.waitTill !== undefined) {
|
||||||
|
@ -251,8 +249,48 @@ export const pushConnection = mixins(
|
||||||
} else if (runDataExecuted.finished !== true) {
|
} else if (runDataExecuted.finished !== true) {
|
||||||
this.$titleSet(workflow.name as string, 'ERROR');
|
this.$titleSet(workflow.name as string, 'ERROR');
|
||||||
|
|
||||||
|
if (
|
||||||
|
runDataExecuted.data.resultData.error!.name === 'ExpressionError' &&
|
||||||
|
(runDataExecuted.data.resultData.error as ExpressionError).context.functionality === 'pairedItem'
|
||||||
|
) {
|
||||||
|
const error = runDataExecuted.data.resultData.error as ExpressionError;
|
||||||
|
|
||||||
|
this.getWorkflowDataToSave().then((workflowData) => {
|
||||||
|
const eventData: IDataObject = {
|
||||||
|
caused_by_credential: false,
|
||||||
|
error_message: error.description,
|
||||||
|
error_title: error.message,
|
||||||
|
error_type: error.context.type,
|
||||||
|
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error.context.nodeCause && ['no pairing info', 'invalid pairing info'].includes(error.context.type as string)) {
|
||||||
|
const node = workflow.getNode(error.context.nodeCause as string);
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
eventData.is_pinned = !!workflow.getPinDataOfNode(node.name);
|
||||||
|
eventData.mode = node.parameters.mode;
|
||||||
|
eventData.node_type = node.type;
|
||||||
|
eventData.operation = node.parameters.operation;
|
||||||
|
eventData.resource = node.parameters.resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$telemetry.track('Instance FE emitted paired item error', eventData);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let title: string;
|
||||||
|
if (runDataExecuted.data.resultData.lastNodeExecuted) {
|
||||||
|
title = `Problem in node ‘${runDataExecuted.data.resultData.lastNodeExecuted}‘`;
|
||||||
|
} else {
|
||||||
|
title = 'Problem executing workflow';
|
||||||
|
}
|
||||||
|
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title: 'Problem executing workflow',
|
title,
|
||||||
message: runDataExecutedErrorMessage,
|
message: runDataExecutedErrorMessage,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { ExecutionError } from 'n8n-workflow';
|
import { IRunExecutionData } from 'n8n-workflow';
|
||||||
import type { ElMessageBoxOptions } from 'element-ui/types/message-box';
|
import type { ElMessageBoxOptions } from 'element-ui/types/message-box';
|
||||||
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
|
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
|
||||||
import { isChildOf } from './helpers';
|
|
||||||
import { sanitizeHtml } from '@/utils';
|
import { sanitizeHtml } from '@/utils';
|
||||||
|
|
||||||
let stickyNotificationQueue: ElNotificationComponent[] = [];
|
let stickyNotificationQueue: ElNotificationComponent[] = [];
|
||||||
|
@ -83,16 +82,22 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
return this.$message(config);
|
return this.$message(config);
|
||||||
},
|
},
|
||||||
|
|
||||||
$getExecutionError(error?: ExecutionError) {
|
$getExecutionError(data: IRunExecutionData) {
|
||||||
// There was a problem with executing the workflow
|
const error = data.resultData.error;
|
||||||
let errorMessage = 'There was a problem executing the workflow!';
|
|
||||||
|
let errorMessage: string;
|
||||||
|
|
||||||
|
if (data.resultData.lastNodeExecuted && error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
} else {
|
||||||
|
errorMessage = 'There was a problem executing the workflow!';
|
||||||
|
|
||||||
if (error && error.message) {
|
if (error && error.message) {
|
||||||
let nodeName: string | undefined;
|
let nodeName: string | undefined;
|
||||||
if (error.node) {
|
if ('node' in error) {
|
||||||
nodeName = typeof error.node === 'string'
|
nodeName = typeof error.node === 'string'
|
||||||
? error.node
|
? error.node
|
||||||
: error.node.name;
|
: error.node!.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const receivedError = nodeName
|
const receivedError = nodeName
|
||||||
|
@ -100,6 +105,7 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
: error.message;
|
: error.message;
|
||||||
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
|
errorMessage = `There was a problem executing the workflow:<br /><strong>"${receivedError}"</strong>`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
},
|
},
|
||||||
|
|
|
@ -498,7 +498,7 @@ export const workflowHelpers = mixins(
|
||||||
|
|
||||||
getWebhookUrl (webhookData: IWebhookDescription, node: INode, showUrlFor?: string): string {
|
getWebhookUrl (webhookData: IWebhookDescription, node: INode, showUrlFor?: string): string {
|
||||||
if (webhookData.restartWebhook === true) {
|
if (webhookData.restartWebhook === true) {
|
||||||
return '$resumeWebhookUrl';
|
return '$execution.resumeUrl';
|
||||||
}
|
}
|
||||||
let baseUrl = this.$store.getters.getWebhookUrl;
|
let baseUrl = this.$store.getters.getWebhookUrl;
|
||||||
if (showUrlFor === 'test') {
|
if (showUrlFor === 'test') {
|
||||||
|
@ -577,6 +577,13 @@ export const workflowHelpers = mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||||
|
$execution: {
|
||||||
|
id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
mode: 'test',
|
||||||
|
resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
},
|
||||||
|
|
||||||
|
// deprecated
|
||||||
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
};
|
};
|
||||||
|
|
|
@ -309,7 +309,27 @@ export const TEST_PIN_DATA = [
|
||||||
code: 2,
|
code: 2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export const MAPPING_PARAMS = [`$evaluateExpression`, `$item`, `$jmespath`, `$node`, `$binary`, `$data`, `$env`, `$json`, `$now`, `$parameters`, `$position`, `$resumeWebhookUrl`, `$runIndex`, `$today`, `$workflow`, '$parameter'];
|
export const MAPPING_PARAMS = [
|
||||||
|
'$binary',
|
||||||
|
'$data',
|
||||||
|
'$env',
|
||||||
|
'$evaluateExpression',
|
||||||
|
'$execution',
|
||||||
|
'$input',
|
||||||
|
'$item',
|
||||||
|
'$jmespath',
|
||||||
|
'$json',
|
||||||
|
'$node',
|
||||||
|
'$now',
|
||||||
|
'$parameter',
|
||||||
|
'$parameters',
|
||||||
|
'$position',
|
||||||
|
'$prevNode',
|
||||||
|
'$resumeWebhookUrl',
|
||||||
|
'$runIndex',
|
||||||
|
'$today',
|
||||||
|
'$workflow',
|
||||||
|
];
|
||||||
|
|
||||||
export const DEFAULT_STICKY_HEIGHT = 160;
|
export const DEFAULT_STICKY_HEIGHT = 160;
|
||||||
export const DEFAULT_STICKY_WIDTH = 240;
|
export const DEFAULT_STICKY_WIDTH = 240;
|
||||||
|
|
|
@ -535,8 +535,8 @@ export default mixins(
|
||||||
|
|
||||||
if (nodeErrorFound === false) {
|
if (nodeErrorFound === false) {
|
||||||
const resultError = data.data.resultData.error;
|
const resultError = data.data.resultData.error;
|
||||||
const errorMessage = this.$getExecutionError(resultError);
|
const errorMessage = this.$getExecutionError(data.data);
|
||||||
const shouldTrack = resultError && resultError.node && resultError.node.type.startsWith('n8n-nodes-base');
|
const shouldTrack = resultError && 'node' in resultError && resultError.node!.type.startsWith('n8n-nodes-base');
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title: 'Failed execution',
|
title: 'Failed execution',
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
|
|
|
@ -114,7 +114,7 @@ export class Wait implements INodeType {
|
||||||
],
|
],
|
||||||
default: 'none',
|
default: 'none',
|
||||||
description:
|
description:
|
||||||
'If and how incoming resume-webhook-requests to $resumeWebhookUrl should be authenticated for additional security',
|
'If and how incoming resume-webhook-requests to $execution.resumeUrl should be authenticated for additional security',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Resume',
|
displayName: 'Resume',
|
||||||
|
@ -212,7 +212,7 @@ export class Wait implements INodeType {
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
{
|
{
|
||||||
displayName:
|
displayName:
|
||||||
'The webhook URL will be generated at run time. It can be referenced with the <strong>$resumeWebhookUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
|
'The webhook URL will be generated at run time. It can be referenced with the <strong>$execution.resumeUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
|
||||||
name: 'webhookNotice',
|
name: 'webhookNotice',
|
||||||
type: 'notice',
|
type: 'notice',
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import { IDataObject } from './Interfaces';
|
||||||
import { ExecutionBaseError } from './NodeErrors';
|
import { ExecutionBaseError } from './NodeErrors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,11 +12,14 @@ export class ExpressionError extends ExecutionBaseError {
|
||||||
causeDetailed?: string;
|
causeDetailed?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
descriptionTemplate?: string;
|
descriptionTemplate?: string;
|
||||||
runIndex?: number;
|
failExecution?: boolean;
|
||||||
|
functionality?: 'pairedItem';
|
||||||
itemIndex?: number;
|
itemIndex?: number;
|
||||||
messageTemplate?: string;
|
messageTemplate?: string;
|
||||||
|
nodeCause?: string;
|
||||||
parameter?: string;
|
parameter?: string;
|
||||||
failExecution?: boolean;
|
runIndex?: number;
|
||||||
|
type?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
super(new Error(message));
|
super(new Error(message));
|
||||||
|
@ -23,30 +28,25 @@ export class ExpressionError extends ExecutionBaseError {
|
||||||
this.description = options.description;
|
this.description = options.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.descriptionTemplate !== undefined) {
|
|
||||||
this.context.descriptionTemplate = options.descriptionTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.causeDetailed !== undefined) {
|
|
||||||
this.context.causeDetailed = options.causeDetailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.runIndex !== undefined) {
|
|
||||||
this.context.runIndex = options.runIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.itemIndex !== undefined) {
|
|
||||||
this.context.itemIndex = options.itemIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.parameter !== undefined) {
|
|
||||||
this.context.parameter = options.parameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.messageTemplate !== undefined) {
|
|
||||||
this.context.messageTemplate = options.messageTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.context.failExecution = !!options?.failExecution;
|
this.context.failExecution = !!options?.failExecution;
|
||||||
|
|
||||||
|
const allowedKeys = [
|
||||||
|
'causeDetailed',
|
||||||
|
'descriptionTemplate',
|
||||||
|
'functionality',
|
||||||
|
'itemIndex',
|
||||||
|
'messageTemplate',
|
||||||
|
'nodeCause',
|
||||||
|
'parameter',
|
||||||
|
'runIndex',
|
||||||
|
'type',
|
||||||
|
];
|
||||||
|
if (options !== undefined) {
|
||||||
|
Object.keys(options as IDataObject).forEach((key) => {
|
||||||
|
if (allowedKeys.includes(key)) {
|
||||||
|
this.context[key] = (options as IDataObject)[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type { WorkflowHooks } from './WorkflowHooks';
|
||||||
import type { WorkflowActivationError } from './WorkflowActivationError';
|
import type { WorkflowActivationError } from './WorkflowActivationError';
|
||||||
import type { WorkflowOperationError } from './WorkflowErrors';
|
import type { WorkflowOperationError } from './WorkflowErrors';
|
||||||
import type { NodeApiError, NodeOperationError } from './NodeErrors';
|
import type { NodeApiError, NodeOperationError } from './NodeErrors';
|
||||||
|
import { ExpressionError } from './ExpressionError';
|
||||||
|
|
||||||
export interface IAdditionalCredentialOptions {
|
export interface IAdditionalCredentialOptions {
|
||||||
oauth2?: IOAuth2Options;
|
oauth2?: IOAuth2Options;
|
||||||
|
@ -62,6 +63,7 @@ export interface IConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExecutionError =
|
export type ExecutionError =
|
||||||
|
| ExpressionError
|
||||||
| WorkflowActivationError
|
| WorkflowActivationError
|
||||||
| WorkflowOperationError
|
| WorkflowOperationError
|
||||||
| NodeOperationError
|
| NodeOperationError
|
||||||
|
|
|
@ -106,6 +106,16 @@ export class WorkflowDataProxy {
|
||||||
const that = this;
|
const that = this;
|
||||||
const node = this.workflow.nodes[nodeName];
|
const node = this.workflow.nodes[nodeName];
|
||||||
|
|
||||||
|
if (!that.runExecutionData?.executionData) {
|
||||||
|
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(
|
return new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
@ -128,11 +138,6 @@ export class WorkflowDataProxy {
|
||||||
name = name.toString();
|
name = name.toString();
|
||||||
const contextData = NodeHelpers.getContext(that.runExecutionData!, 'node', node);
|
const contextData = NodeHelpers.getContext(that.runExecutionData!, 'node', node);
|
||||||
|
|
||||||
if (!contextData.hasOwnProperty(name)) {
|
|
||||||
// Parameter does not exist on node
|
|
||||||
throw new Error(`Could not find parameter "${name}" on context of node "${nodeName}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return contextData[name];
|
return contextData[name];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -253,10 +258,13 @@ export class WorkflowDataProxy {
|
||||||
// Long syntax got used to return data from node in path
|
// Long syntax got used to return data from node in path
|
||||||
|
|
||||||
if (that.runExecutionData === null) {
|
if (that.runExecutionData === null) {
|
||||||
throw new ExpressionError(`Workflow did not run so do not have any execution-data.`, {
|
throw new ExpressionError(
|
||||||
|
`The workflow hasn't been executed yet, so you can't reference any output data`,
|
||||||
|
{
|
||||||
runIndex: that.runIndex,
|
runIndex: that.runIndex,
|
||||||
itemIndex: that.itemIndex,
|
itemIndex: that.itemIndex,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
||||||
|
@ -450,6 +458,46 @@ export class WorkflowDataProxy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private prevNodeGetter() {
|
||||||
|
const allowedValues = ['name', 'outputIndex', 'runIndex'];
|
||||||
|
const that = this;
|
||||||
|
|
||||||
|
return new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
ownKeys(target) {
|
||||||
|
return allowedValues;
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(k) {
|
||||||
|
return {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
get(target, name, receiver) {
|
||||||
|
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
|
* Returns a proxy to query data from the workflow
|
||||||
*
|
*
|
||||||
|
@ -472,11 +520,22 @@ export class WorkflowDataProxy {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
get(target, name, receiver) {
|
get(target, name, receiver) {
|
||||||
if (!allowedValues.includes(name.toString())) {
|
if (allowedValues.includes(name.toString())) {
|
||||||
throw new Error(`The key "${name.toString()}" is not supported!`);
|
const value = that.workflow[name as keyof typeof target];
|
||||||
|
|
||||||
|
if (value === undefined && name === 'id') {
|
||||||
|
throw new ExpressionError('Workflow is not saved', {
|
||||||
|
description: `Please save the workflow first to use $workflow`,
|
||||||
|
runIndex: that.runIndex,
|
||||||
|
itemIndex: that.itemIndex,
|
||||||
|
failExecution: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return that.workflow[name as keyof typeof target];
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(target, name, receiver);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -528,27 +587,60 @@ export class WorkflowDataProxy {
|
||||||
return jmespath.search(data, query);
|
return jmespath.search(data, query);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFunctionNode = (nodeName: string) => {
|
||||||
|
const node = that.workflow.getNode(nodeName);
|
||||||
|
return node && ['n8n-nodes-base.function', 'n8n-nodes-base.functionItem'].includes(node.type);
|
||||||
|
};
|
||||||
|
|
||||||
const createExpressionError = (
|
const createExpressionError = (
|
||||||
message: string,
|
message: string,
|
||||||
context?: {
|
context?: {
|
||||||
causeDetailed?: string;
|
causeDetailed?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
descriptionTemplate?: string;
|
descriptionTemplate?: string;
|
||||||
|
functionOverrides?: {
|
||||||
|
// Custom data to display for Function-Nodes
|
||||||
|
message?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
itemIndex?: number;
|
||||||
messageTemplate?: string;
|
messageTemplate?: string;
|
||||||
|
moreInfoLink?: boolean;
|
||||||
|
nodeCause?: string;
|
||||||
|
runIndex?: number;
|
||||||
|
type?: string;
|
||||||
},
|
},
|
||||||
nodeName?: string,
|
|
||||||
) => {
|
) => {
|
||||||
if (nodeName) {
|
if (isFunctionNode(that.activeNodeName) && 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);
|
const pinData = this.workflow.getPinDataOfNode(nodeName);
|
||||||
|
|
||||||
if (pinData) {
|
if (pinData) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
context = {};
|
context = {};
|
||||||
}
|
}
|
||||||
message = `‘${nodeName}‘ must be unpinned to execute`;
|
message = `‘Node ${nodeName}‘ must be unpinned to execute`;
|
||||||
context.description = `To fetch the data the expression needs, The node ‘${nodeName}’ needs to execute without being pinned. <a>Unpin it</a>`;
|
context.messageTemplate = undefined;
|
||||||
context.description = `To fetch the data for the expression, you must unpin the node '${nodeName}' and execute the workflow again.`;
|
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 '${nodeName}' 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 || isFunctionNode(nodeName))) {
|
||||||
|
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;
|
||||||
|
context.descriptionTemplate += moreInfoLink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -556,6 +648,7 @@ export class WorkflowDataProxy {
|
||||||
runIndex: that.runIndex,
|
runIndex: that.runIndex,
|
||||||
itemIndex: that.itemIndex,
|
itemIndex: that.itemIndex,
|
||||||
failExecution: true,
|
failExecution: true,
|
||||||
|
functionality: 'pairedItem',
|
||||||
...context,
|
...context,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -575,6 +668,8 @@ export class WorkflowDataProxy {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentPairedItem = pairedItem;
|
||||||
|
|
||||||
let nodeBeforeLast: string | undefined;
|
let nodeBeforeLast: string | undefined;
|
||||||
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
|
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
|
||||||
taskData =
|
taskData =
|
||||||
|
@ -584,46 +679,54 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
||||||
if (previousNodeOutput >= taskData.data!.main.length) {
|
if (previousNodeOutput >= taskData.data!.main.length) {
|
||||||
// `Could not resolve as the defined node-output is not valid on node '${sourceData.previousNode}'.`
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
throw createExpressionError(
|
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||||
'Can’t get data for expression',
|
functionOverrides: {
|
||||||
{
|
message: 'Can’t get data',
|
||||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’',
|
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
nodeBeforeLast,
|
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) {
|
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
|
||||||
// `Could not resolve as the defined item index is not valid on node '${sourceData.previousNode}'.
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
throw createExpressionError(
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
'Can’t get data for expression',
|
functionOverrides: {
|
||||||
{
|
message: 'Can’t get data',
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
|
||||||
description: `Item points to an item which does not exist`,
|
|
||||||
causeDetailed: `The pairedItem data points to an item ‘${pairedItem.item}‘ which does not exist on node ‘${sourceData.previousNode}‘ (output node did probably supply a wrong one)`,
|
|
||||||
},
|
},
|
||||||
nodeBeforeLast,
|
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 =
|
const itemPreviousNode: INodeExecutionData =
|
||||||
taskData.data!.main[previousNodeOutput]![pairedItem.item];
|
taskData.data!.main[previousNodeOutput]![pairedItem.item];
|
||||||
|
|
||||||
if (itemPreviousNode.pairedItem === undefined) {
|
if (itemPreviousNode.pairedItem === undefined) {
|
||||||
// `Could not resolve, as pairedItem data is missing on node '${sourceData.previousNode}'.`,
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
throw createExpressionError(
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
'Can’t get data for expression',
|
functionOverrides: {
|
||||||
{
|
message: 'Can’t get data',
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
|
||||||
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}’ did probably not supply it)`,
|
|
||||||
},
|
},
|
||||||
sourceData.previousNode,
|
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)) {
|
if (Array.isArray(itemPreviousNode.pairedItem)) {
|
||||||
|
@ -650,13 +753,20 @@ export class WorkflowDataProxy {
|
||||||
if (results.length !== 1) {
|
if (results.length !== 1) {
|
||||||
throw createExpressionError('Invalid expression', {
|
throw createExpressionError('Invalid expression', {
|
||||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||||
description: `The expression uses data in node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
|
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];
|
return results[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPairedItem = pairedItem;
|
||||||
|
|
||||||
// pairedItem is not an array
|
// pairedItem is not an array
|
||||||
if (typeof itemPreviousNode.pairedItem === 'number') {
|
if (typeof itemPreviousNode.pairedItem === 'number') {
|
||||||
pairedItem = {
|
pairedItem = {
|
||||||
|
@ -672,19 +782,30 @@ export class WorkflowDataProxy {
|
||||||
// A trigger node got reached, so looks like that that item can not be resolved
|
// A trigger node got reached, so looks like that that item can not be resolved
|
||||||
throw createExpressionError('Invalid expression', {
|
throw createExpressionError('Invalid expression', {
|
||||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||||
description: `The expression uses data in node ‘${destinationNodeName}’ but there is no path back to it. Please check this node is connected to node ‘${that.activeNodeName}’ (there can be other nodes in between).`,
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData.previousNode}'.`
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
throw createExpressionError(
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
'Can’t get data for expression',
|
functionOverrides: {
|
||||||
{
|
message: `Can’t get data`,
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
|
||||||
description: `Item points to a node input which does not exist`,
|
|
||||||
causeDetailed: `The pairedItem data points to a node input ‘${itemInput}‘ which does not exist on node ‘${sourceData.previousNode}‘ (node did probably supply a wrong one)`,
|
|
||||||
},
|
},
|
||||||
nodeBeforeLast,
|
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;
|
nodeBeforeLast = sourceData.previousNode;
|
||||||
|
@ -692,15 +813,16 @@ export class WorkflowDataProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceData === null) {
|
if (sourceData === null) {
|
||||||
// 'Could not resolve, probably no pairedItem exists.'
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
throw createExpressionError(
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
'Can’t get data for expression',
|
functionOverrides: {
|
||||||
{
|
message: `Can’t get data`,
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
|
||||||
description: `Could not resolve, probably no pairedItem exists`,
|
|
||||||
},
|
},
|
||||||
nodeBeforeLast,
|
nodeCause: nodeBeforeLast,
|
||||||
);
|
description: `Could not resolve, proably no pairedItem exists`,
|
||||||
|
type: 'no pairing info',
|
||||||
|
moreInfoLink: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
taskData =
|
taskData =
|
||||||
|
@ -710,25 +832,36 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
||||||
if (previousNodeOutput >= taskData.data!.main.length) {
|
if (previousNodeOutput >= taskData.data!.main.length) {
|
||||||
// `Could not resolve pairedItem as the node output '${previousNodeOutput}' does not exist on node '${sourceData.previousNode}'`
|
|
||||||
throw createExpressionError('Can’t get data for expression', {
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
|
functionOverrides: {
|
||||||
|
message: `Can’t get data`,
|
||||||
|
},
|
||||||
description: `Item points to a node output which does not exist`,
|
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)`,
|
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) {
|
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
|
||||||
// `Could not resolve pairedItem as the item with the index '${pairedItem.item}' does not exist on node '${sourceData.previousNode}'.`
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
throw createExpressionError(
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
'Can’t get data for expression',
|
functionOverrides: {
|
||||||
{
|
message: `Can’t get data`,
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
|
||||||
description: `Item points to an item which does not exist`,
|
|
||||||
causeDetailed: `The pairedItem data points to an item ‘${pairedItem.item}‘ which does not exist on node ‘${sourceData.previousNode}‘ (output node did probably supply a wrong one)`,
|
|
||||||
},
|
},
|
||||||
nodeBeforeLast,
|
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];
|
return taskData.data!.main[previousNodeOutput]![pairedItem.item];
|
||||||
|
@ -737,20 +870,26 @@ export class WorkflowDataProxy {
|
||||||
const base = {
|
const base = {
|
||||||
$: (nodeName: string) => {
|
$: (nodeName: string) => {
|
||||||
if (!nodeName) {
|
if (!nodeName) {
|
||||||
throw new ExpressionError('When calling $(), please specify a node', {
|
throw createExpressionError('When calling $(), please specify a node');
|
||||||
runIndex: that.runIndex,
|
}
|
||||||
itemIndex: that.itemIndex,
|
|
||||||
failExecution: true,
|
const referencedNode = that.workflow.getNode(nodeName);
|
||||||
});
|
if (referencedNode === null) {
|
||||||
|
throw createExpressionError(`No node called ‘${nodeName}‘`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Proxy(
|
return new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
get(target, property, receiver) {
|
get(target, property, receiver) {
|
||||||
if (property === 'pairedItem') {
|
if (['pairedItem', 'itemMatching', 'item'].includes(property as string)) {
|
||||||
return (itemIndex?: number) => {
|
const pairedItemMethod = (itemIndex?: number) => {
|
||||||
if (itemIndex === undefined) {
|
if (itemIndex === undefined) {
|
||||||
|
if (property === 'itemMatching') {
|
||||||
|
throw createExpressionError('Missing item index for .itemMatching()', {
|
||||||
|
itemIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
itemIndex = that.itemIndex;
|
itemIndex = that.itemIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -762,24 +901,27 @@ export class WorkflowDataProxy {
|
||||||
const pairedItem = executionData[itemIndex].pairedItem as IPairedItemData;
|
const pairedItem = executionData[itemIndex].pairedItem as IPairedItemData;
|
||||||
|
|
||||||
if (pairedItem === undefined) {
|
if (pairedItem === undefined) {
|
||||||
throw new ExpressionError('Can’t get data for expression', {
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
|
messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
|
||||||
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘${that.activeNodeName}‘`,
|
functionOverrides: {
|
||||||
causeDetailed: `Missing pairedItem data (node ‘${that.activeNodeName}‘ did probably not supply it)`,
|
description: `To fetch the data from other nodes that this code needs, more information is needed from the node ‘<strong>${that.activeNodeName}</strong>‘`,
|
||||||
runIndex: that.runIndex,
|
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,
|
itemIndex,
|
||||||
failExecution: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!that.executeData?.source) {
|
if (!that.executeData?.source) {
|
||||||
throw new ExpressionError('Can’t get data for expression', {
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’',
|
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`,
|
description: `Apologies, this is an internal error. See details for more information`,
|
||||||
causeDetailed: `Missing sourceData (probably an internal error)`,
|
causeDetailed: `Missing sourceData (probably an internal error)`,
|
||||||
runIndex: that.runIndex,
|
|
||||||
itemIndex,
|
itemIndex,
|
||||||
failExecution: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -787,12 +929,14 @@ export class WorkflowDataProxy {
|
||||||
// graph before the current one
|
// graph before the current one
|
||||||
const parentNodes = that.workflow.getParentNodes(that.activeNodeName);
|
const parentNodes = that.workflow.getParentNodes(that.activeNodeName);
|
||||||
if (!parentNodes.includes(nodeName)) {
|
if (!parentNodes.includes(nodeName)) {
|
||||||
throw new ExpressionError('Invalid expression', {
|
throw createExpressionError('Invalid expression', {
|
||||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||||
description: `The expression uses data in node ‘${nodeName}’ but there is no path back to it. Please check this node is connected to node ‘${that.activeNodeName}’ (there can be other nodes in between).`,
|
functionOverrides: {
|
||||||
runIndex: that.runIndex,
|
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,
|
itemIndex,
|
||||||
failExecution: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -802,49 +946,11 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
return getPairedItem(nodeName, sourceData, pairedItem);
|
return getPairedItem(nodeName, sourceData, pairedItem);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
if (property === 'item') {
|
if (property === 'item') {
|
||||||
return (itemIndex?: number, branchIndex?: number, runIndex?: number) => {
|
return pairedItemMethod();
|
||||||
if (itemIndex === undefined) {
|
|
||||||
itemIndex = that.itemIndex;
|
|
||||||
branchIndex = 0;
|
|
||||||
runIndex = that.runIndex;
|
|
||||||
}
|
}
|
||||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
return pairedItemMethod;
|
||||||
|
|
||||||
if (executionData[itemIndex]) {
|
|
||||||
return executionData[itemIndex];
|
|
||||||
}
|
|
||||||
let errorMessage = '';
|
|
||||||
|
|
||||||
if (branchIndex === undefined && runIndex === undefined) {
|
|
||||||
errorMessage = `
|
|
||||||
No item found at index ${itemIndex}
|
|
||||||
(for node "${nodeName}")`;
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
if (branchIndex === undefined) {
|
|
||||||
errorMessage = `
|
|
||||||
No item found at index ${itemIndex}
|
|
||||||
in run ${runIndex || that.runIndex}
|
|
||||||
(for node "${nodeName}")`;
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
if (runIndex === undefined) {
|
|
||||||
errorMessage = `
|
|
||||||
No item found at index ${itemIndex}
|
|
||||||
of branch ${branchIndex || 0}
|
|
||||||
(for node "${nodeName}")`;
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage = `
|
|
||||||
No item found at index ${itemIndex}
|
|
||||||
of branch ${branchIndex || 0}
|
|
||||||
in run ${runIndex || that.runIndex}
|
|
||||||
(for node "${nodeName}")`;
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (property === 'first') {
|
if (property === 'first') {
|
||||||
return (branchIndex?: number, runIndex?: number) => {
|
return (branchIndex?: number, runIndex?: number) => {
|
||||||
|
@ -882,22 +988,25 @@ export class WorkflowDataProxy {
|
||||||
$input: new Proxy(
|
$input: new Proxy(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
ownKeys(target) {
|
||||||
|
return ['all', 'context', 'first', 'item', 'last', 'params'];
|
||||||
|
},
|
||||||
|
getOwnPropertyDescriptor(k) {
|
||||||
|
return {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
get(target, property, receiver) {
|
get(target, property, receiver) {
|
||||||
if (property === 'thisItem') {
|
if (property === 'item') {
|
||||||
return that.connectionInputData[that.itemIndex];
|
return that.connectionInputData[that.itemIndex];
|
||||||
}
|
}
|
||||||
if (property === 'item') {
|
|
||||||
return (itemIndex?: number) => {
|
|
||||||
if (itemIndex === undefined) itemIndex = that.itemIndex;
|
|
||||||
const result = that.connectionInputData;
|
|
||||||
if (result[itemIndex]) {
|
|
||||||
return result[itemIndex];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (property === 'first') {
|
if (property === 'first') {
|
||||||
return () => {
|
return (...args: unknown[]) => {
|
||||||
|
if (args.length) {
|
||||||
|
throw createExpressionError('$input.first() should have no arguments');
|
||||||
|
}
|
||||||
|
|
||||||
const result = that.connectionInputData;
|
const result = that.connectionInputData;
|
||||||
if (result[0]) {
|
if (result[0]) {
|
||||||
return result[0];
|
return result[0];
|
||||||
|
@ -906,7 +1015,11 @@ export class WorkflowDataProxy {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (property === 'last') {
|
if (property === 'last') {
|
||||||
return () => {
|
return (...args: unknown[]) => {
|
||||||
|
if (args.length) {
|
||||||
|
throw createExpressionError('$input.last() should have no arguments');
|
||||||
|
}
|
||||||
|
|
||||||
const result = that.connectionInputData;
|
const result = that.connectionInputData;
|
||||||
if (result.length && result[result.length - 1]) {
|
if (result.length && result[result.length - 1]) {
|
||||||
return result[result.length - 1];
|
return result[result.length - 1];
|
||||||
|
@ -923,12 +1036,37 @@ export class WorkflowDataProxy {
|
||||||
return [];
|
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);
|
return Reflect.get(target, property, receiver);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
$thisItem: that.connectionInputData[that.itemIndex],
|
|
||||||
$binary: {}, // Placeholder
|
$binary: {}, // Placeholder
|
||||||
$data: {}, // Placeholder
|
$data: {}, // Placeholder
|
||||||
$env: this.envGetter(),
|
$env: this.envGetter(),
|
||||||
|
@ -982,15 +1120,14 @@ export class WorkflowDataProxy {
|
||||||
$node: this.nodeGetter(),
|
$node: this.nodeGetter(),
|
||||||
$self: this.selfGetter(),
|
$self: this.selfGetter(),
|
||||||
$parameter: this.nodeParameterGetter(this.activeNodeName),
|
$parameter: this.nodeParameterGetter(this.activeNodeName),
|
||||||
$position: this.itemIndex,
|
$prevNode: this.prevNodeGetter(),
|
||||||
$runIndex: this.runIndex,
|
$runIndex: this.runIndex,
|
||||||
$mode: this.mode,
|
$mode: this.mode,
|
||||||
$workflow: this.workflowGetter(),
|
$workflow: this.workflowGetter(),
|
||||||
$thisRunIndex: this.runIndex,
|
$itemIndex: this.itemIndex,
|
||||||
$thisItemIndex: this.itemIndex,
|
|
||||||
$now: DateTime.now(),
|
$now: DateTime.now(),
|
||||||
$today: DateTime.now().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }),
|
$today: DateTime.now().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }),
|
||||||
$jmespath: jmespathWrapper,
|
$jmesPath: jmespathWrapper,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
DateTime,
|
DateTime,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
@ -998,6 +1135,13 @@ export class WorkflowDataProxy {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
Duration,
|
Duration,
|
||||||
...that.additionalKeys,
|
...that.additionalKeys,
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
$jmespath: jmespathWrapper,
|
||||||
|
$position: this.itemIndex,
|
||||||
|
$thisItem: that.connectionInputData[that.itemIndex],
|
||||||
|
$thisItemIndex: this.itemIndex,
|
||||||
|
$thisRunIndex: this.runIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Proxy(base, {
|
return new Proxy(base, {
|
||||||
|
|
|
@ -1,46 +1,48 @@
|
||||||
import { Workflow, WorkflowDataProxy } from '../src';
|
import { Workflow, WorkflowDataProxy } from '../src';
|
||||||
import * as Helpers from './Helpers';
|
import * as Helpers from './Helpers';
|
||||||
import { IConnections, INode, INodeExecutionData, IRunExecutionData } from '../src/Interfaces';
|
import { IConnections, IExecuteData, INode, IRunExecutionData } from '../src/Interfaces';
|
||||||
|
|
||||||
describe('WorkflowDataProxy', () => {
|
describe('WorkflowDataProxy', () => {
|
||||||
describe('test data proxy', () => {
|
describe('test data proxy', () => {
|
||||||
const nodes: INode[] = [
|
const nodes: INode[] = [
|
||||||
{
|
{
|
||||||
parameters: {},
|
|
||||||
name: 'Start',
|
name: 'Start',
|
||||||
type: 'test.set',
|
type: 'test.set',
|
||||||
|
parameters: {},
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
id: 'uuid-1',
|
id: 'uuid-1',
|
||||||
position: [100, 200],
|
position: [100, 200],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: 'Function',
|
||||||
|
type: 'test.set',
|
||||||
parameters: {
|
parameters: {
|
||||||
functionCode:
|
functionCode:
|
||||||
'// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require("luxon");\n\nconst data = [\n {\n "length": 105\n },\n {\n "length": 160\n },\n {\n "length": 121\n },\n {\n "length": 275\n },\n {\n "length": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));',
|
'// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require("luxon");\n\nconst data = [\n {\n "length": 105\n },\n {\n "length": 160\n },\n {\n "length": 121\n },\n {\n "length": 275\n },\n {\n "length": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));',
|
||||||
},
|
},
|
||||||
name: 'Function',
|
|
||||||
type: 'test.set',
|
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
id: 'uuid-2',
|
id: 'uuid-2',
|
||||||
position: [280, 200],
|
position: [280, 200],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
parameters: {
|
|
||||||
keys: {
|
|
||||||
key: [
|
|
||||||
{
|
|
||||||
currentKey: 'length',
|
|
||||||
newKey: 'data',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: 'Rename',
|
name: 'Rename',
|
||||||
type: 'test.set',
|
type: 'test.set',
|
||||||
|
parameters: {
|
||||||
|
value1: 'data',
|
||||||
|
value2: 'initialName',
|
||||||
|
},
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
id: 'uuid-3',
|
id: 'uuid-3',
|
||||||
position: [460, 200],
|
position: [460, 200],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'End',
|
||||||
|
type: 'test.set',
|
||||||
|
parameters: {},
|
||||||
|
typeVersion: 1,
|
||||||
|
id: 'uuid-4',
|
||||||
|
position: [640, 200],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const connections: IConnections = {
|
const connections: IConnections = {
|
||||||
|
@ -66,11 +68,38 @@ describe('WorkflowDataProxy', () => {
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Rename: {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'End',
|
||||||
|
type: 'main',
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const runExecutionData: IRunExecutionData = {
|
const runExecutionData: IRunExecutionData = {
|
||||||
resultData: {
|
resultData: {
|
||||||
runData: {
|
runData: {
|
||||||
|
Start: [
|
||||||
|
{
|
||||||
|
startTime: 1,
|
||||||
|
executionTime: 1,
|
||||||
|
data: {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
source: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
Function: [
|
Function: [
|
||||||
{
|
{
|
||||||
startTime: 1,
|
startTime: 1,
|
||||||
|
@ -79,24 +108,33 @@ describe('WorkflowDataProxy', () => {
|
||||||
main: [
|
main: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
json: { length: 105 },
|
json: { initialName: 105 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { length: 160 },
|
json: { initialName: 160 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { length: 121 },
|
json: { initialName: 121 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { length: 275 },
|
json: { initialName: 275 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { length: 950 },
|
json: { initialName: 950 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
source: [],
|
source: [
|
||||||
|
{
|
||||||
|
previousNode: 'Start',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Rename: [
|
Rename: [
|
||||||
|
@ -108,51 +146,109 @@ describe('WorkflowDataProxy', () => {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
json: { data: 105 },
|
json: { data: 105 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { data: 160 },
|
json: { data: 160 },
|
||||||
|
pairedItem: { item: 1 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { data: 121 },
|
json: { data: 121 },
|
||||||
|
pairedItem: { item: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { data: 275 },
|
json: { data: 275 },
|
||||||
|
pairedItem: { item: 3 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
json: { data: 950 },
|
json: { data: 950 },
|
||||||
|
pairedItem: { item: 4 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
source: [],
|
source: [
|
||||||
|
{
|
||||||
|
previousNode: 'Function',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
End: [
|
||||||
|
{
|
||||||
|
startTime: 1,
|
||||||
|
executionTime: 1,
|
||||||
|
data: {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: { data: 105 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
json: { data: 160 },
|
||||||
|
pairedItem: { item: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
json: { data: 121 },
|
||||||
|
pairedItem: { item: 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
json: { data: 275 },
|
||||||
|
pairedItem: { item: 3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
json: { data: 950 },
|
||||||
|
pairedItem: { item: 4 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
source: [
|
||||||
|
{
|
||||||
|
previousNode: 'Rename',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const renameNodeConnectionInputData: INodeExecutionData[] = [
|
|
||||||
{ json: { length: 105 } },
|
|
||||||
{ json: { length: 160 } },
|
|
||||||
{ json: { length: 121 } },
|
|
||||||
{ json: { length: 275 } },
|
|
||||||
{ json: { length: 950 } },
|
|
||||||
];
|
|
||||||
|
|
||||||
const nodeTypes = Helpers.NodeTypes();
|
const nodeTypes = Helpers.NodeTypes();
|
||||||
const workflow = new Workflow({ nodes, connections, active: false, nodeTypes });
|
const workflow = new Workflow({
|
||||||
|
id: '123',
|
||||||
|
name: 'test workflow',
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
active: false,
|
||||||
|
nodeTypes,
|
||||||
|
});
|
||||||
|
const nameLastNode = 'End';
|
||||||
|
|
||||||
|
const lastNodeConnectionInputData =
|
||||||
|
runExecutionData.resultData.runData[nameLastNode][0].data!.main[0];
|
||||||
|
|
||||||
|
const executeData: IExecuteData = {
|
||||||
|
data: runExecutionData.resultData.runData[nameLastNode][0].data!,
|
||||||
|
node: nodes.find((node) => node.name === nameLastNode) as INode,
|
||||||
|
source: {
|
||||||
|
main: runExecutionData.resultData.runData[nameLastNode][0].source!,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const dataProxy = new WorkflowDataProxy(
|
const dataProxy = new WorkflowDataProxy(
|
||||||
workflow,
|
workflow,
|
||||||
runExecutionData,
|
runExecutionData,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
'Rename',
|
nameLastNode,
|
||||||
renameNodeConnectionInputData || [],
|
lastNodeConnectionInputData || [],
|
||||||
{},
|
{},
|
||||||
'manual',
|
'manual',
|
||||||
'America/New_York',
|
'America/New_York',
|
||||||
{},
|
{},
|
||||||
|
executeData,
|
||||||
);
|
);
|
||||||
const proxy = dataProxy.getDataProxy();
|
const proxy = dataProxy.getDataProxy();
|
||||||
|
|
||||||
|
@ -162,11 +258,17 @@ describe('WorkflowDataProxy', () => {
|
||||||
test('test $("NodeName").all() length', () => {
|
test('test $("NodeName").all() length', () => {
|
||||||
expect(proxy.$('Rename').all().length).toEqual(5);
|
expect(proxy.$('Rename').all().length).toEqual(5);
|
||||||
});
|
});
|
||||||
test('test $("NodeName").item()', () => {
|
test('test $("NodeName").item', () => {
|
||||||
expect(proxy.$('Rename').item().json.data).toEqual(105);
|
expect(proxy.$('Rename').item).toEqual({ json: { data: 105 }, pairedItem: { item: 0 } });
|
||||||
});
|
});
|
||||||
test('test $("NodeName").item(2)', () => {
|
test('test $("NodeNameEarlier").item', () => {
|
||||||
expect(proxy.$('Rename').item(2).json.data).toEqual(121);
|
expect(proxy.$('Function').item).toEqual({
|
||||||
|
json: { initialName: 105 },
|
||||||
|
pairedItem: { item: 0 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('test $("NodeName").itemMatching(2)', () => {
|
||||||
|
expect(proxy.$('Rename').itemMatching(2).json.data).toEqual(121);
|
||||||
});
|
});
|
||||||
test('test $("NodeName").first()', () => {
|
test('test $("NodeName").first()', () => {
|
||||||
expect(proxy.$('Rename').first().json.data).toEqual(105);
|
expect(proxy.$('Rename').first().json.data).toEqual(105);
|
||||||
|
@ -175,26 +277,55 @@ describe('WorkflowDataProxy', () => {
|
||||||
expect(proxy.$('Rename').last().json.data).toEqual(950);
|
expect(proxy.$('Rename').last().json.data).toEqual(950);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('test $("NodeName").params', () => {
|
||||||
|
expect(proxy.$('Rename').params).toEqual({ value1: 'data', value2: 'initialName' });
|
||||||
|
});
|
||||||
|
|
||||||
test('test $input.all()', () => {
|
test('test $input.all()', () => {
|
||||||
expect(proxy.$input.all()[1].json.length).toEqual(160);
|
expect(proxy.$input.all()[1].json.data).toEqual(160);
|
||||||
});
|
});
|
||||||
test('test $input.all() length', () => {
|
test('test $input.all() length', () => {
|
||||||
expect(proxy.$input.all().length).toEqual(5);
|
expect(proxy.$input.all().length).toEqual(5);
|
||||||
});
|
});
|
||||||
test('test $input.item()', () => {
|
|
||||||
expect(proxy.$input.item().json.length).toEqual(105);
|
|
||||||
});
|
|
||||||
test('test $thisItem', () => {
|
|
||||||
expect(proxy.$thisItem.json.length).toEqual(105);
|
|
||||||
});
|
|
||||||
test('test $input.item(2)', () => {
|
|
||||||
expect(proxy.$input.item(2).json.length).toEqual(121);
|
|
||||||
});
|
|
||||||
test('test $input.first()', () => {
|
test('test $input.first()', () => {
|
||||||
expect(proxy.$input.first().json.length).toEqual(105);
|
expect(proxy.$input.first().json.data).toEqual(105);
|
||||||
});
|
});
|
||||||
test('test $input.last()', () => {
|
test('test $input.last()', () => {
|
||||||
expect(proxy.$input.last().json.length).toEqual(950);
|
expect(proxy.$input.last().json.data).toEqual(950);
|
||||||
|
});
|
||||||
|
test('test $input.item', () => {
|
||||||
|
expect(proxy.$input.item.json.data).toEqual(105);
|
||||||
|
});
|
||||||
|
test('test $thisItem', () => {
|
||||||
|
expect(proxy.$thisItem.json.data).toEqual(105);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test $binary', () => {
|
||||||
|
expect(proxy.$binary).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test $json', () => {
|
||||||
|
expect(proxy.$json).toEqual({ data: 105 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test $itemIndex', () => {
|
||||||
|
expect(proxy.$itemIndex).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test $prevNode', () => {
|
||||||
|
expect(proxy.$prevNode).toEqual({ name: 'Rename', outputIndex: 0, runIndex: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test $runIndex', () => {
|
||||||
|
expect(proxy.$runIndex).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test $workflow', () => {
|
||||||
|
expect(proxy.$workflow).toEqual({
|
||||||
|
active: false,
|
||||||
|
id: '123',
|
||||||
|
name: 'test workflow',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue