From 944a40246481483700f7f5401f015a4e28e6ea2f Mon Sep 17 00:00:00 2001 From: AASHISH MALIK <=> Date: Mon, 9 Dec 2024 02:18:52 +0530 Subject: [PATCH] feat: http form format --- package-lock.json | 10 ++++ package.json | 1 + .../v2/destinations/http/procWorkflow.yaml | 3 +- src/cdk/v2/destinations/http/utils.js | 14 +++++ test/integrations/destinations/http/common.ts | 60 +++++++++++++++++++ .../http/processor/configuration.ts | 54 ++++++++++++++++- 6 files changed, 140 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40033e278b..e79cd2a033 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "parse-static-imports": "^1.1.0", "prom-client": "^14.2.0", "qs": "^6.11.1", + "querystring": "^0.2.1", "rs-jsonpath": "^1.1.2", "set-value": "^4.1.0", "sha256": "^0.2.0", @@ -19513,6 +19514,15 @@ "node": ">=0.10.0" } }, + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "funding": [ diff --git a/package.json b/package.json index e0b17a4f47..22dd50f257 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "parse-static-imports": "^1.1.0", "prom-client": "^14.2.0", "qs": "^6.11.1", + "querystring": "^0.2.1", "rs-jsonpath": "^1.1.2", "set-value": "^4.1.0", "sha256": "^0.2.0", diff --git a/src/cdk/v2/destinations/http/procWorkflow.yaml b/src/cdk/v2/destinations/http/procWorkflow.yaml index 080dcdd80a..ddf1ab78c6 100644 --- a/src/cdk/v2/destinations/http/procWorkflow.yaml +++ b/src/cdk/v2/destinations/http/procWorkflow.yaml @@ -54,11 +54,12 @@ steps: const payload = $.getCustomMappings(.message, .destination.Config.propertiesMapping); $.context.payload = $.removeUndefinedAndNullValues($.excludeMappedFields(payload, .destination.Config.propertiesMapping)) $.context.format === "XML" && !$.isEmptyObject($.context.payload) ? $.context.payload = {payload: $.getXMLPayload($.context.payload)}; + $.context.format === "FORM" && !$.isEmptyObject($.context.payload) ? $.context.payload = {payload: $.getFORMPayload($.context.payload)}; - name: buildResponseForProcessTransformation template: | const response = $.defaultRequestConfig(); - $.context.format === "JSON" ? response.body.JSON = $.context.payload: response.body.XML = $.context.payload; + $.context.format === "JSON" ? response.body.JSON = $.context.payload : $.context.format === "XML" ? response.body.XML = $.context.payload : response.body.FORM = $.context.payload ; response.endpoint = $.context.endpoint; response.headers = $.context.headers; response.method = $.context.method; diff --git a/src/cdk/v2/destinations/http/utils.js b/src/cdk/v2/destinations/http/utils.js index 355eb03487..3923b9c135 100644 --- a/src/cdk/v2/destinations/http/utils.js +++ b/src/cdk/v2/destinations/http/utils.js @@ -1,6 +1,7 @@ const { toXML } = require('jstoxml'); const { groupBy } = require('lodash'); const { createHash } = require('crypto'); +const querystring = require('querystring'); const { ConfigurationError } = require('@rudderstack/integrations-lib'); const { BatchUtils } = require('@rudderstack/workflow-engine'); const { @@ -146,6 +147,18 @@ const batchSuccessfulEvents = (events, batchSize) => { return response; }; +/** + * Converts JSON payload to application/x-www-form-urlencoded format. + * @param {Object} payload - The JSON payload to be converted. + * @returns {string} - The payload in application/x-www-form-urlencoded format. + */ +const getFORMPayload = (payload) => { + if (!payload) { + throw new ConfigurationError('Invalid payload for FORM format'); + } + return querystring.stringify(payload); +}; + module.exports = { getAuthHeaders, getCustomMappings, @@ -153,4 +166,5 @@ module.exports = { excludeMappedFields, getXMLPayload, batchSuccessfulEvents, + getFORMPayload, }; diff --git a/test/integrations/destinations/http/common.ts b/test/integrations/destinations/http/common.ts index f0c8bc8a33..563464dfb2 100644 --- a/test/integrations/destinations/http/common.ts +++ b/test/integrations/destinations/http/common.ts @@ -278,6 +278,66 @@ const destinations: Destination[] = [ Transformations: [], WorkspaceID: 'test-workspace-id', }, + { + Config: { + apiUrl: 'http://abc.com/events', + auth: 'bearerTokenAuth', + bearerToken: 'test-token', + method: 'POST', + format: 'FORM', + headers: [ + { + to: '$.h1', + from: "'val1'", + }, + { + to: '$.h2', + from: '$.key1', + }, + { + to: "$.'content-type'", + from: "'application/json'", + }, + ], + propertiesMapping: [ + { + from: '$.event', + to: '$.event', + }, + { + from: '$.properties.currency', + to: '$.currency', + }, + { + from: '$.userId', + to: '$.userId', + }, + { + from: '$.properties.products[*].product_id', + to: '$.properties.items[*].item_id', + }, + { + from: '$.properties.products[*].name', + to: '$.properties.items[*].name', + }, + { + from: '$.properties.products[*].price', + to: '$.properties.items[*].price', + }, + ], + }, + DestinationDefinition: { + DisplayName: displayName, + ID: '123', + Name: destTypeInUpperCase, + Config: { cdkV2Enabled: true }, + }, + Enabled: true, + ID: '123', + Name: destTypeInUpperCase, + Transformations: [], + WorkspaceID: 'test-workspace-id', + }, ]; const traits = { diff --git a/test/integrations/destinations/http/processor/configuration.ts b/test/integrations/destinations/http/processor/configuration.ts index b493a236ee..e6a61c9bf2 100644 --- a/test/integrations/destinations/http/processor/configuration.ts +++ b/test/integrations/destinations/http/processor/configuration.ts @@ -1,6 +1,6 @@ import { ProcessorTestData } from '../../../testTypes'; import { generateMetadata, transformResultBuilder } from '../../../testUtils'; -import { destType, destinations, properties, traits } from '../common'; +import { destinations, destType, properties, traits } from '../common'; export const configuration: ProcessorTestData[] = [ { @@ -203,4 +203,56 @@ export const configuration: ProcessorTestData[] = [ }, }, }, + { + id: 'http-configuration-test-4', + name: destType, + description: + 'Track call with bearer token, form format, post method, additional headers and properties mapping', + scenario: 'Business', + successCriteria: + 'Response should be in form format with post method, headers and properties mapping', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: destinations[7], + message: { + type: 'track', + userId: 'userId123', + event: 'Order Completed', + properties, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint: destinations[7].Config.apiUrl, + headers: { + Authorization: 'Bearer test-token', + h1: 'val1', + 'content-type': 'application/json', + }, + FORM: { + payload: 'event=Order%20Completed¤cy=USD&userId=userId123&properties=', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, ];