mcdev
Version:
Accenture Salesforce Marketing Cloud DevTools
913 lines (873 loc) • 170 kB
JavaScript
'use strict';
import MetadataType from './MetadataType.js';
import TransactionalEmail from './TransactionalEmail.js';
import TriggeredSend from './TriggeredSend.js';
import Event from './Event.js';
import { Util } from '../util/util.js';
import cache from '../util/cache.js';
import File from '../util/file.js';
import ReplaceCbReference from '../util/replaceContentBlockReference.js';
import Retriever from '../Retriever.js';
import pLimit from 'p-limit';
import yoctoSpinner from 'yocto-spinner';
/**
* @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
* @typedef {import('../../types/mcdev.d.js').CodeExtract} CodeExtract
* @typedef {import('../../types/mcdev.d.js').CodeExtractItem} CodeExtractItem
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItem} MetadataTypeItem
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItemDiff} MetadataTypeItemDiff
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItemObj} MetadataTypeItemObj
* @typedef {import('../../types/mcdev.d.js').MetadataTypeMap} MetadataTypeMap
* @typedef {import('../../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj
* @typedef {import('../../types/mcdev.d.js').SoapRequestParams} SoapRequestParams
* @typedef {import('../../types/mcdev.d.js').TemplateMap} TemplateMap
* @typedef {import('../../types/mcdev.d.js').TypeKeyCombo} TypeKeyCombo
*/
/**
* Journey MetadataType
* id: A unique id of the journey assigned by the journey’s API during its creation
* key: A unique id of the journey within the MID. Can be generated by the developer
* definitionId: A unique UUID provided by Salesforce Marketing Cloud. Each version of a journey has a unique DefinitionID while the Id and Key remain the same. Version 1 will have id == definitionId
*
* @augments MetadataType
*/
class Journey extends MetadataType {
/**
* Retrieves Metadata of Journey
*
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
* @param {void | string[]} [_] unused parameter
* @param {void | string[]} [__] unused parameter
* @param {string} [key] customer key of single item to retrieve
* @returns {Promise.<MetadataTypeMapObj>} Promise
*/
static async retrieve(retrieveDir, _, __, key) {
const extrasDefault = 'activities';
let singleKey = '';
let mode = 'all';
if (key) {
if (key.startsWith('%23')) {
// correct the format
key = 'id:' + key.slice(3);
}
if (key.startsWith('id:')) {
// ! allow selecting journeys by ID because that's what users see in the URL
// if the key started with %23 assume an ID was copied from the URL but the user forgot to prefix it with id:
// remove id: or %23
singleKey = key.slice(3);
if (singleKey.startsWith('%23')) {
// in the journey URL the Id is prefixed with an HTML-encoded "#" which could accidentally be copied by users
// despite the slicing above, this still needs testing here because users might have prefixed the ID with id: but did not know to remove the #23
singleKey = singleKey.slice(3);
// correct the format to ensure we show sth readable in the "Downloaded" log
key = 'id:' + singleKey;
}
if (singleKey.includes('/')) {
// in the journey URL the version is appended after the ID, separated by a forward-slash. Needs to be removed from the ID for the retrieve as we always aim to retrieve the latest version only
singleKey = singleKey.split('/')[0];
}
mode = 'id';
} else if (key.startsWith('name:')) {
singleKey = '?nameOrDescription=' + encodeURIComponent(key.slice(5));
mode = 'name';
} else {
// assume actual key was provided
singleKey = 'key:' + encodeURIComponent(key);
mode = 'key';
}
}
try {
const uri = `/interaction/v1/interactions/`;
if ((singleKey && (mode === 'key' || mode === 'id')) || !retrieveDir) {
// full details for retrieve, only base data for caching; reduces caching time from minutes to seconds
const extras = retrieveDir && singleKey ? extrasDefault : '';
// caching or single retrieve
return await super.retrieveREST(
retrieveDir,
`${uri}${singleKey}?extras=${extras}${key && key.includes('/') ? '&versionNumber=' + key.split('/')[1] : ''}`,
null,
key
);
} else {
// retrieve all
const results = this.definition.restPagination
? await this.client.rest.getBulk(
uri + (mode === 'name' ? singleKey : ''),
this.definition.restPageSize || 500
)
: await this.client.rest.get(uri + (mode === 'name' ? singleKey : ''));
if (results.items?.length) {
// empty results will come back without "items" defined
Util.logger.info(
Util.getGrayMsg(
` - ${results.items?.length} ${this.definition.type}s found. Retrieving details...`
)
);
}
// full details for retrieve
const extras = extrasDefault;
let parsed;
if (retrieveDir) {
const searchName = mode === 'name' ? key.slice(5) : null;
const foundKey = [];
// get extra details for saving this
const details = results.items
? await Promise.all(
results.items.map(async (a) => {
if (mode === 'name') {
// when filtering by name, the API in fact does a LIKE search with placeholders left and right of the search term - and also searches the description field.
if (searchName === a[this.definition.nameField]) {
foundKey.push(a[this.definition.keyField]);
} else {
// skip because the name does not match
return null;
}
}
try {
return await this.client.rest.get(
`${uri}key:${a[this.definition.keyField]}?extras=${extras}` +
`&versionNumber=${a.version}`
);
} catch (ex) {
// if we do get here, we should log the error and continue instead of failing to download all automations
Util.logger.warn(
` ☇ skipping ${this.definition.type} ${
a[this.definition.nameField]
} (${a[this.definition.keyField]}): ${ex.message} (${
ex.code
})${
ex.endpoint
? Util.getGrayMsg(
' - ' +
ex.endpoint.split(
'rest.marketingcloudapis.com'
)[1]
)
: ''
}`
);
return null;
}
})
)
: [];
parsed = this.parseResponseBody({ items: details.filter(Boolean) });
// * retrieveDir is mandatory in this method as it is not used for caching (there is a seperate method for that)
const savedMetadata = await this.saveResults(parsed, retrieveDir, null, null);
Util.logger.info(
`Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
Util.getKeysString(
mode === 'name' ? `${foundKey.join(', ')} (${key})` : key
)
);
} else {
// limit to main details for caching
parsed = this.parseResponseBody(results);
}
return {
metadata: parsed,
type: this.definition.type,
};
}
} catch (ex) {
// if the interaction does not exist, the API returns an error code which would otherwise bring execution to a hold
if (
[
'Interaction matching key not found.', // might be outdated
'Interaction matching criteria not found.', // seen in 2025-05
'Must provide a valid ID or Key parameter',
].includes(ex.message) ||
(key && ex.code === 'ERR_BAD_REQUEST')
) {
Util.logger.info(
`Downloaded: ${this.definition.type} (0)${Util.getKeysString(
mode === 'id' ? singleKey : key,
mode === 'id'
)}`
);
if (mode === 'key') {
this.postDeleteTasks(key);
}
} else {
throw ex;
}
}
}
/**
* Delete a metadata item from the specified business unit
*
* @param {string} key Identifier of item
* @returns {Promise.<boolean>} deletion success status
*/
static async deleteByKey(key) {
let version;
let id;
let cachedJourney;
if (key.startsWith('id:') || key.startsWith('%23')) {
// ! allow selecting journeys by ID because that's what users see in the URL
// if the key started with %23 assume an ID was copied from the URL but the user forgot to prefix it with id:
// remove id: or %23
id = key.slice(3);
if (id.startsWith('%23')) {
// in the journey URL the Id is prefixed with an HTML-encoded "#" which could accidentally be copied by users
// despite the slicing above, this still needs testing here because users might have prefixed the ID with id: but did not know to remove the #23
id = id.slice(3);
}
if (id.includes('/')) {
// in the journey URL the version is appended after the ID, separated by a forward-slash.
[id, version] = id.split('/');
}
try {
const response = await this.client.rest.get(
`/interaction/v1/interactions/${encodeURIComponent(id)}?extras=activities`
);
const results = this.parseResponseBody(response, key);
cachedJourney = results[key];
} catch {
// handle below
}
} else {
if (key.includes('/')) {
// in the journey URL the version is appended after the ID, separated by a forward-slash.
[key, version] = key.split('/');
}
// delete by key with specified version does not work, therefore we need to get the ID first
try {
const response = await this.client.rest.get(
`/interaction/v1/interactions/key:${encodeURIComponent(key)}?extras=activities`
);
const results = this.parseResponseBody(response, key);
cachedJourney = results[key];
id = cachedJourney?.id;
} catch {
// handle below
}
}
if (!cachedJourney?.key) {
await this.deleteNotFound(key);
return false;
}
switch (cachedJourney.definitionType) {
case 'Multistep': {
if (version && version !== '*' && version > cachedJourney.version) {
Util.logger.error(
`The chosen version (${version}) is higher than the latest known version (${cachedJourney.version}). Please choose a lower version.`
);
return false;
}
if (version !== '*') {
if (!/^\d+$/.test(version)) {
Util.logger.error(
'Version is required for deleting journeys to avoid accidental deletion of the wrong item. Please append it at the end of the key or id, separated by forward-slash. Example for deleting version 4: ' +
key +
'/4'
);
return false;
}
Util.logger.warn(
`Deleting Journeys via this command breaks following retrieve-by-key/id requests until you've deployed/created a new draft version! You can get still get the latest available version of your journey by retrieving all journeys on this BU.`
);
}
return super.deleteByKeyREST(
'/interaction/v1/interactions/' +
id +
(version === '*' ? '' : `?versionNumber=${version}`),
key
);
// break;
}
case 'Quicksend': {
// Quicksend doesnt have versions
const isDeleted = await super.deleteByKeyREST(
'/interaction/v1/interactions/' + id,
key
);
return isDeleted;
}
case 'Transactional': {
// Transactional dont have versions
const isDeleted = await super.deleteByKeyREST(
'/interaction/v1/interactions/' + id,
key
);
const transactionalEmailKey =
cachedJourney.activities[0]?.configurationArguments?.triggeredSendKey;
if (isDeleted) {
const msg = [];
if (cachedJourney.activities[0]?.configurationArguments?.triggeredSendKey) {
msg.push(
`transactionalEmail "${cachedJourney.activities[0].configurationArguments.triggeredSendKey}"`
);
}
}
if (isDeleted && transactionalEmailKey) {
Util.logger.info(
` - deleted ${TransactionalEmail.definition.type}: ${transactionalEmailKey} (SFMC auto-deletes the related transactionalEmail of ${this.definition.type} ${key})`
);
TransactionalEmail.buObject = this.buObject;
TransactionalEmail.properties = this.properties;
TransactionalEmail.client = this.client;
TransactionalEmail.postDeleteTasks(transactionalEmailKey);
}
return isDeleted;
}
}
}
/**
* Deploys metadata
*
* @param {MetadataTypeMap} metadataMap metadata mapped by their keyField
* @param {string} deployDir directory where deploy metadata are saved
* @param {string} retrieveDir directory where metadata after deploy should be saved
* @returns {Promise.<MetadataTypeMap>} Promise of keyField => metadata map
*/
static async deploy(metadataMap, deployDir, retrieveDir) {
Util.logBeta(this.definition.type);
let needTransactionalEmail = false;
for (const key in metadataMap) {
if (metadataMap[key].definitionType == 'Transactional') {
needTransactionalEmail = true;
break;
}
}
if (needTransactionalEmail && !cache.getCache()?.transactionalEmail) {
// ! interaction and transactionalEmail both link to each other. caching transactionalEmail here "manually", assuming that it's quicker than the other way round
Util.logger.info(' - Caching dependent Metadata: transactionalEmail');
TransactionalEmail.buObject = this.buObject;
TransactionalEmail.client = this.client;
TransactionalEmail.properties = this.properties;
const result = await TransactionalEmail.retrieveForCache();
cache.setMetadata('transactionalEmail', result.metadata);
}
return super.deploy(metadataMap, deployDir, retrieveDir);
}
/**
* Updates a single item
*
* @param {MetadataTypeItem} metadata a single item
* @returns {Promise} Promise
*/
static update(metadata) {
return super.updateREST(
metadata,
'/interaction/v1/interactions/key:' + metadata.key,
'put'
);
}
/**
* Creates a single item
*
* @param {MetadataTypeItem} metadata a single item
* @returns {Promise} Promise
*/
static create(metadata) {
return super.createREST(metadata, '/interaction/v1/interactions/');
}
/**
* Helper for writing Metadata to disk, used for Retrieve and deploy
*
* @param {MetadataTypeMap} results metadata results from deploy
* @param {string} retrieveDir directory where metadata should be stored after deploy/retrieve
* @param {string} [overrideType] for use when there is a subtype (such as folder-queries)
* @param {TemplateMap} [templateVariables] variables to be replaced in the metadata
* @returns {Promise.<MetadataTypeMap>} Promise of saved metadata
*/
static async saveResults(results, retrieveDir, overrideType, templateVariables) {
if (Object.keys(results).length) {
// only execute the following if records were found
await this._postRetrieveTasksBulk(results);
}
return super.saveResults(results, retrieveDir, overrideType, templateVariables);
}
/**
* helper for Journey's {@link Journey.saveResults}. Gets executed after retreive of metadata type and
*
* @param {MetadataTypeMap} metadataMap key=customer key, value=metadata
*/
static async _postRetrieveTasksBulk(metadataMap) {
let needTransactionalEmail = false;
for (const key in metadataMap) {
if (metadataMap[key].definitionType == 'Transactional') {
needTransactionalEmail = true;
break;
}
}
if (needTransactionalEmail && !cache.getCache()?.transactionalEmail) {
// ! interaction and transactionalEmail both link to each other. caching transactionalEmail here "manually", assuming that it's quicker than the other way round
Util.logger.info(' - Caching dependent Metadata: transactionalEmail');
TransactionalEmail.buObject = this.buObject;
TransactionalEmail.client = this.client;
TransactionalEmail.properties = this.properties;
const result = await TransactionalEmail.retrieveForCache();
cache.setMetadata('transactionalEmail', result.metadata);
}
}
/**
* manages post retrieve steps
*
* @param {MetadataTypeItem} metadata a single item
* @returns {Promise.<MetadataTypeItem>} Array with one metadata object
*/
static async postRetrieveTasks(metadata) {
// folder
if (metadata.r__folder_Path && Util.OPTIONS.publish) {
// if we re-retrieve this as part of deploy with --publish then the cached version will already have been processed
return metadata;
}
super.setFolderPath(metadata);
switch (metadata.definitionType) {
case 'Quicksend': // Single Send Journey
case 'Multistep': {
// Single Send Journey
// ~~~ TRIGGERS ~~~~
// event && triggers[].type === 'ContactAudience'
// Multi-Step Journey
// ~~~ TRIGGERS ~~~~
// event / definitionType==='Multistep' && channel==='' && triggers[].type === 'EmailAudience'|'APIEvent'
if (metadata.triggers?.length > 0) {
const search = ['arguments', 'metaData'];
for (const area of search) {
const config = metadata.triggers[0][area];
if (config?.eventDefinitionId) {
// trigger found; there can only be one entry in this array
try {
const edKey = cache.searchForField(
'event',
config.eventDefinitionId,
'id',
'eventDefinitionKey'
);
if (config.eventDefinitionKey !== edKey) {
Util.logger.debug(
`eventDefinitionKey not matching eventDefinitionId. Overwriting '${config.eventDefinitionKey}' with the correct key '${edKey}'.`
);
}
config.r__event_key = edKey;
delete config.eventDefinitionKey;
delete config.eventDefinitionId;
} catch (ex) {
const msg = ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
metadata[this.definition.keyField]
}) ${metadata.status}: ${ex.message}.`;
Util.logger.warn(
metadata.status === 'Published' ? msg : Util.getGrayMsg(msg)
);
}
}
if (config?.dataExtensionId) {
try {
config.r__dataExtension_key = cache.searchForField(
'dataExtension',
config.dataExtensionId,
'ObjectID',
'CustomerKey'
);
delete config.dataExtensionId;
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
metadata[this.definition.keyField]
}): ${ex.message}.`
);
}
}
}
try {
await Event.postRetrieveTasks_SalesforceEntryEvents(
metadata.triggers[0].type,
metadata.triggers[0].configurationArguments,
metadata.key,
metadata.status === 'Published',
this.definition.type
);
} catch (ex) {
const msg = ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${metadata[this.definition.keyField]}) ${metadata.status}: ${ex.message}`;
Util.logger.warn(
metadata.status === 'Published' ? msg : Util.getGrayMsg(msg)
);
}
}
// ~~~ ACTIVITIES ~~~~
this._postRetrieveTasks_activities(metadata);
// TODO: journey template id? / metaData.templateId
break;
}
case 'Transactional': {
// Transactional Send Journey
// ~~~ TRIGGERS ~~~~
// ! journeys so far only supports transactional EMAIL messages. SMS and Push do not create their own journey.
// ! transactional (email) journeys only have a dummy trigger without real content.
// transactionalEmail / definitionType==='Transactional' && channel==='email' && triggers[].type === 'transactional-api'
// --> nothing to do here
// ~~~ ACTIVITIES ~~~~
// ! transactional (email) journeys only have one activity (type=EMAILV2) which links back to the transactionalEmail ()
switch (metadata.channel) {
case 'email': {
if (metadata.activities?.length > 0) {
const activity = metadata.activities[0];
// trigger found; there can only be one entry in this array
if (activity.configurationArguments?.triggeredSendId) {
try {
const tEmailKey = cache.searchForField(
'transactionalEmail',
activity.configurationArguments?.triggeredSendId,
'definitionId',
'definitionKey'
);
if (
activity.configurationArguments?.triggeredSendKey &&
tEmailKey !=
activity.configurationArguments?.triggeredSendKey
) {
Util.logger.debug(
`triggeredSendKey not matching triggeredSendId. Overwriting '${activity.configurationArguments.triggeredSendKey}' with the correct key '${tEmailKey}'.`
);
}
activity.configurationArguments.r__transactionalEmail_key =
tEmailKey;
delete activity.configurationArguments.triggeredSendKey;
delete activity.configurationArguments.triggeredSendId;
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${
metadata[this.definition.nameField]
} (${metadata[this.definition.keyField]}): ${ex.message}.`
);
}
}
if (
activity.metaData?.highThroughput?.definitionKey &&
activity.configurationArguments?.r__transactionalEmail_key &&
activity.metaData?.highThroughput?.definitionKey !=
activity.configurationArguments.r__transactionalEmail_key
) {
Util.logger.warn(
` - ${this.definition.type} ${
metadata[this.definition.nameField]
} (${metadata[this.definition.keyField]}): activities[0].metaData.highThroughput.definitionKey not matching key in activities[0].configurationArguments.r__transactionalEmail_key.`
);
} else if (
activity.configurationArguments?.r__transactionalEmail_key &&
metadata.status === 'Published'
) {
// as long as status is Draft, we wont have r__transactionalEmail_key set as that record will not have been created
delete activity.metaData.highThroughput.definitionKey;
}
this._postRetrieveTasks_activities(metadata);
if (activity.metaData?.highThroughput?.dataExtensionId) {
try {
activity.metaData.highThroughput.r__dataExtension_key =
cache.searchForField(
'dataExtension',
activity.metaData.highThroughput.dataExtensionId,
'ObjectID',
'CustomerKey'
);
delete activity.metaData.highThroughput.dataExtensionId;
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${
metadata[this.definition.nameField]
} (${metadata[this.definition.keyField]}): ${ex.message}.`
);
}
}
}
break;
}
default: {
// it is expected that we'll see 'sms' and 'push' here in the future
Util.logger.warn(
` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
metadata[this.definition.keyField]
}): channel ${
metadata.channel
} is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
);
}
}
break;
}
default: {
Util.logger.warn(
` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
metadata[this.definition.keyField]
}): definitionType ${
metadata.definitionType
} is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
);
}
}
return metadata;
}
/**
* helper for {@link Journey.postRetrieveTasks}
*
* @private
* @param {MetadataTypeItem} metadata a single item
*/
static _postRetrieveTasks_activities(metadata) {
if (!metadata.activities) {
return;
}
for (const activity of metadata.activities) {
switch (activity.type) {
case 'EMAILV2': {
// triggeredSend + email+asset
const configurationArguments = activity.configurationArguments;
if (configurationArguments) {
try {
// configurationArguments.triggeredSendKey && configurationArguments.triggeredSendId are only set on a running journey; if a journey is new, they do not exist
if (configurationArguments.triggeredSendId) {
// triggeredSendKey is not always set but triggeredSendId is
const tsKey = cache.searchForField(
'triggeredSend',
configurationArguments.triggeredSendId,
'ObjectID',
'CustomerKey'
);
if (configurationArguments.triggeredSendKey != tsKey) {
Util.logger.debug(
`triggeredSendKey not matching triggeredSendId. Overwriting '${configurationArguments.triggeredSendKey}' with the correct key '${tsKey}'.`
);
configurationArguments.triggeredSendKey = tsKey;
}
configurationArguments.r__triggeredSend_key =
configurationArguments.triggeredSendKey;
delete configurationArguments.triggeredSendKey;
delete configurationArguments.triggeredSendId;
} else if (configurationArguments.triggeredSendKey) {
// very rare case but it's been seen that no triggeredSendId was saved
Util.logger.debug(
`triggeredSendKey found on activity but no triggeredSendId present on journey. Checking key directly...`
);
configurationArguments.r__triggeredSend_key = cache.searchForField(
'triggeredSend',
configurationArguments.triggeredSendKey,
'CustomerKey',
'CustomerKey'
);
delete configurationArguments.triggeredSendKey;
}
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
metadata[this.definition.keyField]
}) activity-key=${activity.key}: ${ex.message}`
);
}
}
if (
configurationArguments?.triggeredSend &&
'string' === typeof configurationArguments?.triggeredSend
) {
// sometimes, the API returns this object as a string for unknown reasons. Good job, product team!
configurationArguments.triggeredSend = JSON.parse(
configurationArguments?.triggeredSend
);
}
const triggeredSend = configurationArguments?.triggeredSend;
if (triggeredSend) {
// this section is likely only relevant for QuickSends and not for Multi-Step Journeys
// triggeredSend key
if (configurationArguments.r__transactionalEmail_key) {
const linkedTE = cache.getByKey(
'transactionalEmail',
configurationArguments.r__transactionalEmail_key
);
if (linkedTE) {
if (linkedTE.subscriptions) {
triggeredSend.autoAddSubscribers =
linkedTE.subscriptions.autoAddSubscriber;
triggeredSend.autoUpdateSubscribers =
linkedTE.subscriptions.updateSubscriber;
// List
if (linkedTE.subscriptions?.list) {
triggeredSend.publicationListId = cache.searchForField(
'list',
linkedTE.subscriptions.list,
'CustomerKey',
'ID'
);
} else if (linkedTE.subscriptions.r__list_PathName) {
delete triggeredSend.publicationListId;
triggeredSend.r__list_PathName = {
publicationList:
linkedTE.subscriptions.r__list_PathName,
};
}
// dataExtension
if (linkedTE.subscriptions.dataExtension) {
try {
activity.metaData.highThroughput.r__dataExtension_key =
cache.searchForField(
'dataExtension',
linkedTE.subscriptions.dataExtension,
'CustomerKey',
'CustomerKey'
);
delete activity.metaData.highThroughput.dataExtensionId;
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${
metadata[this.definition.nameField]
} (${metadata[this.definition.keyField]}): ${ex.message}.`
);
}
} else if (linkedTE.subscriptions.r__dataExtension_key) {
activity.metaData.highThroughput.r__dataExtension_key =
linkedTE.subscriptions.r__dataExtension_key;
delete activity.metaData.highThroughput.dataExtensionId;
}
}
if (linkedTE.options) {
triggeredSend.isTrackingClicks =
linkedTE.options.trackLinks || false;
triggeredSend.ccEmail = linkedTE.options.cc || '';
triggeredSend.bccEmail = linkedTE.options.bcc || '';
}
// send classification
if (linkedTE.classification) {
try {
const scKey = cache.searchForField(
'sendClassification',
linkedTE.classification,
'CustomerKey',
'CustomerKey'
);
triggeredSend.r__sendClassification_key = scKey;
delete triggeredSend.sendClassificationId;
} catch (ex) {
Util.logger.warn(
` - transactionalEmail ${linkedTE.definitionKey}: ${ex.message} (sendClassification key ${linkedTE.classification})`
);
}
} else if (linkedTE.r__sendClassification_key) {
triggeredSend.r__sendClassification_key =
linkedTE.r__sendClassification_key;
}
// senderProfile + deliveryProfile from sendClassification
if (triggeredSend.r__sendClassification_key) {
const sc = cache.getByKey(
'sendClassification',
triggeredSend.r__sendClassification_key
);
if (sc.SenderProfile?.ObjectID) {
triggeredSend.r__senderProfile_key = cache.searchForField(
'senderProfile',
sc.SenderProfile.ObjectID,
'ObjectID',
'CustomerKey'
);
delete triggeredSend.senderProfileId;
} else if (sc.r__senderProfile_key) {
triggeredSend.r__senderProfile_key =
sc.r__senderProfile_key;
delete triggeredSend.senderProfileId;
}
if (sc.DeliveryProfile?.ObjectID) {
triggeredSend.r__deliveryProfile_key = cache.searchForField(
'deliveryProfile',
sc.DeliveryProfile.ObjectID,
'ObjectID',
'key'
);
delete triggeredSend.deliveryProfileId;
} else if (sc.r__deliveryProfile_key) {
triggeredSend.r__deliveryProfile_key =
sc.r__deliveryProfile_key;
delete triggeredSend.deliveryProfileId;
}
}
}
} else if (configurationArguments.r__triggeredSend_key) {
// if we have a key set outside of this detailed triggeredSend config then lets overwrite what we've got here with what we cached from the related TS as it will be more current; but we cannot retrieve all info unfortunately
triggeredSend.r__triggeredSend_key =
configurationArguments.r__triggeredSend_key;
delete triggeredSend.id;
delete triggeredSend.key;
const linkedTS = cache.getByKey(
'triggeredSend',
configurationArguments.r__triggeredSend_key
);
if (linkedTS) {
triggeredSend.emailId = linkedTS.Email?.ID;
triggeredSend.dynamicEmailSubject = linkedTS.DynamicEmailSubject;
triggeredSend.emailSubject = linkedTS.EmailSubject;
// only the bccEmail field can be retrieved for triggeredSends, not the ccEmail field; for some reason BccEmail can be retrieved but does not return a value even if stored correctly in the journey.
// triggeredSend.bccEmail = linkedTS.BccEmail;
triggeredSend.isMultipart = linkedTS.IsMultipart;
triggeredSend.autoAddSubscribers = linkedTS.AutoAddSubscribers;
triggeredSend.autoUpdateSubscribers =
linkedTS.AutoUpdateSubscribers;
triggeredSend.isTrackingClicks = !linkedTS.SuppressTracking;
triggeredSend.suppressTracking = linkedTS.SuppressTracking;
triggeredSend.triggeredSendStatus = linkedTS.TriggeredSendStatus;
// from name & email are set in the senderProfile, not in the triggeredSend
// triggeredSend.fromName = linkedTS.FromName;
// triggeredSend.fromAddress = linkedTS.FromAddress;
// List
if (linkedTS.List?.ID) {
triggeredSend.publicationListId = linkedTS.List.ID;
} else if (linkedTS.r__list_PathName) {
delete triggeredSend.publicationListId;
triggeredSend.r__list_PathName = {
publicationList: linkedTS.r__list_PathName,
};
}
if (linkedTS.SenderProfile?.CustomerKey) {
try {
const spKey = cache.searchForField(
'senderProfile',
linkedTS.SenderProfile.ObjectID,
'ObjectID',
'CustomerKey'
);
triggeredSend.r__senderProfile_key = spKey;
delete triggeredSend.senderProfileId;
} catch (ex) {
Util.logger.warn(
` - triggeredSend ${linkedTS.CustomerKey}: ${ex.message} (senderProfile key ${linkedTS.SenderProfile.CustomerKey})`
);
}
} else if (linkedTS.r__senderProfile_key) {
triggeredSend.r__senderProfile_key =
linkedTS.r__senderProfile_key;
}
// send classification
if (linkedTS.SendClassification?.CustomerKey) {
try {
const scKey = cache.searchForField(
'sendClassification',
linkedTS.SendClassification.ObjectID,
'ObjectID',
'CustomerKey'
);
triggeredSend.r__sendClassification_key = scKey;
delete triggeredSend.sendClassificationId;
} catch (ex) {
Util.logger.warn(
` - triggeredSend ${linkedTS.CustomerKey}: ${ex.message} (sendClassification key ${linkedTS.SendClassification.CustomerKey})`
);
}
} else if (linkedTS.r__sendClassification_key) {
triggeredSend.r__sendClassification_key =
linkedTS.r__sendClassification_key;
}
if (linkedTS.c__priority) {
delete triggeredSend.priority;
triggeredSend.c__priority = linkedTS.c__priority;
}
if (linkedTS.Email?.ID) {
triggeredSend.emailId = linkedTS.Email.ID;
} else if (linkedTS.r__asset_key) {
delete triggeredSend.emailId;
triggeredSend.r__asset_name_readOnly =
linkedTS.r__asset_name_readOnly;
triggeredSend.r__asset_key = linkedTS.r__asset_key;
}
}
} else if (triggeredSend.id) {
// triggeredSendKey is not always set but id is
const tsKey = cache.searchForField(
'triggeredSend',