Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for form format #4000

Merged
merged 9 commits into from
Jan 24, 2025
11 changes: 6 additions & 5 deletions src/cdk/v2/destinations/http/procWorkflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ steps:

- name: prepareParams
template: |
const params = $.getCustomMappings(.message, .destination.Config.queryParams)
$.context.params = $.encodeParamsObject(params)
const params = $.getCustomMappings(.message, .destination.Config.queryParams);
$.context.params = $.encodeParamsObject(params);

- name: deduceEndPoint
template: |
Expand All @@ -55,13 +55,14 @@ steps:
- name: prepareBody
template: |
const payload = $.getCustomMappings(.message, .destination.Config.propertiesMapping);
$.context.payload = $.excludeMappedFields(payload, .destination.Config.propertiesMapping)
$.context.format === "XML" && !$.isEmptyObject($.context.payload) ? $.context.payload = {payload: $.getXMLPayload($.context.payload)} : $.context.payload = $.removeUndefinedAndNullValues($.context.payload)
$.context.payload = $.prepareBody(payload, $.context.format);

- 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;
sandeepdsvs marked this conversation as resolved.
Show resolved Hide resolved
$.context.format === "XML" ? response.body.XML = $.context.payload;
$.context.format === "FORM-URLENCODED" ? response.body.FORM = $.context.payload;
response.endpoint = $.context.endpoint;
response.headers = $.context.headers;
response.method = $.context.method;
Expand Down
73 changes: 36 additions & 37 deletions src/cdk/v2/destinations/http/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
applyCustomMappings,
isEmptyObject,
applyJSONStringTemplate,
removeUndefinedAndNullValues,
} = require('../../../../v0/util');

