UNPKG

mcdev

Version:

Accenture Salesforce Marketing Cloud DevTools

913 lines (873 loc) 171 kB
'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',