fix: Overhaul expression error messages related to paired item (#8765)

This commit is contained in:
Elias Meire 2024-03-21 16:59:22 +01:00 committed by GitHub
parent 079a1147d4
commit 45461c8cb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 135 additions and 102 deletions

View file

@ -22,6 +22,7 @@ import { sanitizeHtml } from '@/utils/htmlUtils';
import { useAIStore } from '@/stores/ai.store';
import { MAX_DISPLAY_DATA_SIZE } from '@/constants';
import VueMarkdown from 'vue-markdown-render';
import type { BaseTextKey } from '@/plugins/i18n';
const props = defineProps({
error: {
@ -188,6 +189,21 @@ function getErrorDescription(): string {
}),
);
}
if (props.error.context?.descriptionKey) {
const interpolate = {
nodeCause: props.error.context.nodeCause as string,
runIndex: (props.error.context.runIndex as string) ?? '0',
itemIndex: (props.error.context.itemIndex as string) ?? '0',
};
return sanitizeHtml(
i18n.baseText(
`nodeErrorView.description.${props.error.context.descriptionKey as string}` as BaseTextKey,
{ interpolate },
),
);
}
if (!props.error.context?.descriptionTemplate) {
return sanitizeHtml(props.error.description ?? '');
}
@ -397,7 +413,7 @@ function copySuccess() {
</N8nButton>
</div>
<div
v-if="error.description"
v-if="error.description || error.context?.descriptionKey"
class="node-error-view__header-description"
v-html="getErrorDescription()"
></div>
@ -661,6 +677,19 @@ function copySuccess() {
&__header-description {
padding: 0 var(--spacing-s) var(--spacing-3xs) var(--spacing-s);
font-size: var(--font-size-xs);
ul {
padding: var(--spacing-s) 0;
padding-left: var(--spacing-l);
}
code {
font-size: var(--font-size-xs);
color: var(--color-text-base);
background: var(--color-background-base);
padding: var(--spacing-5xs);
border-radius: var(--border-radius-base);
}
}
&__debugging {

View file

@ -664,6 +664,8 @@ export const ALLOWED_HTML_TAGS = [
'a',
'br',
'i',
'ul',
'li',
'em',
'small',
'details',

View file

@ -1033,6 +1033,17 @@
"nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed",
"nodeErrorView.time": "Time",
"nodeErrorView.inputPanel.previousNodeError.title": "Error running node '{nodeName}'",
"nodeErrorView.description.pairedItemInvalidInfo": "An expression here won't work because it uses <code>.item</code> and n8n can't figure out the <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/”>matching item</a>. This is because the node <strong>'{nodeCause}'</strong> returned incorrect matching information (for item {itemIndex} of run {runIndex}). <br/><br/>Try using <code>.first()</code>, <code>.last()</code> or <code>.all()[index]</code> instead of <code>.item</code>.",
"nodeErrorView.description.pairedItemNoInfo": "An expression here won't work because it uses <code>.item</code> and n8n can't figure out the <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/”>matching item</a>. The node <strong>'{nodeCause}'</strong> didn't return enough information.",
"nodeErrorView.description.pairedItemNoInfoCodeNode": "An expression here won't work because it uses <code>.item</code> and n8n can't figure out the <a href=\"https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/\">matching item</a>. You can either: <ul><li>Add the <a href=\"https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/\">missing information</a> to the node <strong>'{nodeCause}'</strong></li><li>Or use <code>.first()</code>, <code>.last()</code> or <code>.all()[index]</code> instead of <code>.item</code></li></ul>",
"nodeErrorView.description.pairedItemNoConnection": "There is no connection back to the node <strong>'{nodeCause}'</strong>, but it's used in an expression here.<br/><br/>Please wire up the node (there can be other nodes in between).",
"nodeErrorView.description.pairedItemNoConnectionCodeNode": "There is no connection back to the node <strong>'{nodeCause}'</strong>, but it's used in code here.<br/><br/>Please wire up the node (there can be other nodes in between).",
"nodeErrorView.description.noNodeExecutionData": "An expression references the node <strong>'{nodeCause}'</strong>, but it hasn't been executed yet. Either change the expression, or re-wire your workflow to make sure that node executes first.",
"nodeErrorView.description.nodeNotFound": "The node <strong>'{nodeCause}'</strong> doesn't exist, but it's used in an expression here.",
"nodeErrorView.description.noInputConnection": "This node has no input data. Please make sure this node is connected to another node.",
"nodeErrorView.description.pairedItemMultipleMatches": "An expression here won't work because it uses <code>.item</code> and n8n can't figure out the <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/”>matching item</a>. (There are multiple possible matches) <br/><br/>Try using <code>.first()</code>, <code>.last()</code> or <code>.all()[index]</code> instead of <code>.item</code> or <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/”>reference a different node</a>.",
"nodeErrorView.description.pairedItemMultipleMatchesCodeNode": "The code here won't work because it uses <code>.item</code> and n8n can't figure out the <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/”>matching item</a>. (There are multiple possible matches) <br/><br/>Try using <code>.first()</code>, <code>.last()</code> or <code>.all()[index]</code> instead of <code>.item</code> or <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/”>reference a different node</a>.",
"nodeErrorView.description.pairedItemPinned": "The <a href=”https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-errors/”>item-matching</a> data in that node may be stale. It is needed by an expression in this node that uses <code>.item</code>.",
"nodeErrorView.debugError.button": "Ask AI ✨",
"nodeErrorView.debugError.loading": "Asking AI.. ✨",
"nodeErrorView.debugError.feedback.reload": "Regenerate answer",

View file

@ -271,9 +271,11 @@ export class WorkflowDataProxy {
}
if (!that.workflow.getNode(nodeName)) {
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
@ -281,10 +283,11 @@ export class WorkflowDataProxy {
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
!that.workflow.getPinDataOfNode(nodeName)
) {
throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
throw new ExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_node_execution_data',
descriptionKey: 'noNodeExecutionData',
nodeCause: nodeName,
});
}
@ -369,7 +372,12 @@ export class WorkflowDataProxy {
name = name.toString();
if (!node) {
throw new ExpressionError(`"${nodeName}" node doesn't exist`);
throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
if (['binary', 'data', 'json'].includes(name)) {
@ -380,8 +388,7 @@ export class WorkflowDataProxy {
throw new ExpressionError('No execution data available', {
messageTemplate:
'No execution data available to expression under %%PARAMETER%%',
description:
'This node has no input data. Please make sure this node is connected to another node.',
descriptionKey: 'noInputConnection',
nodeCause: nodeName,
runIndex: that.runIndex,
itemIndex: that.itemIndex,
@ -589,9 +596,11 @@ export class WorkflowDataProxy {
const nodeName = name.toString();
if (that.workflow.getNode(nodeName) === null) {
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
@ -666,10 +675,9 @@ export class WorkflowDataProxy {
if (!context) {
context = {};
}
message = `Node ${nodeName} must be unpinned to execute`;
message = `Unpin '${nodeName}' to execute`;
context.messageTemplate = undefined;
context.description = `To fetch the data for the expression, you must unpin the node <strong>'${nodeName}'</strong> and execute the workflow again.`;
context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node <strong>'${nodeName}'</strong> and execute the workflow again.`;
context.descriptionKey = 'pairedItemPinned';
}
if (context.moreInfoLink && (pinData || isScriptingNode(nodeName, that.workflow))) {
@ -688,6 +696,48 @@ export class WorkflowDataProxy {
});
};
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) => {
return createExpressionError("Can't get data for expression", {
messageTemplate: 'Info for expression missing from previous node',
functionality: 'pairedItem',
functionOverrides: {
message: "Can't get data",
},
nodeCause,
descriptionKey: isScriptingNode(nodeCause, that.workflow)
? 'pairedItemNoInfoCodeNode'
: 'pairedItemNoInfo',
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,
});
};
const getPairedItem = (
destinationNodeName: string,
incomingSourceData: ISourceData | null,
@ -736,42 +786,15 @@ export class WorkflowDataProxy {
const source = taskData?.source ?? [];
if (pairedItem.item >= previousNodeOutputData.length) {
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%% field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Cant get data',
},
nodeCause: nodeBeforeLast,
description: `In node <strong>${nodeBeforeLast!}</strong>, output item ${
currentPairedItem.item || 0
} ${
sourceData.previousNodeRun
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
: ''
}points to an input item on node <strong>${
sourceData.previousNode
}</strong> that doesnt exist.`,
type: 'paired_item_invalid_info',
moreInfoLink: true,
throw createInvalidPairedItemError({
nodeName: sourceData.previousNode,
});
}
const itemPreviousNode: INodeExecutionData = previousNodeOutputData[pairedItem.item];
if (itemPreviousNode.pairedItem === undefined) {
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%% field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Cant get data',
},
nodeCause: sourceData.previousNode,
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node <strong>${sourceData.previousNode}</strong>`,
causeDetailed: `Missing pairedItem data (node ${sourceData.previousNode} probably didnt supply it)`,
type: 'paired_item_no_info',
moreInfoLink: true,
});
throw createMissingPairedItemError(sourceData.previousNode);
}
if (Array.isArray(itemPreviousNode.pairedItem)) {
@ -804,13 +827,17 @@ export class WorkflowDataProxy {
}
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under %%PARAMETER%%',
messageTemplate: `Multiple matching items for expression [item ${
currentPairedItem.item || 0
}]`,
functionality: 'pairedItem',
functionOverrides: {
description: `The code uses data in the node <strong>${destinationNodeName}</strong> but there is more than one matching item in that node`,
message: 'Invalid code',
message: `Multiple matching items for code [item ${currentPairedItem.item || 0}]`,
},
description: `The expression uses data in the node <strong>${destinationNodeName}</strong> but there is more than one matching item in that node`,
nodeCause: destinationNodeName,
descriptionKey: isScriptingNode(destinationNodeName, that.workflow)
? 'pairedItemMultipleMatchesCodeNode'
: 'pairedItemMultipleMatches',
type: 'paired_item_multiple_matches',
});
}
@ -833,17 +860,7 @@ export class WorkflowDataProxy {
if (itemInput >= source.length) {
if (source.length === 0) {
// A trigger node got reached, so looks like that that item can not be resolved
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under %%PARAMETER%%',
functionality: 'pairedItem',
functionOverrides: {
description: `The code uses data in the node <strong>${destinationNodeName}</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
message: 'Invalid code',
},
description: `The expression uses data in the node <strong>${destinationNodeName}</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
type: 'paired_item_no_connection',
moreInfoLink: true,
});
throw createNoConnectionError(destinationNodeName);
}
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%% field',
@ -905,24 +922,8 @@ export class WorkflowDataProxy {
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%% field',
functionality: 'pairedItem',
functionOverrides: {
message: 'Cant get data',
},
nodeCause: nodeBeforeLast,
description: `In node <strong>${nodeBeforeLast!}</strong>, output item ${
currentPairedItem.item || 0
} ${
sourceData.previousNodeRun
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
: ''
}points to an input item on node <strong>${
sourceData.previousNode
}</strong> that doesnt exist.`,
type: 'paired_item_invalid_info',
moreInfoLink: true,
throw createInvalidPairedItemError({
nodeName: sourceData.previousNode,
});
}
@ -937,7 +938,12 @@ export class WorkflowDataProxy {
const referencedNode = that.workflow.getNode(nodeName);
if (referencedNode === null) {
throw createExpressionError(`"${nodeName}" node doesn't exist`);
throw createExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
nodeCause: nodeName,
descriptionKey: 'nodeNotFound',
});
}
const ensureNodeExecutionData = () => {
@ -945,8 +951,11 @@ export class WorkflowDataProxy {
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
!that.workflow.getPinDataOfNode(nodeName)
) {
throw createExpressionError(`no data, execute "${nodeName}" node first`, {
throw createExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_node_execution_data',
descriptionKey: 'noNodeExecutionData',
nodeCause: nodeName,
});
}
@ -989,17 +998,7 @@ export class WorkflowDataProxy {
}
const parentNodes = that.workflow.getParentNodes(contextNode);
if (!parentNodes.includes(nodeName)) {
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under %%PARAMETER%%',
functionality: 'pairedItem',
functionOverrides: {
description: `The code uses data in the node <strong>${nodeName}</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
message: `No path back to node ${nodeName}`,
},
description: `The expression uses data in the node <strong>${nodeName}</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
nodeCause: nodeName,
type: 'paired_item_no_connection',
});
throw createNoConnectionError(nodeName);
}
ensureNodeExecutionData();
@ -1037,17 +1036,7 @@ export class WorkflowDataProxy {
const pairedItem = input.pairedItem as IPairedItemData;
if (pairedItem === undefined) {
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%% field',
functionality: 'pairedItem',
functionOverrides: {
description: `To fetch the data from other nodes that this code needs, more information is needed from the node <strong>${that.activeNodeName}</strong>`,
message: 'Cant 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 didnt supply it)`,
itemIndex,
});
throw createMissingPairedItemError(that.activeNodeName);
}
if (!that.executeData?.source) {

View file

@ -5,6 +5,7 @@ export interface ExpressionErrorOptions {
cause?: Error;
causeDetailed?: string;
description?: string;
descriptionKey?: string;
descriptionTemplate?: string;
functionality?: 'pairedItem';
itemIndex?: number;
@ -38,6 +39,7 @@ export class ExpressionError extends ExecutionBaseError {
const allowedKeys = [
'causeDetailed',
'descriptionTemplate',
'descriptionKey',
'functionality',
'itemIndex',
'messageTemplate',

View file

@ -162,7 +162,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('"does not exist" node doesn\'t exist');
expect(exprError.message).toEqual("Referenced node doesn't exist");
done();
}
});
@ -193,7 +193,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('no data, execute "Impossible" node first');
expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data');
done();
}
@ -221,7 +221,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('no data, execute "Impossible if" node first');
expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data');
done();
}
@ -263,7 +263,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Cant get data for expression');
expect(exprError.message).toEqual("Can't get data for expression");
expect(exprError.context.type).toEqual('paired_item_no_info');
done();
}
@ -277,7 +277,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
expect(exprError.message).toEqual('Cant get data for expression');
expect(exprError.message).toEqual("Can't get data for expression");
expect(exprError.context.type).toEqual('paired_item_invalid_info');
done();
}