From 5bb5a65edf39c524b6ea11d54c308dd1034d87bd Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 15 Jul 2025 10:02:27 +0200 Subject: [PATCH] fix(AWS Bedrock Chat Model Node): Do not show issues for arbitrary model names (#17079) --- .../LmChatAwsBedrock/LmChatAwsBedrock.node.ts | 1 + .../src/components/ParameterInput.vue | 10 +- .../useNodeSettingsParameters.test.ts | 8 +- .../src/utils/nodeSettingsUtils.test.ts | 159 +++++++++++++++++- .../editor-ui/src/utils/nodeSettingsUtils.ts | 11 +- packages/workflow/src/interfaces.ts | 2 + 6 files changed, 180 insertions(+), 11 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts index fa8da0b03f..e8d764e0c0 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts @@ -60,6 +60,7 @@ export class LmChatAwsBedrock implements INodeType { displayName: 'Model', name: 'model', type: 'options', + allowArbitraryValues: true, // Hide issues when model name is specified in the expression and does not match any of the options description: 'The model which will generate the completion. Learn more.', typeOptions: { diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue index fb6fd57449..ad269933d3 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue @@ -417,17 +417,19 @@ const getIssues = computed(() => { ['options', 'multiOptions'].includes(props.parameter.type) && !remoteParameterOptionsLoading.value && remoteParameterOptionsLoadingIssues.value === null && - parameterOptions.value + parameterOptions.value && + (!isModelValueExpression.value || props.expressionEvaluated !== null) ) { - // Check if the value resolves to a valid option - // Currently it only displays an error in the node itself in + // Check if the value resolves to a valid option. + // For expressions do not validate if there is no evaluated value. + // Currently, it only displays an error in the node itself in // case the value is not valid. The workflow can still be executed // and the error is not displayed on the node in the workflow const validOptions = parameterOptions.value.map((options) => options.value); let checkValues: string[] = []; - if (!shouldSkipParamValidation(displayValue.value)) { + if (!shouldSkipParamValidation(props.parameter, displayValue.value)) { if (Array.isArray(displayValue.value)) { checkValues = checkValues.concat(displayValue.value); } else { diff --git a/packages/frontend/editor-ui/src/composables/useNodeSettingsParameters.test.ts b/packages/frontend/editor-ui/src/composables/useNodeSettingsParameters.test.ts index b1dc4b6186..83e5d14e73 100644 --- a/packages/frontend/editor-ui/src/composables/useNodeSettingsParameters.test.ts +++ b/packages/frontend/editor-ui/src/composables/useNodeSettingsParameters.test.ts @@ -14,11 +14,11 @@ import { mockedStore } from '@/__tests__/utils'; import type { INodeUi } from '@/Interface'; describe('useNodeSettingsParameters', () => { - describe('setValue', () => { - beforeEach(() => { - setActivePinia(createTestingPinia()); - }); + beforeEach(() => { + setActivePinia(createTestingPinia()); + }); + describe('setValue', () => { afterEach(() => { vi.clearAllMocks(); }); diff --git a/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.test.ts b/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.test.ts index e726644108..a0e0644c8b 100644 --- a/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.test.ts +++ b/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.test.ts @@ -6,6 +6,7 @@ import type { IDataObject, INodeTypeDescription, INodePropertyOptions, + INodeProperties, } from 'n8n-workflow'; import { updateDynamicConnections, @@ -13,8 +14,9 @@ import { nameIsParameter, formatAsExpression, parseFromExpression, + shouldSkipParamValidation, } from './nodeSettingsUtils'; -import { SWITCH_NODE_TYPE } from '@/constants'; +import { CUSTOM_API_CALL_KEY, SWITCH_NODE_TYPE } from '@/constants'; import type { INodeUi, IUpdateInformation } from '@/Interface'; describe('updateDynamicConnections', () => { @@ -396,3 +398,158 @@ describe('parseFromExpression', () => { expect(parseFromExpression({}, undefined, 'json', null, [])).toBeNull(); }); }); + +describe('shouldSkipParamValidation', () => { + describe('CUSTOM_API_CALL_KEY detection', () => { + it('should skip validation when value is CUSTOM_API_CALL_KEY', () => { + const parameter: INodeProperties = { + name: 'testParam', + displayName: 'Test Parameter', + type: 'string', + default: '', + }; + + const result = shouldSkipParamValidation(parameter, CUSTOM_API_CALL_KEY); + expect(result).toBe(true); + }); + + it('should skip validation when value is a string containing CUSTOM_API_CALL_KEY', () => { + const parameter: INodeProperties = { + name: 'testParam', + displayName: 'Test Parameter', + type: 'string', + default: '', + }; + + const valueWithKey = `some prefix ${CUSTOM_API_CALL_KEY} some suffix`; + const result = shouldSkipParamValidation(parameter, valueWithKey); + expect(result).toBe(true); + }); + + it('should not skip validation when value is a string not containing CUSTOM_API_CALL_KEY', () => { + const parameter: INodeProperties = { + name: 'testParam', + displayName: 'Test Parameter', + type: 'string', + default: '', + }; + + const result = shouldSkipParamValidation(parameter, 'regular string value'); + expect(result).toBe(false); + }); + }); + + describe('options parameter type with allowArbitraryValues', () => { + it('should skip validation for options parameter with allowArbitraryValues=true', () => { + const parameter: INodeProperties = { + name: 'optionsParam', + displayName: 'Options Parameter', + type: 'options', + options: [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: 'option2' }, + ], + allowArbitraryValues: true, + default: '', + }; + + const result = shouldSkipParamValidation(parameter, 'arbitrary_value'); + expect(result).toBe(true); + }); + + it('should not skip validation for options parameter with allowArbitraryValues=false', () => { + const parameter: INodeProperties = { + name: 'optionsParam', + displayName: 'Options Parameter', + type: 'options', + options: [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: 'option2' }, + ], + allowArbitraryValues: false, + default: '', + }; + + const result = shouldSkipParamValidation(parameter, 'arbitrary_value'); + expect(result).toBe(false); + }); + + it('should not skip validation for options parameter with allowArbitraryValues=undefined', () => { + const parameter: INodeProperties = { + name: 'optionsParam', + displayName: 'Options Parameter', + type: 'options', + options: [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: 'option2' }, + ], + default: '', + }; + + const result = shouldSkipParamValidation(parameter, 'arbitrary_value'); + expect(result).toBe(false); + }); + }); + + describe('multiOptions parameter type with allowArbitraryValues', () => { + it('should skip validation for multiOptions parameter with allowArbitraryValues=true', () => { + const parameter: INodeProperties = { + name: 'multiOptionsParam', + displayName: 'Multi Options Parameter', + type: 'multiOptions', + options: [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: 'option2' }, + ], + allowArbitraryValues: true, + default: [], + }; + + const result = shouldSkipParamValidation(parameter, ['arbitrary_value']); + expect(result).toBe(true); + }); + + it('should not skip validation for multiOptions parameter with allowArbitraryValues=false', () => { + const parameter: INodeProperties = { + name: 'multiOptionsParam', + displayName: 'Multi Options Parameter', + type: 'multiOptions', + options: [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: 'option2' }, + ], + allowArbitraryValues: false, + default: [], + }; + + const result = shouldSkipParamValidation(parameter, ['arbitrary_value']); + expect(result).toBe(false); + }); + }); + + describe('non-options parameter types', () => { + const nonOptionsParameterTypes = [ + 'string', + 'number', + 'boolean', + 'json', + 'dateTime', + 'color', + ] as Array; + + nonOptionsParameterTypes.forEach((type) => { + it(`should not skip validation for ${type} parameter type regardless of allowArbitraryValues`, () => { + const parameter: INodeProperties = { + name: 'testParam', + displayName: 'Test Parameter', + type, + allowArbitraryValues: true, + default: '', + }; + + const result = shouldSkipParamValidation(parameter, 'test_value'); + expect(result).toBe(false); + }); + }); + }); +}); diff --git a/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts b/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts index a3231664a6..4e5c9ccea6 100644 --- a/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts +++ b/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts @@ -359,6 +359,13 @@ export function parseFromExpression( return null; } -export function shouldSkipParamValidation(value: string | number | boolean | null) { - return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY); +export function shouldSkipParamValidation( + parameter: INodeProperties, + value: NodeParameterValueType, +) { + return ( + (typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY)) || + (['options', 'multiOptions'].includes(parameter.type) && + Boolean(parameter.allowArbitraryValues)) + ); } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 4643c362d5..6e5aa76a6f 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -1466,6 +1466,8 @@ export interface INodeProperties { // allows to skip validation during execution or set custom validation/casting logic inside node // inline error messages would still be shown in UI ignoreValidationDuringExecution?: boolean; + // for type: options | multiOptions – skip validation of the value (e.g. when value is not in the list and specified via expression) + allowArbitraryValues?: boolean; } export interface INodePropertyModeTypeOptions {