mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-31 15:37:26 -08:00
feat(Code Node): Warning if pairedItem absent or could not be auto mapped (#11737)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Shireen Missi <shireen@n8n.io>
This commit is contained in:
parent
af61dbf37f
commit
3a5bd12945
|
@ -5,6 +5,7 @@ import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
import ParameterInputHint from '@/components/ParameterInputHint.vue';
|
import ParameterInputHint from '@/components/ParameterInputHint.vue';
|
||||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { isExpression, stringifyExpressionResult } from '@/utils/expressions';
|
import { isExpression, stringifyExpressionResult } from '@/utils/expressions';
|
||||||
import type { AssignmentValue, INodeProperties, Result } from 'n8n-workflow';
|
import type { AssignmentValue, INodeProperties, Result } from 'n8n-workflow';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
@ -101,7 +102,12 @@ const hint = computed(() => {
|
||||||
result = { ok: false, error };
|
result = { ok: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
return stringifyExpressionResult(result);
|
const hasRunData =
|
||||||
|
!!useWorkflowsStore().workflowExecutionData?.data?.resultData?.runData[
|
||||||
|
ndvStore.activeNode?.name ?? ''
|
||||||
|
];
|
||||||
|
|
||||||
|
return stringifyExpressionResult(result, hasRunData);
|
||||||
});
|
});
|
||||||
|
|
||||||
const highlightHint = computed(() => Boolean(hint.value && ndvStore.getHoveringItem));
|
const highlightHint = computed(() => Boolean(hint.value && ndvStore.getHoveringItem));
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type { EventBus } from 'n8n-design-system/utils';
|
||||||
import { createEventBus } from 'n8n-design-system/utils';
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parameter: INodeProperties;
|
parameter: INodeProperties;
|
||||||
|
@ -144,7 +145,11 @@ const evaluatedExpressionValue = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const evaluatedExpressionString = computed(() => {
|
const evaluatedExpressionString = computed(() => {
|
||||||
return stringifyExpressionResult(evaluatedExpression.value);
|
const hasRunData =
|
||||||
|
!!useWorkflowsStore().workflowExecutionData?.data?.resultData?.runData[
|
||||||
|
ndvStore.activeNode?.name ?? ''
|
||||||
|
];
|
||||||
|
return stringifyExpressionResult(evaluatedExpression.value, hasRunData);
|
||||||
});
|
});
|
||||||
|
|
||||||
const expressionOutput = computed(() => {
|
const expressionOutput = computed(() => {
|
||||||
|
|
|
@ -305,7 +305,11 @@ export const useExpressionEditor = ({
|
||||||
result.resolved = workflowHelpers.resolveExpression('=' + resolvable, undefined, opts);
|
result.resolved = workflowHelpers.resolveExpression('=' + resolvable, undefined, opts);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
result.resolved = `[${getExpressionErrorMessage(error)}]`;
|
const hasRunData =
|
||||||
|
!!workflowsStore.workflowExecutionData?.data?.resultData?.runData[
|
||||||
|
ndvStore.activeNode?.name ?? ''
|
||||||
|
];
|
||||||
|
result.resolved = `[${getExpressionErrorMessage(error, hasRunData)}]`;
|
||||||
result.error = true;
|
result.error = true;
|
||||||
result.fullError = error;
|
result.fullError = error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -834,6 +834,7 @@
|
||||||
"expressionModalInput.pairedItemConnectionError": "No path back to node",
|
"expressionModalInput.pairedItemConnectionError": "No path back to node",
|
||||||
"expressionModalInput.pairedItemInvalidPinnedError": "Unpin node ‘{node}’ and execute",
|
"expressionModalInput.pairedItemInvalidPinnedError": "Unpin node ‘{node}’ and execute",
|
||||||
"expressionModalInput.pairedItemError": "Can’t determine which item to use",
|
"expressionModalInput.pairedItemError": "Can’t determine which item to use",
|
||||||
|
"expressionModalInput.pairedItemError.noRunData": "Can't determine which item to use - execute node for more info",
|
||||||
"fixedCollectionParameter.choose": "Choose...",
|
"fixedCollectionParameter.choose": "Choose...",
|
||||||
"fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist",
|
"fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist",
|
||||||
"fixedCollectionParameter.deleteItem": "Delete item",
|
"fixedCollectionParameter.deleteItem": "Delete item",
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const getResolvableState = (error: unknown, ignoreError = false): Resolva
|
||||||
return 'invalid';
|
return 'invalid';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getExpressionErrorMessage = (error: Error): string => {
|
export const getExpressionErrorMessage = (error: Error, nodeHasRunData = false): string => {
|
||||||
if (isNoExecDataExpressionError(error) || isPairedItemIntermediateNodesError(error)) {
|
if (isNoExecDataExpressionError(error) || isPairedItemIntermediateNodesError(error)) {
|
||||||
return i18n.baseText('expressionModalInput.noExecutionData');
|
return i18n.baseText('expressionModalInput.noExecutionData');
|
||||||
}
|
}
|
||||||
|
@ -109,19 +109,24 @@ export const getExpressionErrorMessage = (error: Error): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAnyPairedItemError(error)) {
|
if (isAnyPairedItemError(error)) {
|
||||||
return i18n.baseText('expressionModalInput.pairedItemError');
|
return nodeHasRunData
|
||||||
|
? i18n.baseText('expressionModalInput.pairedItemError')
|
||||||
|
: i18n.baseText('expressionModalInput.pairedItemError.noRunData');
|
||||||
}
|
}
|
||||||
|
|
||||||
return error.message;
|
return error.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stringifyExpressionResult = (result: Result<unknown, Error>): string => {
|
export const stringifyExpressionResult = (
|
||||||
|
result: Result<unknown, Error>,
|
||||||
|
nodeHasRunData = false,
|
||||||
|
): string => {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (getResolvableState(result.error) !== 'invalid') {
|
if (getResolvableState(result.error) !== 'invalid') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `[${i18n.baseText('parameterInput.error')}: ${getExpressionErrorMessage(result.error)}]`;
|
return `[${i18n.baseText('parameterInput.error')}: ${getExpressionErrorMessage(result.error, nodeHasRunData)}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.result === null) {
|
if (result.result === null) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { JavaScriptSandbox } from './JavaScriptSandbox';
|
||||||
import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox';
|
import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox';
|
||||||
import { PythonSandbox } from './PythonSandbox';
|
import { PythonSandbox } from './PythonSandbox';
|
||||||
import { getSandboxContext } from './Sandbox';
|
import { getSandboxContext } from './Sandbox';
|
||||||
import { standardizeOutput } from './utils';
|
import { addPostExecutionWarning, standardizeOutput } from './utils';
|
||||||
|
|
||||||
const { CODE_ENABLE_STDOUT } = process.env;
|
const { CODE_ENABLE_STDOUT } = process.env;
|
||||||
|
|
||||||
|
@ -142,6 +142,8 @@ export class Code implements INodeType {
|
||||||
return sandbox;
|
return sandbox;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputDataItems = this.getInputData();
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// runOnceForAllItems
|
// runOnceForAllItems
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -163,7 +165,7 @@ export class Code implements INodeType {
|
||||||
standardizeOutput(item.json);
|
standardizeOutput(item.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [items];
|
return addPostExecutionWarning(items, inputDataItems?.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -172,9 +174,7 @@ export class Code implements INodeType {
|
||||||
|
|
||||||
const returnData: INodeExecutionData[] = [];
|
const returnData: INodeExecutionData[] = [];
|
||||||
|
|
||||||
const items = this.getInputData();
|
for (let index = 0; index < inputDataItems.length; index++) {
|
||||||
|
|
||||||
for (let index = 0; index < items.length; index++) {
|
|
||||||
const sandbox = getSandbox(index);
|
const sandbox = getSandbox(index);
|
||||||
let result: INodeExecutionData | undefined;
|
let result: INodeExecutionData | undefined;
|
||||||
try {
|
try {
|
||||||
|
@ -201,6 +201,6 @@ export class Code implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [returnData];
|
return addPostExecutionWarning(returnData, inputDataItems?.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,8 @@ describe('Code Node unit test', () => {
|
||||||
jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input);
|
jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input);
|
||||||
|
|
||||||
const output = await node.execute.call(thisArg);
|
const output = await node.execute.call(thisArg);
|
||||||
expect(output).toEqual([expected]);
|
|
||||||
|
expect([...output]).toEqual([expected]);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -109,7 +110,7 @@ describe('Code Node unit test', () => {
|
||||||
jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input);
|
jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input);
|
||||||
|
|
||||||
const output = await node.execute.call(thisArg);
|
const output = await node.execute.call(thisArg);
|
||||||
expect(output).toEqual([[{ json: expected?.json, pairedItem: { item: 0 } }]]);
|
expect([...output]).toEqual([[{ json: expected?.json, pairedItem: { item: 0 } }]]);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
48
packages/nodes-base/nodes/Code/test/utils.test.ts
Normal file
48
packages/nodes-base/nodes/Code/test/utils.test.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import type { INodeExecutionData } from 'n8n-workflow';
|
||||||
|
import { NodeExecutionOutput } from 'n8n-workflow';
|
||||||
|
import { addPostExecutionWarning } from '../utils';
|
||||||
|
|
||||||
|
describe('addPostExecutionWarning', () => {
|
||||||
|
const inputItemsLength = 2;
|
||||||
|
|
||||||
|
it('should return a NodeExecutionOutput warning when returnData length differs from inputItemsLength', () => {
|
||||||
|
const returnData: INodeExecutionData[] = [{ json: {}, pairedItem: 0 }];
|
||||||
|
|
||||||
|
const result = addPostExecutionWarning(returnData, inputItemsLength);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(NodeExecutionOutput);
|
||||||
|
expect((result as NodeExecutionOutput)?.getHints()).toEqual([
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'To make sure expressions after this node work, return the input items that produced each output item. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
|
||||||
|
location: 'outputPane',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a NodeExecutionOutput warning when any item has undefined pairedItem', () => {
|
||||||
|
const returnData: INodeExecutionData[] = [{ json: {}, pairedItem: 0 }, { json: {} }];
|
||||||
|
|
||||||
|
const result = addPostExecutionWarning(returnData, inputItemsLength);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(NodeExecutionOutput);
|
||||||
|
expect((result as NodeExecutionOutput)?.getHints()).toEqual([
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'To make sure expressions after this node work, return the input items that produced each output item. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
|
||||||
|
location: 'outputPane',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return returnData array when all items match inputItemsLength and have defined pairedItem', () => {
|
||||||
|
const returnData: INodeExecutionData[] = [
|
||||||
|
{ json: {}, pairedItem: 0 },
|
||||||
|
{ json: {}, pairedItem: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = addPostExecutionWarning(returnData, inputItemsLength);
|
||||||
|
|
||||||
|
expect(result).toEqual([returnData]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { INodeExecutionData, IDataObject } from 'n8n-workflow';
|
||||||
|
import { NodeExecutionOutput } from 'n8n-workflow';
|
||||||
|
|
||||||
export function isObject(maybe: unknown): maybe is { [key: string]: unknown } {
|
export function isObject(maybe: unknown): maybe is { [key: string]: unknown } {
|
||||||
return (
|
return (
|
||||||
|
@ -36,3 +37,26 @@ export function standardizeOutput(output: IDataObject) {
|
||||||
standardizeOutputRecursive(output);
|
standardizeOutputRecursive(output);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const addPostExecutionWarning = (
|
||||||
|
returnData: INodeExecutionData[],
|
||||||
|
inputItemsLength: number,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
returnData.length !== inputItemsLength ||
|
||||||
|
returnData.some((item) => item.pairedItem === undefined)
|
||||||
|
) {
|
||||||
|
return new NodeExecutionOutput(
|
||||||
|
[returnData],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'To make sure expressions after this node work, return the input items that produced each output item. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
|
||||||
|
location: 'outputPane',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [returnData];
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue