Skip to content

Commit

Permalink
feat: add support for form format (#4000)
Browse files Browse the repository at this point in the history
* feat(http): add support for form format

* feat(http): updated form support logic

* feat(http): adding namespace tag for xml

* chore: addressed review comments

* feat: remove template support for apiUrl. Only pathParams is valid

* chore: updated regex to resolve sonal issue

* chore: updated regex to resolve sonal issue

* chore: updated regex to resolve sonal issue

* chore: updated regex to resolve sonal issue
  • Loading branch information
sandeepdsvs authored Jan 24, 2025
1 parent d050117 commit 1fc15bf
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 84 deletions.
12 changes: 6 additions & 6 deletions src/cdk/v2/destinations/http/procWorkflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ steps:
- name: deduceBodyFormat
template: |
$.context.format = .destination.Config.format ?? 'JSON';
const format = .destination.Config.format ?? 'JSON';
$.context.format = $.CONTENT_TYPES_MAP[format];
- name: buildHeaders
template: |
Expand All @@ -45,8 +46,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 +56,12 @@ 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;
response.body[$.context.format] = $.context.payload;
response.endpoint = $.context.endpoint;
response.headers = $.context.headers;
response.method = $.context.method;
Expand Down
90 changes: 45 additions & 45 deletions src/cdk/v2/destinations/http/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ const {
base64Convertor,
applyCustomMappings,
isEmptyObject,
applyJSONStringTemplate,
removeUndefinedAndNullValues,
} = require('../../../../v0/util');

const CONTENT_TYPES_MAP = {
JSON: 'JSON',
XML: 'XML',
FORM: 'FORM',
};

const getAuthHeaders = (config) => {
let headers;
switch (config.auth) {
Expand Down Expand Up @@ -85,49 +91,14 @@ const getPathParamsSubString = (message, pathParamsArray) => {
};

const prepareEndpoint = (message, apiUrl, pathParams) => {
let requestUrl;
try {
requestUrl = applyJSONStringTemplate(message, `\`${apiUrl}\``);
} catch (e) {
throw new ConfigurationError(`Error in api url template: ${e.message}`);
}
if (!Array.isArray(pathParams)) {
return requestUrl;
return apiUrl;
}
const requestUrl = apiUrl.replace(/\/{1,10}$/, '');
const pathParamsSubString = getPathParamsSubString(message, 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 CONTENT_TYPES_MAP.XML:
return { 'Content-Type': 'application/xml' };
case CONTENT_TYPES_MAP.FORM:
return { 'Content-Type': 'application/x-www-form-urlencoded' };
default:
return { 'Content-Type': 'application/json' };
}
};

const prepareBody = (payload, contentType) => {
let responseBody;
if (contentType === CONTENT_TYPES_MAP.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 @@ -230,12 +230,12 @@ const batchSuccessfulEvents = (events, batchSize) => {
};

module.exports = {
CONTENT_TYPES_MAP,
getAuthHeaders,
getCustomMappings,
encodeParamsObject,
prepareEndpoint,
excludeMappedFields,
getXMLPayload,
metadataHeaders,
prepareBody,
batchSuccessfulEvents,
};
41 changes: 28 additions & 13 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 All @@ -21,12 +21,12 @@ describe('Utils Functions', () => {
describe('prepareEndpoint', () => {
test('should replace template variables in API URL', () => {
const message = { id: 123 };
const apiUrl = 'https://api.example.com/resource/${$.id}';
expect(prepareEndpoint(message, apiUrl, [])).toBe('https://api.example.com/resource/123');
const apiUrl = 'https://api.example.com/resource/';
expect(prepareEndpoint(message, apiUrl, [])).toBe('https://api.example.com/resource');
});
test('should replace template variables in API URL and add path params', () => {
const message = { id: 123, p2: 'P2' };
const apiUrl = 'https://api.example.com/resource/${$.id}';
const apiUrl = 'https://api.example.com/resource/';
const pathParams = [
{
path: 'p1',
Expand All @@ -36,12 +36,12 @@ describe('Utils Functions', () => {
},
];
expect(prepareEndpoint(message, apiUrl, pathParams)).toBe(
'https://api.example.com/resource/123/p1/P2',
'https://api.example.com/resource/p1/P2',
);
});
test('should add path params after uri encoding', () => {
const message = { id: 123, p2: 'P2%&' };
const apiUrl = 'https://api.example.com/resource/${$.id}';
const apiUrl = 'https://api.example.com/resource/';
const pathParams = [
{
path: 'p1',
Expand All @@ -51,7 +51,7 @@ describe('Utils Functions', () => {
},
];
expect(prepareEndpoint(message, apiUrl, pathParams)).toBe(
'https://api.example.com/resource/123/p1/P2%25%26',
'https://api.example.com/resource/p1/P2%25%26',
);
});
test('should throw error as path contains slash', () => {
Expand All @@ -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);
});
});
});
Loading

0 comments on commit 1fc15bf

Please sign in to comment.