Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#706: refresh triggeredSends on email update (deploy) #721

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 62 additions & 23 deletions docs/dist/documentation.md

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions lib/Deployer.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,10 @@ class Deployer {
* @param {TYPE.SupportedMetadataTypes[]} [typeArr] limit deployment to given metadata type (can include subtype)
* @param {string[]} [keyArr] limit deployment to given metadata keys
* @param {boolean} [fromRetrieve] if true, no folders will be updated/created
* @param {boolean} [isRefresh] optional flag to indicate that triggeredSend should be refreshed after deployment of assets
* @returns {Promise.<TYPE.MultiMetadataTypeMap>} Promise of all deployed metadata
*/
async _deploy(typeArr, keyArr, fromRetrieve) {
async _deploy(typeArr, keyArr, fromRetrieve, isRefresh) {
if (await File.pathExists(this.deployDir)) {
/** @type {TYPE.MultiMetadataTypeMap} */
this.metadata = Deployer.readBUMetadata(this.deployDir, typeArr);
Expand Down Expand Up @@ -275,7 +276,8 @@ class Deployer {
const result = await MetadataTypeInfo[type].deploy(
this.metadata[type],
this.deployDir,
this.retrieveDir
this.retrieveDir,
isRefresh
);
multiMetadataTypeMap[type] = result;
cache.mergeMetadata(type, result);
Expand Down
7 changes: 6 additions & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ yargs
},
})
.command({
command: 'deploy [BU] [TYPE] [KEY] [--fromRetrieve]',
command: 'deploy [BU] [TYPE] [KEY] [--fromRetrieve] [--refresh]',
aliases: ['d'],
desc: 'deploys local metadata to a business unit',
builder: (yargs) => {
Expand All @@ -61,6 +61,11 @@ yargs
.option('fromRetrieve', {
type: 'boolean',
describe: 'optionally deploy from retrieve folder',
})
.option('refresh', {
type: 'boolean',
describe:
'optional for asset-message: runs refresh command for related triggeredSends after deploy',
});
},
handler: (argv) => {
Expand Down
72 changes: 72 additions & 0 deletions lib/metadataTypes/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,78 @@ class Asset extends MetadataType {
return metadata;
}

/**
* Gets executed after deployment of metadata type
*
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
* @param {TYPE.MetadataTypeMap} _ originalMetadata to be updated (contains additioanl fields)
* @param {{created: number, updated: number}} createdUpdated counter representing successful creates/updates
* @param {boolean} [isRefresh] optional flag to indicate that triggeredSend should be refreshed after deployment of assets
* @returns {Promise.<void>} -
*/
static async postDeployTasks(metadata, _, createdUpdated, isRefresh) {
if (isRefresh) {
if (createdUpdated.updated) {
// only run this if assets were updated. for created assets we do not expect
this._refreshTriggeredSendDefinition(metadata);
} else {
Util.logger.warn(
'You set the --refresh flag but no updated assets found. Skipping refresh of triggeredSendDefinitions.'
);
}
}
}

/**
* helper for {@link postDeployTasks}. triggers a refresh of active triggerredSendDefinitions associated with the updated asset-message items. Gets executed if isRefresh is true.
*
* @private
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
* @returns {Promise.<void>} -
*/
static async _refreshTriggeredSendDefinition(metadata) {
// get legacyData.legacyId from assets to compare to TSD's metadata.Email.ID to
const legacyIdArr = Object.keys(metadata)
.map((key) => metadata[key]?.legacyData?.legacyId)
.filter(Boolean);

if (!legacyIdArr.length) {
Util.logger.warn(
'No legacyId found in updated emails. Skipping refresh of triggeredSendDefinitions.'
);
return;
}
// prep triggeredSendDefinition class
const TriggeredSendDefinition = require('./TriggeredSendDefinition');
TriggeredSendDefinition.properties = this.properties;
TriggeredSendDefinition.buObject = this.buObject;
TriggeredSendDefinition.client = this.client;
try {
// find refreshable TSDs
const tsdObj = (await TriggeredSendDefinition.findRefreshableItems()).metadata;

const tsdCountInitial = Object.keys(tsdObj).length;
const emailCount = legacyIdArr.length;
// filter TSDs by legacyId
for (const key in tsdObj) {
if (!legacyIdArr.includes(tsdObj[key].Email.ID)) {
delete tsdObj[key];
}
}
const tsdCountFiltered = Object.keys(tsdObj).length;
Util.logger.info(
`Found ${tsdCountFiltered} out of ${tsdCountInitial} total triggeredSendDefinitions for ${emailCount} deployed emails. Commencing validation...`
);

// get keys of TSDs to refresh
const keyArr = await TriggeredSendDefinition.getKeysForValidTSDs(tsdObj);

await TriggeredSendDefinition.refresh(keyArr);
} catch {
Util.logger.warn('Failed to refresh triggeredSendDefinition');
}
}

/**
* prepares an asset definition for deployment
*
Expand Down
7 changes: 3 additions & 4 deletions lib/metadataTypes/Automation.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,11 @@ class Automation extends MetadataType {
* @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
* @param {string} targetBU name/shorthand of target businessUnit for mapping
* @param {string} retrieveDir directory where metadata after deploy should be saved
* @param {boolean} [isRefresh] optional flag - so far not used by automation
* @returns {Promise.<TYPE.AutomationMap>} Promise
*/
static async deploy(metadata, targetBU, retrieveDir) {
const orignalMetadata = JSON.parse(JSON.stringify(metadata));
const upsertResults = await this.upsert(metadata, targetBU);
await this.postDeployTasks(upsertResults, orignalMetadata);
static async deploy(metadata, targetBU, retrieveDir, isRefresh) {
const upsertResults = await this.upsert(metadata, targetBU, isRefresh);
await this.saveResults(upsertResults, retrieveDir, null);
return upsertResults;
}
Expand Down
17 changes: 13 additions & 4 deletions lib/metadataTypes/DataExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class DataExtension extends MetadataType {
`${this.definition.type} upsert: ${createResults.length} of ${deCreatePromises.length} created / ${updateResults.length} of ${deUpdatePromises.length} updated` +
(filteredByPreDeploy > 0 ? ` / ${filteredByPreDeploy} filtered` : '')
);
let upsertResults;
if (successfulResults.length > 0) {
const metadataResults = successfulResults
.map((r) => r.value.Results[0].Object)
Expand All @@ -133,10 +134,15 @@ class DataExtension extends MetadataType {
}
return r;
});
return super.parseResponseBody({ Results: metadataResults });
upsertResults = super.parseResponseBody({ Results: metadataResults });
} else {
return {};
upsertResults = {};
}
await this.postDeployTasks(upsertResults, desToDeploy, {
created: createResults.length,
updated: updateResults.length,
});
return upsertResults;
}

/**
Expand Down Expand Up @@ -247,12 +253,15 @@ class DataExtension extends MetadataType {
*
* @param {TYPE.DataExtensionMap} upsertedMetadata metadata mapped by their keyField
* @param {TYPE.DataExtensionMap} originalMetadata metadata to be updated (contains additioanl fields)
* @param {{created: number, updated: number}} createdUpdated counter representing successful creates/updates
* @returns {void}
*/
static postDeployTasks(upsertedMetadata, originalMetadata) {
static postDeployTasks(upsertedMetadata, originalMetadata, createdUpdated) {
for (const key in upsertedMetadata) {
const item = upsertedMetadata[key];
const cachedVersion = cache.getByKey('dataExtension', item.CustomerKey);
const cachedVersion = createdUpdated.updated
? cache.getByKey('dataExtension', item.CustomerKey)
: null;
if (cachedVersion) {
// UPDATE
// restore retention values that are typically not returned by the update call
Expand Down
5 changes: 3 additions & 2 deletions lib/metadataTypes/EventDefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,12 @@ class EventDefinition extends MetadataType {
* @param {TYPE.MetadataTypeMap} metadata 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
* @param {boolean} [isRefresh] optional flag - so far not used by eventDefinition
* @returns {Promise.<TYPE.MetadataTypeMap>} Promise of keyField => metadata map
*/
static async deploy(metadata, deployDir, retrieveDir) {
static async deploy(metadata, deployDir, retrieveDir, isRefresh) {
Util.logBeta(this.definition.type);
return super.deploy(metadata, deployDir, retrieveDir);
return super.deploy(metadata, deployDir, retrieveDir, isRefresh);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions lib/metadataTypes/Folder.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class Folder extends MetadataType {
* @returns {Promise.<object>} Promise of saved metadata
*/
static async upsert(metadata) {
const orignalMetadata = JSON.parse(JSON.stringify(metadata));
let updateCount = 0;
let updateFailedCount = 0;
let createCount = 0;
Expand Down Expand Up @@ -318,6 +319,10 @@ class Folder extends MetadataType {
` - Folders are recognized for updates based on their CustomerKey or, if that is not given, their folder-path.`
);
}
await this.postDeployTasks(upsertResults, orignalMetadata, {
created: createCount,
updated: updateCount,
});
return upsertResults;
}

Expand Down
5 changes: 3 additions & 2 deletions lib/metadataTypes/Interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,12 @@ class Interaction extends MetadataType {
* @param {TYPE.MetadataTypeMap} metadata 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
* @param {boolean} [isRefresh] optional flag - so far not used by interaction
* @returns {Promise.<TYPE.MetadataTypeMap>} Promise of keyField => metadata map
*/
static async deploy(metadata, deployDir, retrieveDir) {
static async deploy(metadata, deployDir, retrieveDir, isRefresh) {
Util.logBeta(this.definition.type);
return super.deploy(metadata, deployDir, retrieveDir);
return super.deploy(metadata, deployDir, retrieveDir, isRefresh);
}

/**
Expand Down
27 changes: 19 additions & 8 deletions lib/metadataTypes/MetadataType.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ class MetadataType {
* @param {TYPE.MetadataTypeMap} metadata 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
* @param {boolean} [isRefresh] optional flag to indicate that triggeredSend should be refreshed after deployment of assets
* @returns {Promise.<TYPE.MetadataTypeMap>} Promise of keyField => metadata map
*/
static async deploy(metadata, deployDir, retrieveDir) {
const upsertResults = await this.upsert(metadata, deployDir);
await this.postDeployTasks(upsertResults, metadata);
static async deploy(metadata, deployDir, retrieveDir, isRefresh) {
const upsertResults = await this.upsert(metadata, deployDir, isRefresh);
const savedMetadata = await this.saveResults(upsertResults, retrieveDir, null);
if (
this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
Expand All @@ -134,9 +134,11 @@ class MetadataType {
*
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
* @param {TYPE.MetadataTypeMap} originalMetadata metadata to be updated (contains additioanl fields)
* @param {{created: number, updated: number}} createdUpdated counter representing successful creates/updates
* @param {boolean} [isRefresh] optional flag to indicate that triggeredSend should be refreshed after deployment of assets
* @returns {void}
*/
static postDeployTasks(metadata, originalMetadata) {}
static postDeployTasks(metadata, originalMetadata, createdUpdated, isRefresh) {}

/**
* Gets executed after retreive of metadata type
Expand Down Expand Up @@ -446,9 +448,11 @@ class MetadataType {
*
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
* @param {string} deployDir directory where deploy metadata are saved
* @param {boolean} [isRefresh] optional flag to indicate that triggeredSend should be refreshed after deployment of assets
* @returns {Promise.<TYPE.MetadataTypeMap>} keyField => metadata map
*/
static async upsert(metadata, deployDir) {
static async upsert(metadata, deployDir, isRefresh) {
const orignalMetadata = JSON.parse(JSON.stringify(metadata));
const metadataToUpdate = [];
const metadataToCreate = [];
let filteredByPreDeploy = 0;
Expand Down Expand Up @@ -518,7 +522,7 @@ class MetadataType {
`${this.definition.type} upsert: ${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated` +
(filteredByPreDeploy > 0 ? ` / ${filteredByPreDeploy} filtered` : '')
);

let upsertResults;
if (this.definition.bodyIteratorField === 'Results') {
// if Results then parse as SOAP
// put in Retrieve Format for parsing
Expand All @@ -530,15 +534,22 @@ class MetadataType {
.filter((r) => r !== undefined && r !== null && Object.keys(r).length !== 0)
.flatMap((r) => r.Results)
.map((r) => r.Object);
return this.parseResponseBody({ Results: metadataResults });
upsertResults = this.parseResponseBody({ Results: metadataResults });
} else {
// likely comming from one of the many REST APIs
// put in Retrieve Format for parsing
// todo add handling when response does not contain items.
// @ts-ignore
const metadataResults = createResults.concat(updateResults).filter(Boolean);
return this.parseResponseBody(metadataResults);
upsertResults = this.parseResponseBody(metadataResults);
}
await this.postDeployTasks(
upsertResults,
orignalMetadata,
{ created: createResults.length, updated: updateResults.length },
isRefresh
);
return upsertResults;
}

/**
Expand Down
32 changes: 20 additions & 12 deletions lib/metadataTypes/TriggeredSendDefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,13 @@ class TriggeredSendDefinition extends MetadataType {
* TSD-specific refresh method that finds active TSDs and refreshes them
*
* @param {string[]} [keyArr] metadata keys
* @param {boolean} [checkKey=true] whether to check if the key is valid
* @returns {Promise.<void>} -
*/
static async refresh(keyArr) {
static async refresh(keyArr, checkKey = true) {
console.time('Time'); // eslint-disable-line no-console
let checkKey = true;
if (!keyArr) {
keyArr = await this._findRefreshableItems();
keyArr = await this.getKeysForValidTSDs((await this.findRefreshableItems()).metadata);
checkKey = false;
}
// then executes pause, publish, start on them.
Expand All @@ -269,11 +269,25 @@ class TriggeredSendDefinition extends MetadataType {
}

/**
* helper for {@link refresh} that finds active TSDs on the server and filters it by the same rules that {@link retrieve} is using to avoid refreshing TSDs with broken dependencies
* helper for {@link refresh} that extracts the keys from the TSD item map and eli
*
* @param {TYPE.MetadataTypeMapObj} metadata TSD item map
* @returns {Promise.<string[]>} keyArr
*/
static async _findRefreshableItems() {
static async getKeysForValidTSDs(metadata) {
const keyArr = Object.keys(metadata).filter((key) => {
const test = this.postRetrieveTasks(metadata[key]);
return test?.CustomerKey || false;
});
Util.logger.info(`Found ${keyArr.length} refreshable items.`);
return keyArr;
}
/**
* helper for {@link refresh} that finds active TSDs on the server and filters it by the same rules that {@link retrieve} is using to avoid refreshing TSDs with broken dependencies
*
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of TSD item map
*/
static async findRefreshableItems() {
Util.logger.info('Finding refreshable items...');
// cache dependencies to test for broken links
// skip deprecated classic emails here, assuming they cannot be updated and hence are not relevant for {@link refresh}
Expand Down Expand Up @@ -311,13 +325,7 @@ class TriggeredSendDefinition extends MetadataType {
rightOperand: ['dummy', 'Active'], // using equals does not work for this field for an unknown reason and IN requires at least 2 values, hence the 'dummy' entry
},
};
const metadata = (await super.retrieveSOAP(null, requestParams)).metadata;
const keyArr = Object.keys(metadata).filter((key) => {
const test = this.postRetrieveTasks(metadata[key]);
return test?.CustomerKey || false;
});
Util.logger.info(`Found ${keyArr.length} refreshable items.`);
return keyArr;
return super.retrieveSOAP(null, requestParams);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions types/mcdev.d.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const SDK = require('sfmc-sdk');
* @typedef {Object.<string, MetadataTypeItem>} MetadataTypeMap key=customer key
* @typedef {Object.<string, MetadataTypeMap>} MultiMetadataTypeMap key=Supported MetadataType
* @typedef {Object.<string, MetadataTypeItem[]>} MultiMetadataTypeList key=Supported MetadataType
* @typedef {{metadata:MetadataTypeMap,type:SupportedMetadataTypes}} MetadataTypeMapObj
* @typedef {{metadata:MetadataTypeItem,type:SupportedMetadataTypes}} MetadataTypeItemObj
* @typedef {{metadata: MetadataTypeMap, type: SupportedMetadataTypes}} MetadataTypeMapObj
* @typedef {{metadata: MetadataTypeItem, type: SupportedMetadataTypes}} MetadataTypeItemObj
* @typedef {Object.<number, MultiMetadataTypeMap>} Cache key=MID
* @typedef {{before: TYPE.MetadataTypeItem, after: TYPE.MetadataTypeItem}} MetadataTypeItemDiff used during update
*/
Expand Down