diff --git a/src/cdk/v2/destinations/bloomreach/config.ts b/src/cdk/v2/destinations/bloomreach/config.ts index 998b898c88..1a4f06224e 100644 --- a/src/cdk/v2/destinations/bloomreach/config.ts +++ b/src/cdk/v2/destinations/bloomreach/config.ts @@ -3,42 +3,10 @@ import { getMappingConfig } from '../../../../v0/util'; export const CUSTOMER_COMMAND = 'customers'; export const CUSTOMER_EVENT_COMMAND = 'customers/events'; export const MAX_BATCH_SIZE = 50; -export const MAX_PAYLOAD_SIZE = 10000000; -export const MAX_ITEMS = 5000; // ref:- https://documentation.bloomreach.com/engagement/reference/batch-commands-2 export const getBatchEndpoint = (apiBaseUrl: string, projectToken: string): string => `${apiBaseUrl}/track/v2/projects/${projectToken}/batch`; - -// 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 RecordAction = { - INSERT: 'insert', - UPDATE: 'update', - DELETE: 'delete', -}; - const CONFIG_CATEGORIES = { CUSTOMER_PROPERTIES_CONFIG: { name: 'BloomreachCustomerPropertiesConfig' }, }; diff --git a/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml b/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml index 57a56e8a1a..5a9dcaa18d 100644 --- a/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml +++ b/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml @@ -24,41 +24,22 @@ steps: template: | $.context.messageType = .message.type.toLowerCase(); - - name: mappedToDestination - template: | - .message.context.mappedToDestination || false; - - name: validateInput template: | let messageType = $.context.messageType; $.assert(messageType, "message Type is not present. Aborting"); + $.assert(messageType in {{$.EventType.([.IDENTIFY,.TRACK,.PAGE,.SCREEN])}}, "message type " + messageType + " is not supported"); $.assertConfig(.destination.Config.apiBaseUrl, "API Base URL is not present. Aborting"); $.assertConfig(.destination.Config.apiKey, "API Key is not present . Aborting"); $.assertConfig(.destination.Config.apiSecret, "API Secret is not present. Aborting"); $.assertConfig(.destination.Config.projectToken, "Project Token is not present. Aborting"); - steps: - - name: validateContactAndEventsInput - condition: $.outputs.mappedToDestination === false - template: | - let messageType = $.context.messageType; - $.assert(messageType in {{$.EventType.([.IDENTIFY,.TRACK,.PAGE,.SCREEN])}}, "message type " + messageType + " is not supported"); - $.assertConfig(.destination.Config.hardID, "Hard ID is not present. Aborting"); - $.assertConfig(.destination.Config.softID, "Soft ID is not present. Aborting"); - $.assert(.message.timestamp ?? .message.originalTimestamp, "Timestamp is not present. Aborting"); - const userId = .message.().( + $.assertConfig(.destination.Config.hardID, "Hard ID is not present. Aborting"); + $.assertConfig(.destination.Config.softID, "Soft ID is not present. Aborting"); + $.assert(.message.timestamp ?? .message.originalTimestamp, "Timestamp is not present. Aborting"); + const userId = .message.().( {{{{$.getGenericPaths("userIdOnly")}}}}; - ); - $.assert(userId || .message.anonymousId, "Either one of userId or anonymousId is required. Aborting"); - - name: validateCatalogInput - condition: $.outputs.mappedToDestination - template: | - let messageType = $.context.messageType; - $.assert(messageType in {{$.EventType.([.RECORD])}}, "message type " + messageType + " is not supported"); - $.assertConfig(.destination.Config.catalogID, "Catalog Id is not present. Aborting"); - const action = .message.action; - const item_id = .message.fields.item_id; - $.assert(action in {{$.RecordAction.([.INSERT,.UPDATE,.DELETE])}}, "message action " + action + " is not supported"); - $.assert(item_id, "Item Id is required. Aborting"); + ); + $.assert(userId || .message.anonymousId, "Either one of userId or anonymousId is required. Aborting"); - name: prepareIdentifyPayload condition: $.context.messageType === {{$.EventType.IDENTIFY}} @@ -76,9 +57,7 @@ steps: properties }); - $.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_COMMAND, data}); - $.context.endpoint = $.getBatchEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken); - $.context.method = 'POST'; + $.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_COMMAND, data}) - name: prepareEventName steps: @@ -109,7 +88,7 @@ steps: $.context.event = .message.event - name: prepareTrackPageScreenPayload - condition: $.context.messageType !== {{$.EventType.IDENTIFY}} && $.context.messageType !== {{$.EventType.RECORD}} + condition: $.context.messageType !== {{$.EventType.IDENTIFY}} template: | const customerIDs = $.prepareCustomerIDs(.message, .destination); const data = .message.().({ @@ -119,40 +98,22 @@ steps: "event_type": $.context.event, }); - $.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_EVENT_COMMAND, data}); - $.context.endpoint = $.getBatchEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken); - $.context.method = 'POST'; - - - name: prepareRecordPayload - condition: $.context.messageType === {{$.EventType.RECORD}} - steps: - - name: prepareInsertActionPayload - condition: .message.action === {{$.RecordAction.INSERT}} - template: | - $.context.payload = $.prepareRecordInsertOrUpdatePayload(.message.fields); - $.context.endpoint = $.getCreateBulkCatalogItemEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken, .destination.Config.catalogID); - $.context.method = 'PUT'; - - name: prepareUpdateActionPayload - condition: .message.action === {{$.RecordAction.UPDATE}} - template: | - $.context.payload = $.prepareRecordInsertOrUpdatePayload(.message.fields); - $.context.endpoint = $.getUpdateBulkCatalogItemEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken, .destination.Config.catalogID); - $.context.method = 'POST'; - - name: prepareDeleteActionPayload - condition: .message.action === {{$.RecordAction.DELETE}} - template: | - $.context.payload = .message.fields.item_id; - $.context.endpoint = $.getDeleteBulkCatalogItemEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken, .destination.Config.catalogID); - $.context.method = 'DELETE'; + $.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_EVENT_COMMAND, data}) - name: buildResponse + description: In batchMode we return payload directly + condition: $.batchMode template: | - const response = $.defaultRequestConfig(); - response.body.JSON = $.context.payload; - response.endpoint = $.context.endpoint; - response.method = $.context.method; - response.headers = { - "Content-Type": "application/json", - "Authorization": "Basic " + $.base64Convertor(.destination.Config.apiKey + ":" + .destination.Config.apiSecret) - } - response; + $.context.payload + else: + name: buildResponseForProcessTransformation + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = $.context.payload; + response.endpoint = $.getBatchEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken); + response.method = "POST"; + response.headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + $.base64Convertor(.destination.Config.apiKey + ":" + .destination.Config.apiSecret) + } + response; diff --git a/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml b/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml index 717189c911..b8b27ca02e 100644 --- a/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml +++ b/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml @@ -27,9 +27,11 @@ steps: - name: successfulEvents template: | $.outputs.transform#idx.output.({ - "message": .[], - "destination": ^ [idx].destination, - "metadata": ^ [idx].metadata + "batchedRequest": ., + "batched": false, + "destination": ^[idx].destination, + "metadata": ^[idx].metadata, + "statusCode": 200 })[] - name: failedEvents @@ -41,7 +43,33 @@ steps: - name: batchSuccessfulEvents description: Batches the successfulEvents template: | - $.batchResponseBuilder($.outputs.successfulEvents); + let batches = $.BatchUtils.chunkArrayBySizeAndLength( + $.outputs.successfulEvents, {maxItems: $.MAX_BATCH_SIZE}).items; + + batches@batch.({ + "batchedRequest": { + "body": { + "JSON": {"commands": ~r batch.batchedRequest[]}, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": batch[0].destination.Config.().($.getBatchEndpoint(.apiBaseUrl, .projectToken)), + "headers": batch[0].destination.Config.().({ + "Content-Type": "application/json", + "Authorization": "Basic " + $.base64Convertor(.apiKey + ":" + .apiSecret) + }), + "params": {}, + "files": {} + }, + "metadata": ~r batch.metadata[], + "batched": true, + "statusCode": 200, + "destination": batch[0].destination + })[]; - name: finalPayload template: | diff --git a/src/cdk/v2/destinations/bloomreach/utils.ts b/src/cdk/v2/destinations/bloomreach/utils.ts index e12098786a..f834fa74e7 100644 --- a/src/cdk/v2/destinations/bloomreach/utils.ts +++ b/src/cdk/v2/destinations/bloomreach/utils.ts @@ -1,8 +1,5 @@ -import lodash from 'lodash'; -import { BatchUtils } from '@rudderstack/workflow-engine'; import { isObject, isEmptyObject, getIntegrationsObj } from '../../../../v0/util'; import { RudderMessage, Destination } from '../../../../types'; -import { MAX_BATCH_SIZE, MAX_ITEMS, MAX_PAYLOAD_SIZE } from './config'; const getCustomerIDsFromIntegrationObject = (message: RudderMessage): any => { const integrationObj = getIntegrationsObj(message, 'bloomreach' as any) || {}; @@ -32,97 +29,3 @@ export const prepareCustomerIDs = (message: RudderMessage, destination: Destinat }; return customerIDs; }; - -export const prepareRecordInsertOrUpdatePayload = (fields: any): any => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { item_id, ...properties } = fields; - return { item_id, properties }; -}; - -const mergeMetadata = (batch: any[]) => batch.map((event) => event.metadata); - -const getMergedEvents = (batch: any[]) => batch.map((event) => event.message[0].body.JSON); - -const buildBatchedRequest = ( - batch: any[], - constants: { - version: any; - type: any; - method: any; - headers: any; - destination: any; - endPoint: any; - } | null, - endpoint: string, - batchEvent: boolean, -) => ({ - batchedRequest: { - body: { - JSON: batchEvent ? { commands: getMergedEvents(batch) } : {}, - JSON_ARRAY: batchEvent ? {} : { batch: getMergedEvents(batch) }, - XML: {}, - FORM: {}, - }, - version: '1', - type: 'REST', - method: constants?.method, - endpoint, - headers: constants?.headers, - params: {}, - files: {}, - }, - metadata: mergeMetadata(batch), - batched: true, - statusCode: 200, - destination: batch[0].destination, -}); - -const initializeConstants = (successfulEvents: any[]) => { - if (successfulEvents.length === 0) return null; - return { - version: successfulEvents[0].message[0].version, - type: successfulEvents[0].message[0].type, - method: successfulEvents[0].message[0].method, - headers: successfulEvents[0].message[0].headers, - destination: successfulEvents[0].destination, - endPoint: successfulEvents[0].message[0].endpoint, - }; -}; - -export const batchResponseBuilder = (events: any): any => { - const response: any[] = []; - let constants = initializeConstants(events); - if (!constants) return []; - const eventsGroupByEndpoint = lodash.groupBy(events, (event) => event.message[0].endpoint); - - Object.keys(eventsGroupByEndpoint).forEach((eventEndPoint) => { - const batchEvent = eventEndPoint.endsWith('/batch'); - if (batchEvent) { - constants = initializeConstants(eventsGroupByEndpoint[eventEndPoint]); - const bathesOfEvents = BatchUtils.chunkArrayBySizeAndLength( - eventsGroupByEndpoint[eventEndPoint], - { maxItems: MAX_BATCH_SIZE }, - ); - bathesOfEvents.items.forEach((batch) => { - response.push(buildBatchedRequest(batch, constants, eventEndPoint, batchEvent)); - }); - } else { - constants = initializeConstants(eventsGroupByEndpoint[eventEndPoint]); - const bathesOfEvents = BatchUtils.chunkArrayBySizeAndLength( - eventsGroupByEndpoint[eventEndPoint], - { - maxSizeInBytes: MAX_PAYLOAD_SIZE, - maxItems: MAX_ITEMS, - }, - ); - bathesOfEvents.items.forEach((batch) => { - const requests: any = buildBatchedRequest(batch, constants, eventEndPoint, batchEvent); - requests.batchedRequest.body.JSON_ARRAY.batch = JSON.stringify( - requests.batchedRequest.body.JSON_ARRAY.batch, - ); - response.push(requests); - }); - } - }); - return response; -}; 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..f9fecfeea8 --- /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 RecordAction = { + INSERT: 'insert', + UPDATE: 'update', + DELETE: 'delete', +}; diff --git a/src/cdk/v2/destinations/bloomreach_catalog/procWorkflow.yaml b/src/cdk/v2/destinations/bloomreach_catalog/procWorkflow.yaml new file mode 100644 index 0000000000..7e3e875f3c --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/procWorkflow.yaml @@ -0,0 +1,63 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + - name: defaultRequestConfig + path: ../../../../v0/util + - name: base64Convertor + path: ../../../../v0/util + - path: ./utils + - path: ./config + +steps: + - name: messageType + template: | + $.context.messageType = .message.type.toLowerCase(); + + - name: validateInput + template: | + let messageType = $.context.messageType; + $.assert(messageType, "message Type is not present. Aborting"); + $.assert(messageType in {{$.EventType.([.RECORD])}}, "message type " + messageType + " is not supported"); + $.assertConfig(.destination.Config.apiBaseUrl, "API Base URL is not present. Aborting"); + $.assertConfig(.destination.Config.apiKey, "API Key is not present . Aborting"); + $.assertConfig(.destination.Config.apiSecret, "API Secret is not present. Aborting"); + $.assertConfig(.destination.Config.projectToken, "Project Token is not present. Aborting"); + $.assertConfig(.destination.Config.catalogID, "Catalog Id is not present. Aborting"); + const action = .message.action; + const item_id = .message.fields.item_id; + $.assert(action in {{$.RecordAction.([.INSERT,.UPDATE,.DELETE])}}, "message action " + action + " is not supported"); + $.assert(item_id, "Item Id is required. Aborting"); + + - name: prepareInsertActionPayload + condition: .message.action === {{$.RecordAction.INSERT}} + template: | + $.context.payload = $.prepareRecordInsertOrUpdatePayload(.message.fields); + $.context.endpoint = $.getCreateBulkCatalogItemEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken, .destination.Config.catalogID); + $.context.method = 'PUT'; + + - name: prepareUpdateActionPayload + condition: .message.action === {{$.RecordAction.UPDATE}} + template: | + $.context.payload = $.prepareRecordInsertOrUpdatePayload(.message.fields); + $.context.endpoint = $.getUpdateBulkCatalogItemEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken, .destination.Config.catalogID); + $.context.method = 'POST'; + + - name: prepareDeleteActionPayload + condition: .message.action === {{$.RecordAction.DELETE}} + template: | + $.context.payload = .message.fields.item_id; + $.context.endpoint = $.getDeleteBulkCatalogItemEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken, .destination.Config.catalogID); + $.context.method = 'DELETE'; + + - name: buildResponse + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = $.context.payload; + response.endpoint = $.context.endpoint; + response.method = $.context.method; + response.headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + $.base64Convertor(.destination.Config.apiKey + ":" + .destination.Config.apiSecret) + } + response; 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..717189c911 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/rtWorkflow.yaml @@ -0,0 +1,48 @@ +bindings: + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + - path: ./utils + exportAll: true + - name: base64Convertor + path: ../../../../v0/util + - name: toUnixTimestamp + path: ../../../../v0/util + - name: BatchUtils + path: '@rudderstack/workflow-engine' + - path: ./config + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "message": .[], + "destination": ^ [idx].destination, + "metadata": ^ [idx].metadata + })[] + + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.batchSuccessfulEvents, ...$.outputs.failedEvents] 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..ec34229db1 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach_catalog/utils.ts @@ -0,0 +1,84 @@ +import lodash from 'lodash'; +import { BatchUtils } from '@rudderstack/workflow-engine'; +import { MAX_ITEMS, MAX_PAYLOAD_SIZE } from './config'; + +export const prepareRecordInsertOrUpdatePayload = (fields: any): any => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { item_id, ...properties } = fields; + return { item_id, properties }; +}; + +const mergeMetadata = (batch: any[]) => batch.map((event) => event.metadata); + +const getMergedEvents = (batch: any[]) => batch.map((event) => event.message[0].body.JSON); + +const buildBatchedRequest = ( + batch: any[], + constants: { + version: any; + type: any; + method: any; + headers: any; + destination: any; + endPoint: any; + } | null, + endpoint: string, +) => ({ + batchedRequest: { + body: { + JSON: {}, + JSON_ARRAY: { batch: getMergedEvents(batch) }, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: constants?.method, + endpoint, + headers: constants?.headers, + params: {}, + files: {}, + }, + metadata: mergeMetadata(batch), + batched: true, + statusCode: 200, + destination: batch[0].destination, +}); + +const initializeConstants = (successfulEvents: any[]) => { + if (successfulEvents.length === 0) return null; + return { + version: successfulEvents[0].message[0].version, + type: successfulEvents[0].message[0].type, + method: successfulEvents[0].message[0].method, + headers: successfulEvents[0].message[0].headers, + destination: successfulEvents[0].destination, + endPoint: successfulEvents[0].message[0].endpoint, + }; +}; + +export const batchResponseBuilder = (events: any): any => { + const response: any[] = []; + let constants = initializeConstants(events); + if (!constants) return []; + const eventsGroupByEndpoint = lodash.groupBy(events, (event) => event.message[0].endpoint); + + Object.keys(eventsGroupByEndpoint).forEach((eventEndPoint) => { + constants = initializeConstants(eventsGroupByEndpoint[eventEndPoint]); + const bathesOfEvents = BatchUtils.chunkArrayBySizeAndLength( + eventsGroupByEndpoint[eventEndPoint], + { + maxSizeInBytes: MAX_PAYLOAD_SIZE, + maxItems: MAX_ITEMS, + }, + ); + bathesOfEvents.items.forEach((batch) => { + const requests: any = buildBatchedRequest(batch, constants, eventEndPoint); + requests.batchedRequest.body.JSON_ARRAY.batch = JSON.stringify( + requests.batchedRequest.body.JSON_ARRAY.batch, + ); + response.push(requests); + }); + }); + 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/networkHandler.js b/src/v1/destinations/bloomreach/networkHandler.js index b9c70a80d1..a3c17a167b 100644 --- a/src/v1/destinations/bloomreach/networkHandler.js +++ b/src/v1/destinations/bloomreach/networkHandler.js @@ -23,33 +23,12 @@ const tags = require('../../../v0/util/tags'); // "end_time": 1710750816.8518236, // "success": true // } -// 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 = Array.isArray(element.errors) - ? element.errors.join(', ') - : Object.values(element.errors || {}) - .flat() - .join(', '); - + const errorMsg = element.errors.join(', '); return { isAbortable: true, errorMsg }; }; @@ -62,11 +41,7 @@ const responseHandler = (responseParams) => { if (isHttpStatusSuccess(status)) { // check for Partial Event failures and Successes - let { results } = response; - // in case of catalog response - if (Array.isArray(response)) { - results = response; - } + const { results } = response; results.forEach((event, idx) => { const proxyOutput = { statusCode: 200, diff --git a/src/v1/destinations/bloomreach_catalog/networkHandler.js b/src/v1/destinations/bloomreach_catalog/networkHandler.js new file mode 100644 index 0000000000..f14e2739a5 --- /dev/null +++ b/src/v1/destinations/bloomreach_catalog/networkHandler.js @@ -0,0 +1,88 @@ +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 = Array.isArray(element.errors) + ? element.errors.join(', ') + : 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 + const results = response; + results.forEach((event, idx) => { + const proxyOutput = { + statusCode: 200, + metadata: rudderJobMetadata[idx], + error: 'success', + }; + // update status of partial event if abortable + const { isAbortable, errorMsg } = checkIfEventIsAbortableAndExtractErrorMessage(event); + if (isAbortable) { + proxyOutput.statusCode = 400; + proxyOutput.error = errorMsg; + } + 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/common.ts b/test/integrations/destinations/bloomreach/common.ts index 2d39937597..33c086efeb 100644 --- a/test/integrations/destinations/bloomreach/common.ts +++ b/test/integrations/destinations/bloomreach/common.ts @@ -12,7 +12,6 @@ const destination: Destination = { projectToken: 'test-project-token', hardID: 'registered', softID: 'cookie', - catalogID: 'test-catalog-id', }, DestinationDefinition: { DisplayName: displayName, @@ -56,12 +55,6 @@ const properties = { key1: 'value1', }; const endpoint = 'https://demoapp-api.bloomreach.com/track/v2/projects/test-project-token/batch'; -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, @@ -91,12 +84,6 @@ const headers = { 'Content-Type': 'application/json', Authorization: 'Basic dGVzdC1hcGkta2V5OnRlc3QtYXBpLXNlY3JldA==', }; - -const sampleContext = { - destinationFields: 'item_id, title, status, unprinted', - mappedToDestination: 'true', -}; - export { destType, channel, @@ -108,8 +95,4 @@ export { properties, endpoint, proxyV1RetryableErrorStatTags, - insertEndpoint, - updateEndpoint, - deleteEndpoint, - sampleContext, }; diff --git a/test/integrations/destinations/bloomreach/dataDelivery/business.ts b/test/integrations/destinations/bloomreach/dataDelivery/business.ts index a130a803ed..9e71b7a2fd 100644 --- a/test/integrations/destinations/bloomreach/dataDelivery/business.ts +++ b/test/integrations/destinations/bloomreach/dataDelivery/business.ts @@ -1,6 +1,6 @@ import { ProxyV1TestData } from '../../../testTypes'; import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; -import { destType, headers, properties, endpoint, updateEndpoint } from '../common'; +import { destType, headers, properties, endpoint } from '../common'; const customerProperties = { email: 'test@example.com', @@ -192,62 +192,4 @@ export const businessProxyV1: ProxyV1TestData[] = [ }, }, }, - { - id: 'bloomreach_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(3)], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: '[BLOOMREACH 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(3), - error: 'Fields [unprinted1] are not properly defined.', - }, - ], - }, - }, - }, - }, - }, ]; diff --git a/test/integrations/destinations/bloomreach/mocks.ts b/test/integrations/destinations/bloomreach/mocks.ts index 3b0d3f719a..ba3b22b52a 100644 --- a/test/integrations/destinations/bloomreach/mocks.ts +++ b/test/integrations/destinations/bloomreach/mocks.ts @@ -2,5 +2,4 @@ import * as config from '../../../../src/cdk/v2/destinations/bloomreach/config'; export const defaultMockFns = () => { jest.replaceProperty(config, 'MAX_BATCH_SIZE', 3 as typeof config.MAX_BATCH_SIZE); - jest.replaceProperty(config, 'MAX_ITEMS', 2 as typeof config.MAX_ITEMS); }; diff --git a/test/integrations/destinations/bloomreach/network.ts b/test/integrations/destinations/bloomreach/network.ts index 9fc4743928..b20ff881b8 100644 --- a/test/integrations/destinations/bloomreach/network.ts +++ b/test/integrations/destinations/bloomreach/network.ts @@ -1,4 +1,4 @@ -import { destType, headers, properties, endpoint, updateEndpoint } from './common'; +import { destType, headers, properties, endpoint } from './common'; export const networkCallsData = [ { @@ -121,33 +121,4 @@ export const networkCallsData = [ 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/processor/validation.ts b/test/integrations/destinations/bloomreach/processor/validation.ts index 746598cd99..1a6199abb0 100644 --- a/test/integrations/destinations/bloomreach/processor/validation.ts +++ b/test/integrations/destinations/bloomreach/processor/validation.ts @@ -42,7 +42,7 @@ export const validation: ProcessorTestData[] = [ body: [ { error: - 'Either one of userId or anonymousId is required. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: validateContactAndEventsInput, OriginalError: Either one of userId or anonymousId is required. Aborting', + 'Either one of userId or anonymousId is required. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Either one of userId or anonymousId is required. Aborting', metadata: generateMetadata(1), statTags: processorInstrumentationErrorStatTags, statusCode: 400, @@ -86,7 +86,7 @@ export const validation: ProcessorTestData[] = [ body: [ { error: - 'message type group is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: validateContactAndEventsInput, OriginalError: message type group is not supported', + 'message type group is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: message type group is not supported', metadata: generateMetadata(1), statTags: processorInstrumentationErrorStatTags, statusCode: 400, @@ -126,7 +126,7 @@ export const validation: ProcessorTestData[] = [ body: [ { error: - 'Timestamp is not present. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: validateContactAndEventsInput, OriginalError: Timestamp is not present. Aborting', + 'Timestamp is not present. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Timestamp is not present. Aborting', metadata: generateMetadata(1), statTags: processorInstrumentationErrorStatTags, statusCode: 400, @@ -202,45 +202,4 @@ export const validation: ProcessorTestData[] = [ }, }, }, - { - id: 'bloomreach-validation-test-5', - name: destType, - description: 'Missing message action for record event type', - scenario: 'Framework', - successCriteria: 'Instrumentation Error', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - destination, - message: { - type: 'record', - integrations: { - All: true, - }, - originalTimestamp: '2024-03-04T15:32:56.409Z', - }, - metadata: generateMetadata(1), - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: - 'message type record is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: validateContactAndEventsInput, OriginalError: message type record is not supported', - metadata: generateMetadata(1), - statTags: processorInstrumentationErrorStatTags, - statusCode: 400, - }, - ], - }, - }, - }, ]; diff --git a/test/integrations/destinations/bloomreach/router/data.ts b/test/integrations/destinations/bloomreach/router/data.ts index d4e7bf716d..e99d0cc8cd 100644 --- a/test/integrations/destinations/bloomreach/router/data.ts +++ b/test/integrations/destinations/bloomreach/router/data.ts @@ -8,10 +8,6 @@ import { headers, endpoint, RouterInstrumentationErrorStatTags, - insertEndpoint, - updateEndpoint, - deleteEndpoint, - sampleContext, } from '../common'; const routerRequest = { @@ -88,147 +84,6 @@ const routerRequest = { ], destType, }; - -const routerRequest1 = { - 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, - }, - ], - destType, -}; - export const data = [ { id: 'bloomreach-router-test-1', @@ -362,132 +217,4 @@ export const data = [ }, mockFns: defaultMockFns, }, - { - id: 'bloomreach-router-test-2', - 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: routerRequest1, - 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: 'Item Id is required. Aborting', - statTags: RouterInstrumentationErrorStatTags, - destination, - }, - ], - }, - }, - }, - mockFns: defaultMockFns, - }, ]; 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/business.ts b/test/integrations/destinations/bloomreach_catalog/dataDelivery/business.ts new file mode 100644 index 0000000000..23a9b06b51 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/dataDelivery/business.ts @@ -0,0 +1,67 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; +import { destType, headers, updateEndpoint } from '../common'; + +const metadataArray = [generateMetadata(1), generateMetadata(2)]; + +// https://documentation.bloomreach.com/engagement/reference/tips-and-best-practices +export const businessProxyV1: ProxyV1TestData[] = [ + { + id: 'bloomreach_catalog_v1_business_scenario_1', + 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(3)], + ), + 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(3), + error: 'Fields [unprinted1] are not properly defined.', + }, + ], + }, + }, + }, + }, + }, +]; 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..5099eafce7 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/dataDelivery/data.ts @@ -0,0 +1,3 @@ +import { businessProxyV1 } from './business'; +import { otherProxyV1 } from './other'; +export const data = [...businessProxyV1, ...otherProxyV1]; diff --git a/test/integrations/destinations/bloomreach_catalog/dataDelivery/other.ts b/test/integrations/destinations/bloomreach_catalog/dataDelivery/other.ts new file mode 100644 index 0000000000..8fc15aa5f4 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/dataDelivery/other.ts @@ -0,0 +1,212 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; +import { destType, proxyV1RetryableErrorStatTags } from '../common'; + +const metadataArray = [generateMetadata(1)]; + +// https://documentation.bloomreach.com/engagement/reference/tips-and-best-practices +export const otherProxyV1: ProxyV1TestData[] = [ + { + id: 'bloomreach_catalog_v1_other_scenario_1', + name: destType, + description: + '[Proxy v1 API] :: Scenario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_service_not_available', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 503, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH_CATALOG: Error encountered in transformer proxy V1', + status: 503, + }, + }, + }, + }, + }, + { + id: 'bloomreach_catalog_v1_other_scenario_2', + name: destType, + description: '[Proxy v1 API] :: Scenario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_internal_server_error', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Internal Server Error"', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH_CATALOG: Error encountered in transformer proxy V1', + status: 500, + }, + }, + }, + }, + }, + { + id: 'bloomreach_catalog_v1_other_scenario_3', + name: destType, + description: '[Proxy v1 API] :: Scenario for testing Gateway Time Out error from destination', + successCriteria: 'Should return 504 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_gateway_time_out', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Gateway Timeout"', + statusCode: 504, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH_CATALOG: Error encountered in transformer proxy V1', + status: 504, + }, + }, + }, + }, + }, + { + id: 'bloomreach_catalog_v1_other_scenario_4', + name: destType, + description: '[Proxy v1 API] :: Scenario for testing null response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_null_response', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH_CATALOG: Error encountered in transformer proxy V1', + status: 500, + }, + }, + }, + }, + }, + { + id: 'bloomreach_catalog_v1_other_scenario_5', + name: destType, + description: + '[Proxy v1 API] :: Scenario for testing null and no status response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_null_and_no_status', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH_CATALOG: Error encountered in transformer proxy V1', + status: 500, + }, + }, + }, + }, + }, +]; 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..ca71862ae1 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/network.ts @@ -0,0 +1,33 @@ +import { destType, headers, updateEndpoint } from './common'; + +export const networkCallsData = [ + { + 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/processor/data.ts b/test/integrations/destinations/bloomreach_catalog/processor/data.ts new file mode 100644 index 0000000000..8e5419b3d2 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/processor/data.ts @@ -0,0 +1,2 @@ +import { validation } from './validation'; +export const data = [...validation]; diff --git a/test/integrations/destinations/bloomreach_catalog/processor/validation.ts b/test/integrations/destinations/bloomreach_catalog/processor/validation.ts new file mode 100644 index 0000000000..0300168577 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/processor/validation.ts @@ -0,0 +1,47 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata } from '../../../testUtils'; +import { destType, destination, processorInstrumentationErrorStatTags } from '../common'; + +export const validation: ProcessorTestData[] = [ + { + id: 'bloomreach-catalog-validation-test-1', + name: destType, + description: 'Missing message action for record event type', + scenario: 'Framework', + successCriteria: 'Instrumentation Error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'record', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'message action undefined is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: message action undefined is not supported', + metadata: generateMetadata(1), + statTags: processorInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, +]; 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..766b41a669 --- /dev/null +++ b/test/integrations/destinations/bloomreach_catalog/router/data.ts @@ -0,0 +1,283 @@ +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, + }, + ], + 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: 'Item Id is required. Aborting', + statTags: RouterInstrumentationErrorStatTags, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +];