From b29cf9a2492a444cb1dd72e74c9ed1d8722bbc5a Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 31 Mar 2023 19:31:03 +0300 Subject: [PATCH] fix(HTTP Request Node): Support for dot notation in JSON body --- .../nodes/HttpRequest/GenericFunctions.ts | 26 +++++++++++++ .../nodes/HttpRequest/HttpRequest.node.ts | 3 +- .../HttpRequest/V3/HttpRequestV3.node.ts | 23 +++++++++--- .../HttpRequest/test/utils/utils.test.ts | 37 +++++++++++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 packages/nodes-base/nodes/HttpRequest/test/utils/utils.test.ts diff --git a/packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts b/packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts index 19ec21e803..43bd1d0e21 100644 --- a/packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts @@ -1,6 +1,10 @@ import type { IDataObject, INodeExecutionData, IOAuth2Options } from 'n8n-workflow'; import type { OptionsWithUri } from 'request-promise-native'; +import set from 'lodash.set'; + +export type BodyParameter = { name: string; value: string }; + export type IAuthDataSanitizeKeys = { [key: string]: string[]; }; @@ -130,3 +134,25 @@ export const binaryContentTypes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/x-7z-compressed', ]; + +export type BodyParametersReducer = ( + acc: IDataObject, + cur: { name: string; value: string }, +) => IDataObject; + +export const prepareRequestBody = ( + parameters: BodyParameter[], + bodyType: string, + version: number, + defaultReducer: BodyParametersReducer, +) => { + if (bodyType === 'json' && version >= 4) { + return parameters.reduce((acc, entry) => { + const value = entry.value; + set(acc, entry.name, value); + return acc; + }, {} as IDataObject); + } else { + return parameters.reduce(defaultReducer, {}); + } +}; diff --git a/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts index f517531ce7..34394d555d 100644 --- a/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts @@ -14,13 +14,14 @@ export class HttpRequest extends VersionedNodeType { group: ['output'], subtitle: '={{$parameter["requestMethod"] + ": " + $parameter["url"]}}', description: 'Makes an HTTP request and returns the response data', - defaultVersion: 3, + defaultVersion: 4, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new HttpRequestV1(baseDescription), 2: new HttpRequestV2(baseDescription), 3: new HttpRequestV3(baseDescription), + 4: new HttpRequestV3(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 791927f979..6c0b327f60 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -10,14 +10,17 @@ import type { INodeTypeDescription, JsonObject, } from 'n8n-workflow'; + import { BINARY_ENCODING, jsonParse, NodeApiError, NodeOperationError, sleep } from 'n8n-workflow'; import type { OptionsWithUri } from 'request-promise-native'; -import type { IAuthDataSanitizeKeys } from '../GenericFunctions'; +import type { BodyParameter, IAuthDataSanitizeKeys } from '../GenericFunctions'; + import { binaryContentTypes, getOAuth2AdditionalParameters, + prepareRequestBody, replaceNullValues, sanitizeUiMessage, } from '../GenericFunctions'; @@ -35,7 +38,7 @@ export class HttpRequestV3 implements INodeType { this.description = { ...baseDescription, subtitle: '={{$parameter["method"] + ": " + $parameter["url"]}}', - version: 3, + version: [3, 4], defaults: { name: 'HTTP Request', color: '#2200DD', @@ -879,6 +882,7 @@ export class HttpRequestV3 implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); + const nodeVersion = this.getNode().typeVersion; const fullResponseProperties = ['body', 'headers', 'statusCode', 'statusMessage']; @@ -960,9 +964,11 @@ export class HttpRequestV3 implements INodeType { const sendBody = this.getNodeParameter('sendBody', itemIndex, false) as boolean; const bodyContentType = this.getNodeParameter('contentType', itemIndex, '') as string; const specifyBody = this.getNodeParameter('specifyBody', itemIndex, '') as string; - const bodyParameters = this.getNodeParameter('bodyParameters.parameters', itemIndex, []) as [ - { name: string; value: string }, - ]; + const bodyParameters = this.getNodeParameter( + 'bodyParameters.parameters', + itemIndex, + [], + ) as BodyParameter[]; const jsonBodyParameter = this.getNodeParameter('jsonBody', itemIndex, '') as string; const body = this.getNodeParameter('body', itemIndex, '') as string; @@ -1094,7 +1100,12 @@ export class HttpRequestV3 implements INodeType { // Get parameters defined in the UI if (sendBody && bodyParameters) { if (specifyBody === 'keypair' || bodyContentType === 'multipart-form-data') { - requestOptions.body = bodyParameters.reduce(parametersToKeyValue, {}); + requestOptions.body = prepareRequestBody( + bodyParameters, + bodyContentType, + nodeVersion, + parametersToKeyValue, + ); } else if (specifyBody === 'json') { // body is specified using JSON if (typeof jsonBodyParameter !== 'object' && jsonBodyParameter !== null) { diff --git a/packages/nodes-base/nodes/HttpRequest/test/utils/utils.test.ts b/packages/nodes-base/nodes/HttpRequest/test/utils/utils.test.ts new file mode 100644 index 0000000000..db08844120 --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/test/utils/utils.test.ts @@ -0,0 +1,37 @@ +import { prepareRequestBody } from '../../GenericFunctions'; +import type { BodyParameter, BodyParametersReducer } from '../../GenericFunctions'; + +describe('HTTP Node Utils, prepareRequestBody', () => { + it('should call default reducer', () => { + const bodyParameters: BodyParameter[] = [ + { + name: 'foo.bar', + value: 'baz', + }, + ]; + const defaultReducer: BodyParametersReducer = jest.fn(); + + prepareRequestBody(bodyParameters, 'json', 3, defaultReducer); + + expect(defaultReducer).toBeCalledTimes(1); + expect(defaultReducer).toBeCalledWith({}, { name: 'foo.bar', value: 'baz' }, 0, [ + { name: 'foo.bar', value: 'baz' }, + ]); + }); + + it('should call process dot notations', () => { + const bodyParameters: BodyParameter[] = [ + { + name: 'foo.bar.spam', + value: 'baz', + }, + ]; + const defaultReducer: BodyParametersReducer = jest.fn(); + + const result = prepareRequestBody(bodyParameters, 'json', 4, defaultReducer); + + expect(defaultReducer).toBeCalledTimes(0); + expect(result).toBeDefined(); + expect(result).toEqual({ foo: { bar: { spam: 'baz' } } }); + }); +});