Skip to content

Commit

Permalink
feat: onboard sfmc with vdm for rETL (#3655)
Browse files Browse the repository at this point in the history
* feat: onboard sfmc with vdm for rETL

* chore: add test cases and add cache for access token

* chore: address comment
  • Loading branch information
ItsSudip authored Aug 20, 2024
1 parent 06f5d33 commit d987d1f
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/v0/destinations/sfmc/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const ENDPOINTS = {
EVENT: 'rest.marketingcloudapis.com/interaction/v1/events',
};

const ACCESS_TOKEN_CACHE_TTL = process.env.SFMC_ACCESS_TOKEN_CACHE_TTL
? parseInt(process.env.SFMC_ACCESS_TOKEN_CACHE_TTL, 10)
: 1000;

const CONFIG_CATEGORIES = {
IDENTIFY: {
type: 'identify',
Expand All @@ -24,4 +28,5 @@ module.exports = {
ENDPOINTS,
MAPPING_CONFIG,
CONFIG_CATEGORIES,
ACCESS_TOKEN_CACHE_TTL,
};
46 changes: 44 additions & 2 deletions src/v0/destinations/sfmc/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@ const {
InstrumentationError,
isDefinedAndNotNull,
isEmpty,
MappedToDestinationKey,
GENERIC_TRUE_VALUES,
PlatformError,
} = require('@rudderstack/integrations-lib');
const get = require('get-value');
const { EventType } = require('../../../constants');
const { handleHttpRequest } = require('../../../adapters/network');
const { CONFIG_CATEGORIES, MAPPING_CONFIG, ENDPOINTS } = require('./config');
const {
CONFIG_CATEGORIES,
MAPPING_CONFIG,
ENDPOINTS,
ACCESS_TOKEN_CACHE_TTL,
} = require('./config');
const {
removeUndefinedAndNullValues,
getFieldValueFromMessage,
Expand All @@ -21,12 +30,15 @@ const {
toTitleCase,
getHashFromArray,
simpleProcessRouterDest,
getDestinationExternalIDInfoForRetl,
} = require('../../util');
const { getDynamicErrorType } = require('../../../adapters/utils/networkUtils');
const { isHttpStatusSuccess } = require('../../util');
const tags = require('../../util/tags');
const { JSON_MIME_TYPE } = require('../../util/constant');
const Cache = require('../../util/cache');

const accessTokenCache = new Cache(ACCESS_TOKEN_CACHE_TTL);
const CONTACT_KEY_KEY = 'Contact Key';

// DOC: https://developer.salesforce.com/docs/atlas.en-us.mc-app-development.meta/mc-app-development/access-token-s2s.htm
Expand Down Expand Up @@ -271,11 +283,41 @@ const responseBuilderSimple = async ({ message, destination, metadata }, categor
throw new ConfigurationError(`Event type '${category.type}' not supported`);
};

const retlResponseBuilder = async (message, destination, metadata) => {
const { clientId, clientSecret, subDomain, externalKey } = destination.Config;
const token = await accessTokenCache.get(metadata.destinationId, () =>
getToken(clientId, clientSecret, subDomain, metadata),
);
const { destinationExternalId, objectType, identifierType } = getDestinationExternalIDInfoForRetl(
message,
'SFMC',
);
if (objectType?.toLowerCase() === 'data extension') {
const response = defaultRequestConfig();
response.method = defaultPutRequestConfig.requestMethod;
response.endpoint = `https://${subDomain}.${ENDPOINTS.INSERT_CONTACTS}${externalKey}/rows/${identifierType}:${destinationExternalId}`;
response.headers = {
'Content-Type': JSON_MIME_TYPE,
Authorization: `Bearer ${token}`,
};
response.body.JSON = {
values: {
...message.traits,
},
};
return response;
}
throw new PlatformError('Unsupported object type for rETL use case');
};

const processEvent = async ({ message, destination, metadata }) => {
if (!message.type) {
throw new InstrumentationError('Event type is required');
}

const mappedToDestination = get(message, MappedToDestinationKey);
if (mappedToDestination && GENERIC_TRUE_VALUES.includes(mappedToDestination?.toString())) {
return retlResponseBuilder(message, destination, metadata);
}
const messageType = message.type.toLowerCase();
let category;
// only accept track and identify calls
Expand Down
178 changes: 178 additions & 0 deletions test/integrations/destinations/sfmc/processor/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2216,4 +2216,182 @@ export const data = [
},
},
},
{
name: 'sfmc',
description: 'success scenario for rETL use case',
feature: 'processor',
id: 'sfmcRetlTestCase-1',
module: 'destination',
version: 'v0',
input: {
request: {
body: [
{
message: {
type: 'identify',
traits: {
key2: 'value2',
key3: 'value3',
key4: 'value4',
},
userId: '[email protected]',
channel: 'sources',
context: {
sources: {
job_id: '2kbW13URkJ6jfeo5SbFcC7ecP6d',
version: 'v1.53.1',
job_run_id: 'cqtl6pfqskjtoh6t24i0',
task_run_id: 'cqtl6pfqskjtoh6t24ig',
},
externalId: [
{
id: '[email protected]',
type: 'SFMC-data extension',
identifierType: 'key1',
},
],
mappedToDestination: 'true',
},
recordId: '3',
rudderId: 'c5741aa5-b038-4079-99ec-e4169eb0d9e2',
messageId: '95a1b214-03d9-4824-8ada-bc6ef2398100',
},
destination: {
ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
Name: 'SFMC',
Config: {
clientId: 'dummyClientId',
clientSecret: 'dummyClientSecret',
subDomain: 'vcn7AQ2W9GGIAZSsN6Mfq',
createOrUpdateContacts: false,
externalKey: 'externalKey',
},
Enabled: true,
Transformations: [],
},
metadata: {
destinationId: 'destId',
jobId: 'jobid1',
},
},
],
},
},
output: {
response: {
status: 200,
body: [
{
metadata: {
destinationId: 'destId',
jobId: 'jobid1',
},
output: {
body: {
FORM: {},
JSON: {
values: {
key2: 'value2',
key3: 'value3',
key4: 'value4',
},
},
JSON_ARRAY: {},
XML: {},
},
endpoint:
'https://vcn7AQ2W9GGIAZSsN6Mfq.rest.marketingcloudapis.com/hub/v1/dataevents/key:externalKey/rows/key1:[email protected]',
files: {},
headers: {
Authorization: 'Bearer yourAuthToken',
'Content-Type': 'application/json',
},
method: 'PUT',
params: {},
type: 'REST',
userId: '',
version: '1',
},
statusCode: 200,
},
],
},
},
},
{
name: 'sfmc',
description: 'failure scenario for rETL use case when wrong object type is used',
feature: 'processor',
id: 'sfmcRetlTestCase-2',
module: 'destination',
version: 'v0',
input: {
request: {
body: [
{
message: {
type: 'identify',
traits: {
key2: 'value2',
key3: 'value3',
key4: 'value4',
},
userId: '[email protected]',
channel: 'sources',
context: {
externalId: [
{
id: '[email protected]',
type: 'SFMC-contacts',
identifierType: 'key1',
},
],
mappedToDestination: 'true',
},
},
destination: {
ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
Name: 'SFMC',
Config: {
clientId: 'dummyClientId',
clientSecret: 'dummyClientSecret',
subDomain: 'vcn7AQ2W9GGIAZSsN6Mfq',
createOrUpdateContacts: false,
externalKey: 'externalKey',
},
Enabled: true,
Transformations: [],
},
metadata: {
destinationId: 'destId',
jobId: 'jobid1',
},
},
],
},
},
output: {
response: {
status: 200,
body: [
{
error: 'Unsupported object type for rETL use case',
metadata: {
destinationId: 'destId',
jobId: 'jobid1',
},
statTags: {
destType: 'SFMC',
destinationId: 'destId',
errorCategory: 'platform',
feature: 'processor',
implementation: 'native',
module: 'destination',
},
statusCode: 400,
},
],
},
},
},
];

0 comments on commit d987d1f

Please sign in to comment.