const getAuthHeaders = (config) => {
Expand Down Expand Up @@ -98,36 +99,6 @@ const prepareEndpoint = (message, apiUrl, pathParams) => {
return `${requestUrl}${pathParamsSubString}`;
};

const excludeMappedFields = (payload, mapping) => {
const rawPayload = { ...payload };
if (mapping) {
mapping.forEach(({ from, to }) => {
// continue when from === to
if (from === to) return;

// Remove the '$.' prefix and split the remaining string by '.'
const keys = from.replace(/^\$\./, '').split('.');
let current = rawPayload;

// Traverse to the parent of the key to be removed
keys.slice(0, -1).forEach((key) => {
if (current?.[key]) {
current = current[key];
} else {
current = null;
}
});

if (current) {
// Remove the 'from' field from input payload
delete current[keys[keys.length - 1]];
}
});
}

return rawPayload;
};

const sanitizeKey = (key) =>
key
.replace(/[^\w.-]/g, '_') // Replace invalid characters with underscores
Expand Down Expand Up @@ -157,9 +128,20 @@ const getXMLPayload = (payload) => {
const builderOptions = {
ignoreAttributes: false, // Include attributes if they exist
suppressEmptyNode: false, // Ensures that null or undefined values are not omitted
attributeNamePrefix: '@_',
};

if (Object.keys(payload).length !== 1) {
throw new ConfigurationError(
`Error: XML supports only one root key. Please update request body mappings accordingly`,
);
}
const rootKey = Object.keys(payload)[0];

const builder = new XMLBuilder(builderOptions);
return `<?xml version="1.0" encoding="UTF-8"?>${builder.build(preprocessJson(payload))}`;
const processesPayload = preprocessJson(payload);
processesPayload[rootKey]['@_xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
return `<?xml version="1.0" encoding="UTF-8"?>${builder.build(processesPayload)}`;
};

const getMergedEvents = (batch) => {
Expand All @@ -172,10 +154,28 @@ const getMergedEvents = (batch) => {
return events;
};

const metadataHeaders = (contentType) =>
contentType === 'JSON'
? { 'Content-Type': 'application/json' }
: { 'Content-Type': 'application/xml' };
const metadataHeaders = (contentType) => {
switch (contentType) {
case 'XML':
return { 'Content-Type': 'application/xml' };
case 'FORM-URLENCODED':
return { 'Content-Type': 'application/x-www-form-urlencoded' };
default:
return { 'Content-Type': 'application/json' };
}
};

const prepareBody = (payload, contentType) => {
let responseBody;
if (contentType === 'XML' && !isEmptyObject(payload)) {
responseBody = {
payload: getXMLPayload(payload),
};
} else {
responseBody = removeUndefinedAndNullValues(payload);
}
return responseBody;
};

const mergeMetadata = (batch) => batch.map((event) => event.metadata[0]);

Expand Down Expand Up @@ -234,8 +234,7 @@ module.exports = {
getCustomMappings,
encodeParamsObject,
prepareEndpoint,
excludeMappedFields,
getXMLPayload,
metadataHeaders,
prepareBody,
batchSuccessfulEvents,
};
29 changes: 22 additions & 7 deletions src/cdk/v2/destinations/http/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { encodeParamsObject, prepareEndpoint, getXMLPayload } = require('./utils');
const { encodeParamsObject, prepareEndpoint, prepareBody } = require('./utils');

const { XMLBuilder } = require('fast-xml-parser');
const jsonpath = require('rs-jsonpath');
Expand Down Expand Up @@ -71,12 +71,27 @@ describe('Utils Functions', () => {
});
});

describe('getXMLPayload', () => {
test('should generate XML payload with correct structure', () => {
const payload = { key: null };
const expectedXML = '<?xml version="1.0" encoding="UTF-8"?><key xsi:nil></key>';
const result = getXMLPayload(payload);
expect(result).toBe(expectedXML);
describe('prepareBody', () => {
test('should prepare XML payload when content type is XML', () => {
const payload = { root: { key: 'value', key2: null } };
const expectedXML =
'<?xml version="1.0" encoding="UTF-8"?><root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><key>value</key><key2 xsi:nil></key2></root>';
const result = prepareBody(payload, 'XML');
expect(result).toEqual({ payload: expectedXML });
});

test('should prepare FORM-URLENCODED payload when content type is FORM-URLENCODED', () => {
const payload = { key1: 'value1', key2: 'value2' };
const expectedFORM = { key1: 'value1', key2: 'value2' };
const result = prepareBody(payload, 'FORM-URLENCODED');
expect(result).toEqual(expectedFORM);
});

test('should return original payload without null or undefined values for other content types', () => {
const payload = { key1: 'value1', key2: null, key3: undefined, key4: 'value4' };
const expected = { key1: 'value1', key4: 'value4' };
const result = prepareBody(payload, 'JSON');
expect(result).toEqual(expected);
});
});
});
150 changes: 143 additions & 7 deletions test/integrations/destinations/http/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,27 +158,27 @@ const destinations: Destination[] = [
propertiesMapping: [
{
from: '$.event',
to: '$.event',
to: '$.body.event',
sandeepdsvs marked this conversation as resolved.
Show resolved Hide resolved
},
{
from: '$.properties.currency',
to: '$.currency',
to: '$.body.currency',
},
{
from: '$.userId',
to: '$.userId',
to: '$.body.userId',
},
{
from: '$.properties.products[*].product_id',
to: '$.properties.items[*].item_id',
to: '$.body.properties.items[*].item_id',
},
{
from: '$.properties.products[*].name',
to: '$.properties.items[*].name',
to: '$.body.properties.items[*].name',
},
{
from: '$.properties.products[*].price',
to: '$.properties.items[*].price',
to: '$.body.properties.items[*].price',
},
],
},
Expand Down Expand Up @@ -436,6 +436,142 @@ const destinations: Destination[] = [
Transformations: [],
WorkspaceID: 'test-workspace-id',
},
{
Config: {
apiUrl: 'http://abc.com/events',
auth: 'bearerTokenAuth',
bearerToken: 'test-token',
method: 'POST',
format: 'FORM-URLENCODED',
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',
},
],
},
DestinationDefinition: {
DisplayName: displayName,
ID: '123',
Name: destTypeInUpperCase,
Config: { cdkV2Enabled: true },
},
Enabled: true,
ID: '123',
Name: destTypeInUpperCase,
Transformations: [],
WorkspaceID: 'test-workspace-id',
},
{
Config: {
apiUrl: 'http://abc.com/events',
auth: 'bearerTokenAuth',
bearerToken: 'test-token',
method: 'POST',
format: 'FORM-URLENCODED',
headers: [
{
to: '$.h1',
from: "'val1'",
},
{
to: '$.h2',
from: '$.key1',
},
],
propertiesMapping: [
{
from: '$.event',
to: '$.event',
},
{
from: '$.properties.currency',
to: '$.currency',
},
{
from: '$.userId',
to: '$.userId',
},
],
},
DestinationDefinition: {
DisplayName: displayName,
ID: '123',
Name: destTypeInUpperCase,
Config: { cdkV2Enabled: true },
},
Enabled: true,
ID: '123',
Name: destTypeInUpperCase,
Transformations: [],
WorkspaceID: 'test-workspace-id',
},
{
Config: {
apiUrl: 'http://abc.com/events',
auth: 'bearerTokenAuth',
bearerToken: 'test-token',
method: 'POST',
format: 'FORM-URLENCODED',
headers: [
{
to: '$.h1',
from: "'val1'",
},
{
to: '$.h2',
from: '$.key1',
},
],
propertiesMapping: [
{
from: '$.event',
to: '$.event',
},
{
from: '$.properties.currency',
to: '$.currency',
},
{
from: '$.userId',
to: '$.userId',
},
],
},
DestinationDefinition: {
DisplayName: displayName,
ID: '123',
Name: destTypeInUpperCase,
Config: { cdkV2Enabled: true },
},
Enabled: true,
ID: '123',
Name: destTypeInUpperCase,
Transformations: [],
WorkspaceID: 'test-workspace-id',
},
];

const traits = {
Expand Down Expand Up @@ -487,7 +623,7 @@ const properties = {
const processorInstrumentationErrorStatTags = {
destType: destTypeInUpperCase,
errorCategory: 'dataValidation',
errorType: 'instrumentation',
errorType: 'configuration',
feature: 'processor',
implementation: 'cdkV2',
module: 'destination',
Expand Down
Loading
Loading