-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into chore.upgrade-pkgs
- Loading branch information
Showing
47 changed files
with
4,218 additions
and
471 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const SMS_SEND_ENDPOINT = 'https://rest.clicksend.com/v3/sms/send'; | ||
const SMS_CAMPAIGN_ENDPOINT = 'https://rest.clicksend.com/v3/sms-campaigns/send'; | ||
const COMMON_CONTACT_DOMAIN = 'https://rest.clicksend.com/v3/lists'; | ||
|
||
module.exports = { | ||
SMS_SEND_ENDPOINT, | ||
SMS_CAMPAIGN_ENDPOINT, | ||
MAX_BATCH_SIZE: 1000, | ||
COMMON_CONTACT_DOMAIN, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
bindings: | ||
- name: EventType | ||
path: ../../../../constants | ||
- path: ../../bindings/jsontemplate | ||
- name: defaultRequestConfig | ||
path: ../../../../v0/util | ||
- name: removeUndefinedAndNullValues | ||
path: ../../../../v0/util | ||
- name: isDefinedAndNotNull | ||
path: ../../../../v0/util | ||
- path: ./utils | ||
- name: getDestinationExternalID | ||
path: ../../../../v0/util | ||
- name: base64Convertor | ||
path: ../../../../v0/util | ||
- path: ./config | ||
|
||
steps: | ||
- name: messageType | ||
template: | | ||
.message.type.toLowerCase(); | ||
- name: validateInput | ||
template: | | ||
let messageType = $.outputs.messageType; | ||
$.assert(messageType, "message Type is not present. Aborting"); | ||
$.assert(messageType in {{$.EventType.([.TRACK,.IDENTIFY])}}, "message type " + messageType + " is not supported"); | ||
$.assertConfig(.destination.Config.clicksendUsername, "Clicksend user name is not present. Aborting"); | ||
$.assertConfig(.destination.Config.clicksendPassword, "Click send password is not present. Aborting"); | ||
- name: prepareIdentifyPayload | ||
condition: $.outputs.messageType === {{$.EventType.IDENTIFY}} | ||
template: | | ||
const payload = .message.({ | ||
email: {{{{$.getGenericPaths("emailOnly")}}}}, | ||
phone_number: {{{{$.getGenericPaths("phone")}}}}, | ||
first_name: {{{{$.getGenericPaths("firstName")}}}}, | ||
last_name: {{{{$.getGenericPaths("lastName")}}}}, | ||
address_line_1: .traits.address_line_1 || .context.traits.address_line_1 || JSON.stringify({{{{$.getGenericPaths("address")}}}}) , | ||
address_line_2: .traits.address_line_2 || .context.traits.address_line_2 || JSON.stringify({{{{$.getGenericPaths("address")}}}}) , | ||
city: {{{{$.getGenericPaths("city")}}}}, | ||
state: {{{{$.getGenericPaths("state")}}}}, | ||
country: {{{{$.getGenericPaths("country")}}}}, | ||
fax_number: .traits.fax_number || .context.traits.fax_number, | ||
organization_name: .traits.fax_number || .context.traits.fax_number, | ||
}); | ||
$.validateIdentifyPayload(payload); | ||
payload.contact_id = $.getDestinationExternalID(.message,'CLICKSEND_CONTACT_ID'); | ||
const contactList = $.getDestinationExternalID(.message,'CLICKSEND_CONTACT_LIST_ID'); | ||
$.assert(contactList, "externalId does not contain contact list Id of Clicksend. Aborting."); | ||
$.context.endPoint = $.getEndIdentifyPoint(payload.contact_id, contactList); | ||
$.context.payload = $.removeUndefinedAndNullValues(payload); | ||
$.context.method = payload.contact_id ? 'PUT' : 'POST'; | ||
- name: prepareTrackPayload | ||
condition: $.outputs.messageType === {{$.EventType.TRACK}} | ||
steps: | ||
- name: sendSmsCampaignPayload | ||
condition: $.isDefinedAndNotNull($.getDestinationExternalID(^.message,'CLICKSEND_CONTACT_LIST_ID')) | ||
template: | | ||
const sendCampaignPayload = .message.({ | ||
list_id : parseInt($.getDestinationExternalID(^.message,'CLICKSEND_CONTACT_LIST_ID'), 10), | ||
name : String(.properties.name), | ||
body : String(.properties.body), | ||
from : $.getDestinationExternalID(^.message,'CLICKSEND_SENDER_EMAIL') || ^.destination.Config.defaultSenderEmail, | ||
schedule : $.deduceSchedule(.properties.schedule,{{{{$.getGenericPaths("timestamp")}}}}, ^.destination.Config) | ||
}); | ||
$.assert(!Number.isNaN(sendCampaignPayload.list_id), "list_id must be an integer"); | ||
$.validateTrackSMSCampaignPayload(sendCampaignPayload); | ||
$.context.payload = $.removeUndefinedAndNullValues(sendCampaignPayload); | ||
$.context.endPoint = $.SMS_CAMPAIGN_ENDPOINT; | ||
$.context.method = 'POST'; | ||
else: | ||
name: sendSmsPayload | ||
template: | | ||
const sendSmsPayload = .message.({ | ||
from: $.getDestinationExternalID(^.message,'CLICKSEND_SENDER_EMAIL') || ^.destination.Config.defaultSenderEmail, | ||
email: {{{{$.getGenericPaths("emailOnly")}}}}, | ||
to: {{{{$.getGenericPaths("phone")}}}}, | ||
body: .properties.body, | ||
source: .properties.source || ^.destination.Config.defaultSource, | ||
schedule: $.deduceSchedule(.properties.schedule, {{{{$.getGenericPaths("timestamp")}}}}, ^.destination.Config), | ||
custom_string: .properties.custom_string, | ||
country: {{{{$.getGenericPaths("country")}}}}, | ||
from_email: .properties.from_email | ||
}); | ||
$.assert((sendSmsPayload.from && sendSmsPayload.to && sendSmsPayload.body), "all of sender email, phone and body needs to be present for track call"); | ||
$.context.payload = $.removeUndefinedAndNullValues(sendSmsPayload); | ||
$.context.endPoint = $.SMS_SEND_ENDPOINT; | ||
$.context.method = 'POST'; | ||
- name: buildResponse | ||
template: | | ||
const response = $.defaultRequestConfig(); | ||
response.body.JSON = $.context.payload; | ||
response.endpoint = $.context.endPoint; | ||
response.method = $.context.method; | ||
response.headers = { | ||
Authorization : "Basic " + $.base64Convertor(.destination.Config.clicksendUsername + ":" + .destination.Config.clicksendPassword), | ||
"Content-Type" : "application/json", | ||
}; | ||
response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
bindings: | ||
- path: ./utils | ||
- name: handleRtTfSingleEventError | ||
path: ../../../../v0/util/index | ||
|
||
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: | | ||
$.context.batchedPayload = $.batchResponseBuilder($.outputs.successfulEvents); | ||
- name: finalPayload | ||
template: | | ||
[...$.outputs.failedEvents, ...$.context.batchedPayload] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
const { InstrumentationError } = require('@rudderstack/integrations-lib'); | ||
const lodash = require('lodash'); | ||
const { BatchUtils } = require('@rudderstack/workflow-engine'); | ||
const { SMS_SEND_ENDPOINT, MAX_BATCH_SIZE, COMMON_CONTACT_DOMAIN } = require('./config'); | ||
const { isDefinedAndNotNullAndNotEmpty, isDefinedAndNotNull } = require('../../../../v0/util'); | ||
|
||
const getEndIdentifyPoint = (contactId, contactListId) => | ||
`${COMMON_CONTACT_DOMAIN}/${contactListId}/contacts${isDefinedAndNotNullAndNotEmpty(contactId) ? `/${contactId}` : ''}`; | ||
|
||
const validateIdentifyPayload = (payload) => { | ||
if ( | ||
!( | ||
isDefinedAndNotNullAndNotEmpty(payload.phone_number) || | ||
isDefinedAndNotNullAndNotEmpty(payload.email) || | ||
isDefinedAndNotNullAndNotEmpty(payload.fax_number) | ||
) | ||
) { | ||
throw new InstrumentationError( | ||
'Either phone number or email or fax_number is mandatory for contact creation', | ||
); | ||
} | ||
}; | ||
|
||
const validateTrackSMSCampaignPayload = (payload) => { | ||
if (!(payload.body && payload.name && payload.list_id && payload.from)) { | ||
throw new InstrumentationError( | ||
'All of contact list Id, name, body and from are required to trigger an sms campaign', | ||
); | ||
} | ||
}; | ||
|
||
const deduceSchedule = (eventLevelSchedule, timestamp, destConfig) => { | ||
if (isDefinedAndNotNull(eventLevelSchedule) && !Number.isNaN(eventLevelSchedule)) { | ||
return eventLevelSchedule; | ||
} | ||
const { defaultCampaignScheduleUnit = 'minute', defaultCampaignSchedule = 0 } = destConfig; | ||
const date = new Date(timestamp); | ||
|
||
if (defaultCampaignScheduleUnit === 'day') { | ||
date.setDate(date.getDate() + defaultCampaignSchedule); | ||
} else if (defaultCampaignScheduleUnit === 'minute') { | ||
date.setMinutes(date.getMinutes() + defaultCampaignSchedule); | ||
} else { | ||
throw new Error("Invalid delta unit. Use 'day' or 'minute'."); | ||
} | ||
|
||
return Math.floor(date.getTime() / 1000); | ||
}; | ||
|
||
const mergeMetadata = (batch) => batch.map((event) => event.metadata); | ||
|
||
const getMergedEvents = (batch) => batch.map((event) => event.message[0].body.JSON); | ||
|
||
const getHttpMethodForEndpoint = (endpoint) => { | ||
const contactIdPattern = /\/contacts\/[^/]+$/; | ||
return contactIdPattern.test(endpoint) ? 'PUT' : 'POST'; | ||
}; | ||
|
||
const buildBatchedRequest = (batch, constants, endpoint) => ({ | ||
batchedRequest: { | ||
body: { | ||
JSON: | ||
endpoint === SMS_SEND_ENDPOINT | ||
? { messages: getMergedEvents(batch) } | ||
: batch[0].message[0].body.JSON, | ||
JSON_ARRAY: {}, | ||
XML: {}, | ||
FORM: {}, | ||
}, | ||
version: '1', | ||
type: 'REST', | ||
method: getHttpMethodForEndpoint(endpoint), | ||
endpoint, | ||
headers: constants.headers, | ||
params: {}, | ||
files: {}, | ||
}, | ||
metadata: mergeMetadata(batch), | ||
batched: endpoint === SMS_SEND_ENDPOINT, | ||
statusCode: 200, | ||
destination: batch[0].destination, | ||
}); | ||
|
||
const initializeConstants = (successfulEvents) => { | ||
if (successfulEvents.length === 0) return null; | ||
return { | ||
version: successfulEvents[0].message[0].version, | ||
type: successfulEvents[0].message[0].type, | ||
headers: successfulEvents[0].message[0].headers, | ||
destination: successfulEvents[0].destination, | ||
endPoint: successfulEvents[0].message[0].endpoint, | ||
}; | ||
}; | ||
|
||
const batchResponseBuilder = (events) => { | ||
const response = []; | ||
const constants = initializeConstants(events); | ||
if (!constants) return []; | ||
const typedEventGroups = lodash.groupBy(events, (event) => event.message[0].endpoint); | ||
|
||
Object.keys(typedEventGroups).forEach((eventEndPoint) => { | ||
if (eventEndPoint === SMS_SEND_ENDPOINT) { | ||
const batchesOfSMSEvents = BatchUtils.chunkArrayBySizeAndLength( | ||
typedEventGroups[eventEndPoint], | ||
{ maxItems: MAX_BATCH_SIZE }, | ||
); | ||
batchesOfSMSEvents.items.forEach((batch) => { | ||
response.push(buildBatchedRequest(batch, constants, eventEndPoint)); | ||
}); | ||
} else { | ||
response.push( | ||
buildBatchedRequest([typedEventGroups[eventEndPoint][0]], constants, eventEndPoint), | ||
); | ||
} | ||
}); | ||
|
||
return response; | ||
}; | ||
|
||
module.exports = { | ||
batchResponseBuilder, | ||
getEndIdentifyPoint, | ||
validateIdentifyPayload, | ||
validateTrackSMSCampaignPayload, | ||
deduceSchedule, | ||
getHttpMethodForEndpoint, | ||
}; |
Oops, something went wrong.