From b615e51f1319130c3a0f918e882aa3ae3bf5a4f2 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:05:25 +0300 Subject: [PATCH] feat: Option to skip validation in getNodeParameter (#14726) --- .../__tests__/execute-context.test.ts | 13 ++++++ .../node-execution-context.ts | 2 + .../test/v2/node/record/update.test.ts | 42 +++++++++++++++++++ .../v2/actions/record/update.operation.ts | 18 ++++++-- packages/workflow/src/Interfaces.ts | 2 + 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts index 3f71e2fa6f..8ff738d1ef 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts @@ -18,6 +18,7 @@ import { ApplicationError, ExpressionError, NodeConnectionTypes } from 'n8n-work import { describeCommonTests } from './shared-tests'; import { ExecuteContext } from '../execute-context'; +import * as validateUtil from '../utils/validate-value-against-schema'; describe('ExecuteContext', () => { const testCredentialType = 'testCredential'; @@ -177,6 +178,18 @@ describe('ExecuteContext', () => { const parameter = executeContext.getNodeParameter('testParameter', 0); expect(parameter).toEqual([{ name: undefined, value: undefined }]); }); + + it('should not validate parameter if skipValidation in options', () => { + const validateSpy = jest.spyOn(validateUtil, 'validateValueAgainstSchema'); + + executeContext.getNodeParameter('testParameter', 0, '', { + skipValidation: true, + }); + + expect(validateSpy).not.toHaveBeenCalled(); + + validateSpy.mockRestore(); + }); }); describe('getCredentials', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts index 00e84ee3f3..7b5e94d8a6 100644 --- a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts @@ -406,6 +406,8 @@ export abstract class NodeExecutionContext implements Omit { }); describe('Test AirtableV2, update operation', () => { + let mockExecuteFunctions: MockProxy; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should skip validation if typecast option is true', async () => { + mockExecuteFunctions = mock(); + mockExecuteFunctions.helpers.constructExecutionMetaData = jest.fn(() => []); + mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => { + if (key === 'columns.mappingMode') { + return 'defineBelow'; + } + if (key === 'columns.matchingColumns') { + return ['id']; + } + if (key === 'options') { + return { + typecast: true, + }; + } + if (key === 'columns.value') { + return { + id: 'recXXX', + field1: 'foo 1', + field2: 'bar 1', + }; + } + return undefined; + }); + + await update.execute.call(mockExecuteFunctions, [{ json: {} }], 'base', 'table'); + + expect(mockExecuteFunctions.getNodeParameter).toHaveBeenCalledWith('columns.value', 0, [], { + skipValidation: true, + }); + }); + it('should update a record by id, autoMapInputData', async () => { const nodeParameters = { operation: 'update', diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts index de3a56ead8..4db8f76354 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts @@ -80,6 +80,7 @@ export async function execute( try { const records: UpdateRecord[] = []; const options = this.getNodeParameter('options', i, {}); + const typecast = options.typecast ? true : false; if (dataMode === 'autoMapInputData') { if (columnsToMatchOn.includes('id')) { @@ -107,11 +108,22 @@ export async function execute( } if (dataMode === 'defineBelow') { + const getNodeParameterOptions = typecast ? { skipValidation: true } : undefined; if (columnsToMatchOn.includes('id')) { - const { id, ...fields } = this.getNodeParameter('columns.value', i, []) as IDataObject; + const { id, ...fields } = this.getNodeParameter( + 'columns.value', + i, + [], + getNodeParameterOptions, + ) as IDataObject; records.push({ id: id as string, fields }); } else { - const fields = this.getNodeParameter('columns.value', i, []) as IDataObject; + const fields = this.getNodeParameter( + 'columns.value', + i, + [], + getNodeParameterOptions, + ) as IDataObject; const matches = findMatches( tableData, @@ -127,7 +139,7 @@ export async function execute( } } - const body: IDataObject = { typecast: options.typecast ? true : false }; + const body: IDataObject = { typecast }; const responseData = await batchUpdate.call(this, endpoint, body, records); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 38f168db56..842626bbe3 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -571,6 +571,8 @@ export interface IGetNodeParameterOptions { extractValue?: boolean; // get raw value of parameter with unresolved expressions rawExpressions?: boolean; + // skip validation of parameter + skipValidation?: boolean; } namespace ExecuteFunctions {