diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index efa160567d..e808224864 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -100,6 +100,7 @@ import type { WorkflowExecuteMode, CallbackManager, INodeParameters, + EnsureTypeOptions, } from 'n8n-workflow'; import { ExpressionError, @@ -2329,6 +2330,99 @@ export const validateValueAgainstSchema = ( return validationResult.newValue; }; +export function ensureType( + toType: EnsureTypeOptions, + parameterValue: any, + parameterName: string, + errorOptions?: { itemIndex?: number; runIndex?: number; nodeCause?: string }, +): string | number | boolean | object { + let returnData = parameterValue; + + if (returnData === null) { + throw new ExpressionError(`Parameter '${parameterName}' must not be null`, errorOptions); + } + + if (returnData === undefined) { + throw new ExpressionError( + `Parameter '${parameterName}' could not be 'undefined'`, + errorOptions, + ); + } + + if (['object', 'array', 'json'].includes(toType)) { + if (typeof returnData !== 'object') { + // if value is not an object and is string try to parse it, else throw an error + if (typeof returnData === 'string' && returnData.length) { + try { + const parsedValue = JSON.parse(returnData); + returnData = parsedValue; + } catch (error) { + throw new ExpressionError(`Parameter '${parameterName}' could not be parsed`, { + ...errorOptions, + description: error.message, + }); + } + } else { + throw new ExpressionError( + `Parameter '${parameterName}' must be an ${toType}, but we got '${String(parameterValue)}'`, + errorOptions, + ); + } + } else if (toType === 'json') { + // value is an object, make sure it is valid JSON + try { + JSON.stringify(returnData); + } catch (error) { + throw new ExpressionError(`Parameter '${parameterName}' is not valid JSON`, { + ...errorOptions, + description: error.message, + }); + } + } + + if (toType === 'array' && !Array.isArray(returnData)) { + // value is not an array, but has to be + throw new ExpressionError( + `Parameter '${parameterName}' must be an array, but we got object`, + errorOptions, + ); + } + } + + try { + if (toType === 'string') { + if (typeof returnData === 'object') { + returnData = JSON.stringify(returnData); + } else { + returnData = String(returnData); + } + } + + if (toType === 'number') { + returnData = Number(returnData); + if (Number.isNaN(returnData)) { + throw new ExpressionError( + `Parameter '${parameterName}' must be a number, but we got '${parameterValue}'`, + errorOptions, + ); + } + } + + if (toType === 'boolean') { + returnData = Boolean(returnData); + } + } catch (error) { + if (error instanceof ExpressionError) throw error; + + throw new ExpressionError(`Parameter '${parameterName}' could not be converted to ${toType}`, { + ...errorOptions, + description: error.message, + }); + } + + return returnData; +} + /** * Returns the requested resolved (all expressions replaced) node parameters. * @@ -2399,6 +2493,15 @@ export function getNodeParameter( returnData = extractValue(returnData, parameterName, node, nodeType, itemIndex); } + // Make sure parameter value is the type specified in the ensureType option, if needed convert it + if (options?.ensureType) { + returnData = ensureType(options.ensureType, returnData, parameterName, { + itemIndex, + runIndex, + nodeCause: node.name, + }); + } + // Validate parameter value if it has a schema defined(RMC) or validateType defined returnData = validateValueAgainstSchema( node, diff --git a/packages/core/test/NodeExecuteFunctions.test.ts b/packages/core/test/NodeExecuteFunctions.test.ts index 9300a5d9db..8ff4ca22e5 100644 --- a/packages/core/test/NodeExecuteFunctions.test.ts +++ b/packages/core/test/NodeExecuteFunctions.test.ts @@ -2,6 +2,7 @@ import type { SecureContextOptions } from 'tls'; import { cleanupParameterData, copyInputItems, + ensureType, getBinaryDataBuffer, parseIncomingMessage, parseRequestObject, @@ -25,6 +26,7 @@ import type { Workflow, WorkflowHooks, } from 'n8n-workflow'; +import { ExpressionError } from 'n8n-workflow'; import { BinaryDataService } from '@/BinaryData/BinaryData.service'; import nock from 'nock'; import { tmpdir } from 'os'; @@ -583,4 +585,81 @@ describe('NodeExecuteFunctions', () => { }, ); }); + + describe('ensureType', () => { + it('throws error for null value', () => { + expect(() => ensureType('string', null, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' must not be null"), + ); + }); + + it('throws error for undefined value', () => { + expect(() => ensureType('string', undefined, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' could not be 'undefined'"), + ); + }); + + it('returns string value without modification', () => { + const value = 'hello'; + const expectedValue = value; + const result = ensureType('string', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('returns number value without modification', () => { + const value = 42; + const expectedValue = value; + const result = ensureType('number', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('returns boolean value without modification', () => { + const value = true; + const expectedValue = value; + const result = ensureType('boolean', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('converts object to string if toType is string', () => { + const value = { name: 'John' }; + const expectedValue = JSON.stringify(value); + const result = ensureType('string', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('converts string to number if toType is number', () => { + const value = '10'; + const expectedValue = 10; + const result = ensureType('number', value, 'myParam'); + expect(result).toBe(expectedValue); + }); + + it('throws error for invalid conversion to number', () => { + const value = 'invalid'; + expect(() => ensureType('number', value, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' must be a number, but we got 'invalid'"), + ); + }); + + it('parses valid JSON string to object if toType is object', () => { + const value = '{"name": "Alice"}'; + const expectedValue = JSON.parse(value); + const result = ensureType('object', value, 'myParam'); + expect(result).toEqual(expectedValue); + }); + + it('throws error for invalid JSON string to object conversion', () => { + const value = 'invalid_json'; + expect(() => ensureType('object', value, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' could not be parsed"), + ); + }); + + it('throws error for non-array value if toType is array', () => { + const value = { name: 'Alice' }; + expect(() => ensureType('array', value, 'myParam')).toThrowError( + new ExpressionError("Parameter 'myParam' must be an array, but we got object"), + ); + }); + }); }); diff --git a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts index f67d8beeb0..5c00c3eb3f 100644 --- a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts @@ -8,7 +8,7 @@ import type { IWebhookFunctions, } from 'n8n-workflow'; -import { NodeOperationError, jsonParse } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; import get from 'lodash/get'; @@ -162,7 +162,7 @@ export function getMessageContent( }; break; case 'block': - content = jsonParse(this.getNodeParameter('blocksUi', i) as string); + content = this.getNodeParameter('blocksUi', i, {}, { ensureType: 'object' }) as IDataObject; if (includeLinkToWorkflow && Array.isArray(content.blocks)) { content.blocks.push({ diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 39add47db2..52b5e391df 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -622,8 +622,11 @@ export interface IN8nRequestOperationPaginationOffset extends IN8nRequestOperati }; } +export type EnsureTypeOptions = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'json'; export interface IGetNodeParameterOptions { contextNode?: INode; + // make sure that returned value would be of specified type, converts it if needed + ensureType?: EnsureTypeOptions; // extract value from regex, works only when parameter type is resourceLocator extractValue?: boolean; // get raw value of parameter with unresolved expressions