From 6b1a23af845084d6f2f5fd14656e4a1d11a7e34b Mon Sep 17 00:00:00 2001 From: Manish Kumar <144022547+manish339k@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:52:19 +0530 Subject: [PATCH] feat: added bloomreach retl support (#3619) * feat: added bloomreach retl support * fix: resolving comments * feat: added other scenario test * fix: fixing payload structure * fix: fixing network handler for catalog * fix: minor change * fix: catalog batch items * feat: splitting bloomreach destination into bloomreach and bloomreach_catalog * chore: refactoring bloomreach catalog * chore: minor changes * chore: minor changes * chore: refactoring code after suggestion --- .../destinations/bloomreach_catalog/config.ts | 31 ++ .../bloomreach_catalog/rtWorkflow.yaml | 42 +++ .../bloomreach_catalog/transformRecord.ts | 93 +++++ .../destinations/bloomreach_catalog/utils.ts | 147 ++++++++ src/features.json | 3 +- .../bloomreach_catalog/networkHandler.js | 85 +++++ .../destinations/bloomreach_catalog/common.ts | 81 +++++ .../bloomreach_catalog/dataDelivery/data.ts | 197 +++++++++++ .../destinations/bloomreach_catalog/mocks.ts | 5 + .../bloomreach_catalog/network.ts | 108 ++++++ .../bloomreach_catalog/router/data.ts | 328 ++++++++++++++++++ 11 files changed, 1119 insertions(+), 1 deletion(-) create mode 100644 src/cdk/v2/destinations/bloomreach_catalog/config.ts create mode 100644 src/cdk/v2/destinations/bloomreach_catalog/rtWorkflow.yaml create mode 100644 src/cdk/v2/destinations/bloomreach_catalog/transformRecord.ts create mode 100644 src/cdk/v2/destinations/bloomreach_catalog/utils.ts create mode 100644 src/v1/destinations/bloomreach_catalog/networkHandler.js create mode 100644 test/integrations/destinations/bloomreach_catalog/common.ts create mode 100644 test/integrations/destinations/bloomreach_catalog/dataDelivery/data.ts create mode 100644 test/integrations/destinations/bloomreach_catalog/mocks.ts create mode 100644 test/integrations/destinations/bloomreach_catalog/network.ts create mode 100644 test/integrations/destinations/bloomreach_catalog/router/data.ts diff --git a/src/cdk/v2/destinations/bloomreach_catalog/config.ts b/src/cdk/v2/destinations/bloomreach_catalog/config.ts new file mode 100644 index 0000000000..8b469c3cf9 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/config.ts @@ -0,0 +1,31 @@ +export const MAX_PAYLOAD_SIZE = 10000000; +export const MAX_ITEMS = 5000; + +// ref:- https://documentation.bloomreach.com/engagement/reference/bulk-update-catalog-item +export const getCreateBulkCatalogItemEndpoint = ( + apiBaseUrl: string, + projectToken: string, + catalogId: string, +): string => `${apiBaseUrl}/data/v2/projects/${projectToken}/catalogs/${catalogId}/items`; + +// ref:- https://documentation.bloomreach.com/engagement/reference/bulk-partial-update-catalog-item +export const getUpdateBulkCatalogItemEndpoint = ( + apiBaseUrl: string, + projectToken: string, + catalogId: string, +): string => + `${apiBaseUrl}/data/v2/projects/${projectToken}/catalogs/${catalogId}/items/partial-update`; + +// ref:- https://documentation.bloomreach.com/engagement/reference/bulk-delete-catalog-items +export const getDeleteBulkCatalogItemEndpoint = ( + apiBaseUrl: string, + projectToken: string, + catalogId: string, +): string => + `${apiBaseUrl}/data/v2/projects/${projectToken}/catalogs/${catalogId}/items/bulk-delete`; + +export const CatalogAction = { + INSERT: 'insert', + UPDATE: 'update', + DELETE: 'delete', +}; diff --git a/src/cdk/v2/destinations/bloomreach_catalog/rtWorkflow.yaml b/src/cdk/v2/destinations/bloomreach_catalog/rtWorkflow.yaml new file mode 100644 index 0000000000..55809350eb --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/rtWorkflow.yaml @@ -0,0 +1,42 @@ +bindings: + - name: EventType + path: ../../../../constants + - name: processRecordInputs + path: ./transformRecord + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + - name: InstrumentationError + path: '@rudderstack/integrations-lib' + +steps: + - name: validateConfig + template: | + const config = ^[0].destination.Config + $.assertConfig(config.apiBaseUrl, "API Base URL is not present. Aborting"); + $.assertConfig(config.apiKey, "API Key is not present . Aborting"); + $.assertConfig(config.apiSecret, "API Secret is not present. Aborting"); + $.assertConfig(config.projectToken, "Project Token is not present. Aborting"); + $.assertConfig(config.catalogID, "Catalog Id is not present. Aborting"); + + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: processRecordEvents + template: | + $.processRecordInputs(^.{.message.type === $.EventType.RECORD}[], ^[0].destination) + + - name: failOtherEvents + template: | + const otherEvents = ^.{.message.type !== $.EventType.RECORD}[] + let failedEvents = otherEvents.map( + function(event) { + const error = new $.InstrumentationError("Event type " + event.message.type + " is not supported"); + $.handleRtTfSingleEventError(event, error, {}) + } + ) + failedEvents ?? [] + + - name: finalPayload + template: | + [...$.outputs.processRecordEvents, ...$.outputs.failOtherEvents] diff --git a/src/cdk/v2/destinations/bloomreach_catalog/transformRecord.ts b/src/cdk/v2/destinations/bloomreach_catalog/transformRecord.ts new file mode 100644 index 0000000000..68277448d0 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/transformRecord.ts @@ -0,0 +1,93 @@ +import { InstrumentationError } from '@rudderstack/integrations-lib'; +import { CatalogAction } from './config'; +import { batchResponseBuilder } from './utils'; + +import { handleRtTfSingleEventError, isEmptyObject } from '../../../../v0/util'; + +const prepareCatalogInsertOrUpdatePayload = (fields: any): any => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { item_id, ...properties } = fields; + return { item_id, properties }; +}; + +const processEvent = (event: any) => { + const { message } = event; + const { fields, action } = message; + const response = { + action, + payload: null, + }; + if (isEmptyObject(fields)) { + throw new InstrumentationError('`fields` cannot be empty'); + } + if (!fields.item_id) { + throw new InstrumentationError('`item_id` cannot be empty'); + } + if (action === CatalogAction.INSERT || action === CatalogAction.UPDATE) { + response.payload = prepareCatalogInsertOrUpdatePayload(fields); + } else if (action === CatalogAction.DELETE) { + response.payload = fields.item_id; + } else { + throw new InstrumentationError( + `Invalid action type ${action}. You can only add, update or remove items from the catalog`, + ); + } + return response; +}; + +const getEventChunks = ( + input: any, + insertItemRespList: any[], + updateItemRespList: any[], + deleteItemRespList: any[], +) => { + switch (input.response.action) { + case CatalogAction.INSERT: + insertItemRespList.push({ payload: input.response.payload, metadata: input.metadata }); + break; + case CatalogAction.UPDATE: + updateItemRespList.push({ payload: input.response.payload, metadata: input.metadata }); + break; + case CatalogAction.DELETE: + deleteItemRespList.push({ payload: input.response.payload, metadata: input.metadata }); + break; + default: + throw new InstrumentationError(`Invalid action type ${input.response.action}`); + } +}; + +export const processRecordInputs = (inputs: any[], destination: any) => { + const insertItemRespList: any[] = []; + const updateItemRespList: any[] = []; + const deleteItemRespList: any[] = []; + const batchErrorRespList: any[] = []; + + if (!inputs || inputs.length === 0) { + return []; + } + + inputs.forEach((input) => { + try { + getEventChunks( + { + response: processEvent(input), + metadata: input.metadata, + }, + insertItemRespList, + updateItemRespList, + deleteItemRespList, + ); + } catch (error) { + const errRespEvent = handleRtTfSingleEventError(input, error, {}); + batchErrorRespList.push(errRespEvent); + } + }); + + const batchSuccessfulRespList = batchResponseBuilder( + insertItemRespList, + updateItemRespList, + deleteItemRespList, + destination, + ); + return [...batchSuccessfulRespList, ...batchErrorRespList]; +}; diff --git a/src/cdk/v2/destinations/bloomreach_catalog/utils.ts b/src/cdk/v2/destinations/bloomreach_catalog/utils.ts new file mode 100644 index 0000000000..0e74ce9379 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/utils.ts @@ -0,0 +1,147 @@ +import { BatchUtils } from '@rudderstack/workflow-engine'; +import { base64Convertor } from '@rudderstack/integrations-lib'; +import { + getCreateBulkCatalogItemEndpoint, + getDeleteBulkCatalogItemEndpoint, + getUpdateBulkCatalogItemEndpoint, + MAX_ITEMS, + MAX_PAYLOAD_SIZE, +} from './config'; + +const buildBatchedRequest = ( + payload: string, + method: string, + endpoint: string, + headers: any, + metadata: any, + destination: any, +) => ({ + batchedRequest: { + body: { + JSON: {}, + JSON_ARRAY: { batch: payload }, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method, + endpoint, + headers, + params: {}, + files: {}, + }, + metadata, + batched: true, + statusCode: 200, + destination, +}); + +const getHeaders = (destination: any) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${base64Convertor(`${destination.Config.apiKey}:${destination.Config.apiSecret}`)}`, +}); + +// returns merged metadata for a batch +const getMergedMetadata = (batch: any[]) => batch.map((input) => input.metadata); + +// returns merged payload for a batch +const getMergedEvents = (batch: any[]) => batch.map((input) => input.payload); + +// builds final batched response for insert action records +const insertItemBatchResponseBuilder = (insertItemRespList: any[], destination: any) => { + const insertItemBatchedResponse: any[] = []; + + const method = 'PUT'; + const endpoint = getCreateBulkCatalogItemEndpoint( + destination.Config.apiBaseUrl, + destination.Config.projectToken, + destination.Config.catalogID, + ); + const headers = getHeaders(destination); + + const batchesOfEvents = BatchUtils.chunkArrayBySizeAndLength(insertItemRespList, { + maxSizeInBytes: MAX_PAYLOAD_SIZE, + maxItems: MAX_ITEMS, + }); + batchesOfEvents.items.forEach((batch: any) => { + const mergedPayload = JSON.stringify(getMergedEvents(batch)); + const mergedMetadata = getMergedMetadata(batch); + insertItemBatchedResponse.push( + buildBatchedRequest(mergedPayload, method, endpoint, headers, mergedMetadata, destination), + ); + }); + return insertItemBatchedResponse; +}; + +// builds final batched response for update action records +const updateItemBatchResponseBuilder = (updateItemRespList: any[], destination: any) => { + const updateItemBatchedResponse: any[] = []; + + const method = 'POST'; + const endpoint = getUpdateBulkCatalogItemEndpoint( + destination.Config.apiBaseUrl, + destination.Config.projectToken, + destination.Config.catalogID, + ); + const headers = getHeaders(destination); + + const batchesOfEvents = BatchUtils.chunkArrayBySizeAndLength(updateItemRespList, { + maxSizeInBytes: MAX_PAYLOAD_SIZE, + maxItems: MAX_ITEMS, + }); + batchesOfEvents.items.forEach((batch: any) => { + const mergedPayload = JSON.stringify(getMergedEvents(batch)); + const mergedMetadata = getMergedMetadata(batch); + updateItemBatchedResponse.push( + buildBatchedRequest(mergedPayload, method, endpoint, headers, mergedMetadata, destination), + ); + }); + return updateItemBatchedResponse; +}; + +// builds final batched response for delete action records +const deleteItemBatchResponseBuilder = (deleteItemRespList: any[], destination: any) => { + const deleteItemBatchedResponse: any[] = []; + + const method = 'DELETE'; + const endpoint = getDeleteBulkCatalogItemEndpoint( + destination.Config.apiBaseUrl, + destination.Config.projectToken, + destination.Config.catalogID, + ); + const headers = getHeaders(destination); + + const batchesOfEvents = BatchUtils.chunkArrayBySizeAndLength(deleteItemRespList, { + maxSizeInBytes: MAX_PAYLOAD_SIZE, + maxItems: MAX_ITEMS, + }); + batchesOfEvents.items.forEach((batch: any) => { + const mergedPayload = JSON.stringify(getMergedEvents(batch)); + const mergedMetadata = getMergedMetadata(batch); + deleteItemBatchedResponse.push( + buildBatchedRequest(mergedPayload, method, endpoint, headers, mergedMetadata, destination), + ); + }); + return deleteItemBatchedResponse; +}; + +// returns final batched response +export const batchResponseBuilder = ( + insertItemRespList: any, + updateItemRespList: any, + deleteItemRespList: any, + destination: any, +) => { + const response: any[] = []; + if (insertItemRespList.length > 0) { + response.push(...insertItemBatchResponseBuilder(insertItemRespList, destination)); + } + if (updateItemRespList.length > 0) { + response.push(...updateItemBatchResponseBuilder(updateItemRespList, destination)); + } + if (deleteItemRespList.length > 0) { + response.push(...deleteItemBatchResponseBuilder(deleteItemRespList, destination)); + } + return response; +}; diff --git a/src/features.json b/src/features.json index 94e36a2416..1a69a58307 100644 --- a/src/features.json +++ b/src/features.json @@ -76,7 +76,8 @@ "WUNDERKIND": true, "CLICKSEND": true, "ZOHO": true, - "CORDIAL": true + "CORDIAL": true, + "BLOOMREACH_CATALOG": true }, "regulations": [ "BRAZE", diff --git a/src/v1/destinations/bloomreach_catalog/networkHandler.js b/src/v1/destinations/bloomreach_catalog/networkHandler.js new file mode 100644 index 0000000000..1fb987b840 --- /dev/null +++ b/src/v1/destinations/bloomreach_catalog/networkHandler.js @@ -0,0 +1,85 @@ +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { isHttpStatusSuccess } = require('../../../v0/util/index'); +const tags = require('../../../v0/util/tags'); + +// Catalog response +// [ +// { +// "errors": { +// "properties": [ +// "Fields [field1, field2] are not properly defined." +// ] +// }, +// "queued": false, +// "success": false +// }, +// { +// "success" : "True", +// "queued" : "True", +// }, +// ] +const checkIfEventIsAbortableAndExtractErrorMessage = (element) => { + if (element.success) { + return { isAbortable: false, errorMsg: '' }; + } + + const errorMsg = Object.values(element.errors || {}) + .flat() + .join(', '); + + return { isAbortable: true, errorMsg }; +}; + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; + + const message = '[BLOOMREACH_CATALOG Response V1 Handler] - Request Processed Successfully'; + const responseWithIndividualEvents = []; + const { response, status } = destinationResponse; + + if (isHttpStatusSuccess(status)) { + // check for Partial Event failures and Successes + response.forEach((event, idx) => { + const proxyOutput = { + statusCode: 200, + error: 'success', + metadata: rudderJobMetadata[idx], + }; + // update status of partial event if abortable + const { isAbortable, errorMsg } = checkIfEventIsAbortableAndExtractErrorMessage(event); + if (isAbortable) { + proxyOutput.error = errorMsg; + proxyOutput.statusCode = 400; + } + responseWithIndividualEvents.push(proxyOutput); + }); + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; + } + throw new TransformerProxyError( + `BLOOMREACH_CATALOG: Error encountered in transformer proxy V1`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); +}; +function networkHandler() { + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.prepareProxy = prepareProxyRequest; + this.responseHandler = responseHandler; +} +module.exports = { networkHandler }; diff --git a/test/integrations/destinations/bloomreach_catalog/common.ts b/test/integrations/destinations/bloomreach_catalog/common.ts new file mode 100644 index 0000000000..2b4266837b --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/common.ts @@ -0,0 +1,81 @@ +import { Destination } from '../../../../src/types'; + +const destType = 'bloomreach_catalog'; +const destTypeInUpperCase = 'BLOOMREACH_CATALOG'; +const displayName = 'bloomreach catalog'; +const channel = 'web'; +const destination: Destination = { + Config: { + apiBaseUrl: 'https://demoapp-api.bloomreach.com', + apiKey: 'test-api-key', + apiSecret: 'test-api-secret', + projectToken: 'test-project-token', + catalogID: 'test-catalog-id', + }, + DestinationDefinition: { + DisplayName: displayName, + ID: '123', + Name: destTypeInUpperCase, + Config: { cdkV2Enabled: true }, + }, + Enabled: true, + ID: '123', + Name: destTypeInUpperCase, + Transformations: [], + WorkspaceID: 'test-workspace-id', +}; + +const insertEndpoint = + 'https://demoapp-api.bloomreach.com/data/v2/projects/test-project-token/catalogs/test-catalog-id/items'; +const updateEndpoint = + 'https://demoapp-api.bloomreach.com/data/v2/projects/test-project-token/catalogs/test-catalog-id/items/partial-update'; +const deleteEndpoint = + 'https://demoapp-api.bloomreach.com/data/v2/projects/test-project-token/catalogs/test-catalog-id/items/bulk-delete'; + +const processorInstrumentationErrorStatTags = { + destType: destTypeInUpperCase, + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', +}; + +const RouterInstrumentationErrorStatTags = { + ...processorInstrumentationErrorStatTags, + feature: 'router', +}; + +const proxyV1RetryableErrorStatTags = { + ...RouterInstrumentationErrorStatTags, + errorCategory: 'network', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', +}; + +const headers = { + 'Content-Type': 'application/json', + Authorization: 'Basic dGVzdC1hcGkta2V5OnRlc3QtYXBpLXNlY3JldA==', +}; + +const sampleContext = { + destinationFields: 'item_id, title, status, unprinted', + mappedToDestination: 'true', +}; + +export { + destType, + channel, + destination, + processorInstrumentationErrorStatTags, + RouterInstrumentationErrorStatTags, + headers, + proxyV1RetryableErrorStatTags, + insertEndpoint, + updateEndpoint, + deleteEndpoint, + sampleContext, +}; diff --git a/test/integrations/destinations/bloomreach_catalog/dataDelivery/data.ts b/test/integrations/destinations/bloomreach_catalog/dataDelivery/data.ts new file mode 100644 index 0000000000..f8cccd04ed --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/dataDelivery/data.ts @@ -0,0 +1,197 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { destType, headers, updateEndpoint } from '../common'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; + +export const data: ProxyV1TestData[] = [ + { + id: 'bloomreach_catalog_v1_business_scenario_1', + name: destType, + description: + '[Proxy v1 API] :: Test for a valid record request - where the destination responds with 200 with error for request 2 in a batch', + successCriteria: 'Should return 200 with partial failures within the response payload', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params: {}, + JSON: {}, + JSON_ARRAY: { + batch: + '[{"item_id":"test-item-id","properties":{"unprinted":"1"}},{"item_id":"test-item-id-faulty","properties":{"unprinted1":"1"}}]', + }, + endpoint: updateEndpoint, + }, + [generateMetadata(1), generateMetadata(2)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[BLOOMREACH_CATALOG Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: [ + { + success: true, + queued: true, + }, + { + success: false, + queued: false, + errors: { + properties: ['Fields [unprinted1] are not properly defined.'], + }, + }, + ], + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 400, + metadata: generateMetadata(2), + error: 'Fields [unprinted1] are not properly defined.', + }, + ], + }, + }, + }, + }, + }, + { + id: 'bloomreach_catalog_v1_business_scenario_2', + name: destType, + description: + '[Proxy v1 API] :: Test for a valid rETL request - where the destination responds with 200 without any error', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params: {}, + JSON: {}, + JSON_ARRAY: { + batch: + '[{"item_id":"test-item-id-1","properties":{"unprinted":"1"}},{"item_id":"test-item-id-2","properties":{"unprinted":"2"}}]', + }, + endpoint: updateEndpoint, + }, + [generateMetadata(3), generateMetadata(4)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[BLOOMREACH_CATALOG Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: [ + { + success: true, + queued: true, + }, + { + success: true, + queued: true, + }, + ], + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(3), + error: 'success', + }, + { + statusCode: 200, + metadata: generateMetadata(4), + error: 'success', + }, + ], + }, + }, + }, + }, + }, + { + id: 'bloomreach_catalog_v1_business_scenario_3', + name: destType, + description: + '[Proxy v1 API] :: Test for a valid rETL request - where the destination responds with 200 with error', + successCriteria: 'Should return 400 with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params: {}, + JSON: {}, + JSON_ARRAY: { + batch: '[{"item_id":"test-item-id-faulty","properties":{"unprinted1":"1"}}]', + }, + endpoint: updateEndpoint, + }, + [generateMetadata(5)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[BLOOMREACH_CATALOG Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: [ + { + success: false, + queued: false, + errors: { + properties: ['Fields [unprinted1] are not properly defined.'], + }, + }, + ], + status: 200, + }, + response: [ + { + statusCode: 400, + metadata: generateMetadata(5), + error: 'Fields [unprinted1] are not properly defined.', + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach_catalog/mocks.ts b/test/integrations/destinations/bloomreach_catalog/mocks.ts new file mode 100644 index 0000000000..bb07c0ea72 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/mocks.ts @@ -0,0 +1,5 @@ +import * as config from '../../../../src/cdk/v2/destinations/bloomreach_catalog/config'; + +export const defaultMockFns = () => { + jest.replaceProperty(config, 'MAX_ITEMS', 2 as typeof config.MAX_ITEMS); +}; diff --git a/test/integrations/destinations/bloomreach_catalog/network.ts b/test/integrations/destinations/bloomreach_catalog/network.ts new file mode 100644 index 0000000000..b8ae078498 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/network.ts @@ -0,0 +1,108 @@ +import { destType, headers, updateEndpoint } from './common'; + +export const networkCallsData = [ + { + httpReq: { + url: updateEndpoint, + data: [ + { + item_id: 'test-item-id', + properties: { + unprinted: '1', + }, + }, + { + item_id: 'test-item-id-faulty', + properties: { + unprinted1: '1', + }, + }, + ], + params: { destination: destType }, + headers, + method: 'POST', + }, + httpRes: { + data: [ + { + success: true, + queued: true, + }, + { + success: false, + queued: false, + errors: { + properties: ['Fields [unprinted1] are not properly defined.'], + }, + }, + ], + status: 200, + statusText: 'Ok', + }, + }, + { + httpReq: { + url: updateEndpoint, + data: [ + { + item_id: 'test-item-id-1', + properties: { + unprinted: '1', + }, + }, + { + item_id: 'test-item-id-2', + properties: { + unprinted: '2', + }, + }, + ], + params: { destination: destType }, + headers, + method: 'POST', + }, + httpRes: { + data: [ + { + success: true, + queued: true, + }, + { + success: true, + queued: true, + }, + ], + status: 200, + statusText: 'Ok', + }, + }, + { + httpReq: { + url: updateEndpoint, + data: [ + { + item_id: 'test-item-id-faulty', + properties: { + unprinted1: '1', + }, + }, + ], + params: { destination: destType }, + headers, + method: 'POST', + }, + httpRes: { + data: [ + { + success: false, + queued: false, + errors: { + properties: ['Fields [unprinted1] are not properly defined.'], + }, + }, + ], + status: 200, + statusText: 'Ok', + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach_catalog/router/data.ts b/test/integrations/destinations/bloomreach_catalog/router/data.ts new file mode 100644 index 0000000000..68ab422444 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/router/data.ts @@ -0,0 +1,328 @@ +import { generateMetadata } from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; +import { + destType, + destination, + headers, + RouterInstrumentationErrorStatTags, + insertEndpoint, + updateEndpoint, + deleteEndpoint, + sampleContext, +} from '../common'; + +const routerRequest = { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + item_id: 'test-item-id', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 1, + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + metadata: generateMetadata(1), + destination, + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + item_id: 'test-item-id-7', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 1, + test_empty: '', + test_null: null, + test_empty_array: [], + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + metadata: generateMetadata(2), + destination, + }, + { + message: { + type: 'record', + action: 'update', + fields: { + item_id: 'test-item-id', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 3, + }, + channel: 'sources', + context: sampleContext, + recordId: '3', + }, + metadata: generateMetadata(3), + destination, + }, + { + message: { + type: 'record', + action: 'update', + fields: { + item_id: 'test-item-id', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 2, + }, + channel: 'sources', + context: sampleContext, + recordId: '4', + }, + metadata: generateMetadata(4), + destination, + }, + { + message: { + type: 'record', + action: 'delete', + fields: { + item_id: 'test-item-id-1', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 1, + }, + channel: 'sources', + context: sampleContext, + recordId: '5', + }, + metadata: generateMetadata(5), + destination, + }, + { + message: { + type: 'record', + action: 'delete', + fields: { + item_id: 'test-item-id-2', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 1, + }, + channel: 'sources', + context: sampleContext, + recordId: '6', + }, + metadata: generateMetadata(6), + destination, + }, + { + message: { + type: 'record', + action: 'delete', + fields: { + item_id: 'test-item-id-3', + title: 'Hardcover Monthbooks', + status: 'up to date', + unprinted: 1, + }, + channel: 'sources', + context: sampleContext, + recordId: '7', + }, + metadata: generateMetadata(7), + destination, + }, + { + message: { + type: 'record', + action: 'insert', + fields: {}, + channel: 'sources', + context: sampleContext, + recordId: '8', + }, + metadata: generateMetadata(8), + destination, + }, + { + message: { + type: 'record', + action: 'dummy-action', + fields: { + item_id: 'test-item-id', + }, + channel: 'sources', + context: sampleContext, + recordId: '9', + }, + metadata: generateMetadata(9), + destination, + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + item_id: null, + }, + channel: 'sources', + context: sampleContext, + recordId: '10', + }, + metadata: generateMetadata(10), + destination, + }, + ], + destType, +}; + +export const data = [ + { + id: 'bloomreach-catalog-router-test-1', + name: destType, + description: 'Basic Router Test to test record payloads', + scenario: 'Framework', + successCriteria: 'All events should be transformed successfully and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'PUT', + endpoint: insertEndpoint, + headers, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: { + batch: + '[{"item_id":"test-item-id","properties":{"title":"Hardcover Monthbooks","status":"up to date","unprinted":1}},{"item_id":"test-item-id-7","properties":{"title":"Hardcover Monthbooks","status":"up to date","unprinted":1,"test_empty":"","test_null":null,"test_empty_array":[]}}]', + }, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1), generateMetadata(2)], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: updateEndpoint, + headers, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: { + batch: + '[{"item_id":"test-item-id","properties":{"title":"Hardcover Monthbooks","status":"up to date","unprinted":3}},{"item_id":"test-item-id","properties":{"title":"Hardcover Monthbooks","status":"up to date","unprinted":2}}]', + }, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(3), generateMetadata(4)], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'DELETE', + endpoint: deleteEndpoint, + headers, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: { + batch: '["test-item-id-1","test-item-id-2"]', + }, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(5), generateMetadata(6)], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'DELETE', + endpoint: deleteEndpoint, + headers, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: { + batch: '["test-item-id-3"]', + }, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(7)], + batched: true, + statusCode: 200, + destination, + }, + { + metadata: [generateMetadata(8)], + batched: false, + statusCode: 400, + error: '`fields` cannot be empty', + statTags: RouterInstrumentationErrorStatTags, + destination, + }, + { + metadata: [generateMetadata(9)], + batched: false, + statusCode: 400, + error: + 'Invalid action type dummy-action. You can only add, update or remove items from the catalog', + statTags: RouterInstrumentationErrorStatTags, + destination, + }, + { + metadata: [generateMetadata(10)], + batched: false, + statusCode: 400, + error: '`item_id` cannot be empty', + statTags: RouterInstrumentationErrorStatTags, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +];