mcdev 7.6.3 → 7.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/.github/ISSUE_TEMPLATE/task.md +1 -1
  3. package/.github/workflows/coverage-base-update.yml +2 -2
  4. package/.github/workflows/coverage-develop-branch.yml +3 -1
  5. package/.github/workflows/coverage-main-branch.yml +3 -1
  6. package/.github/workflows/coverage.yml +5 -3
  7. package/.mcdev-validations.js +0 -0
  8. package/@types/lib/Builder.d.ts +14 -0
  9. package/@types/lib/Builder.d.ts.map +1 -1
  10. package/@types/lib/MetadataTypeDefinitions.d.ts +2 -0
  11. package/@types/lib/MetadataTypeDefinitions.d.ts.map +1 -1
  12. package/@types/lib/MetadataTypeInfo.d.ts +2 -0
  13. package/@types/lib/MetadataTypeInfo.d.ts.map +1 -1
  14. package/@types/lib/index.d.ts +17 -8
  15. package/@types/lib/index.d.ts.map +1 -1
  16. package/@types/lib/metadataTypes/Asset.d.ts +11 -3
  17. package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
  18. package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
  19. package/@types/lib/metadataTypes/DomainVerification.d.ts +180 -0
  20. package/@types/lib/metadataTypes/DomainVerification.d.ts.map +1 -0
  21. package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
  22. package/@types/lib/metadataTypes/Journey.d.ts +7 -4
  23. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  24. package/@types/lib/metadataTypes/MetadataType.d.ts +15 -7
  25. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  26. package/@types/lib/metadataTypes/MobileKeyword.d.ts +2 -10
  27. package/@types/lib/metadataTypes/MobileKeyword.d.ts.map +1 -1
  28. package/@types/lib/metadataTypes/MobileMessage.d.ts +2 -10
  29. package/@types/lib/metadataTypes/MobileMessage.d.ts.map +1 -1
  30. package/@types/lib/metadataTypes/SendClassification.d.ts.map +1 -1
  31. package/@types/lib/metadataTypes/SenderProfile.d.ts +7 -0
  32. package/@types/lib/metadataTypes/SenderProfile.d.ts.map +1 -1
  33. package/@types/lib/metadataTypes/TransactionalEmail.d.ts +2 -2
  34. package/@types/lib/metadataTypes/TransactionalEmail.d.ts.map +1 -1
  35. package/@types/lib/metadataTypes/TriggeredSend.d.ts +8 -0
  36. package/@types/lib/metadataTypes/TriggeredSend.d.ts.map +1 -1
  37. package/@types/lib/metadataTypes/Verification.d.ts +0 -9
  38. package/@types/lib/metadataTypes/Verification.d.ts.map +1 -1
  39. package/@types/lib/metadataTypes/definitions/DomainVerification.definition.d.ts +100 -0
  40. package/@types/lib/metadataTypes/definitions/DomainVerification.definition.d.ts.map +1 -0
  41. package/@types/lib/metadataTypes/definitions/Journey.definition.d.ts +1 -1
  42. package/@types/lib/util/devops.d.ts.map +1 -1
  43. package/@types/lib/util/replaceContentBlockReference.d.ts +2 -1
  44. package/@types/lib/util/replaceContentBlockReference.d.ts.map +1 -1
  45. package/@types/lib/util/util.d.ts +42 -1
  46. package/@types/lib/util/util.d.ts.map +1 -1
  47. package/@types/lib/util/validations.d.ts.map +1 -1
  48. package/@types/types/mcdev.d.d.ts +34 -0
  49. package/@types/types/mcdev.d.d.ts.map +1 -1
  50. package/boilerplate/config.json +11 -0
  51. package/boilerplate/files/eslint.config.js +98 -3
  52. package/boilerplate/forcedUpdates.json +4 -0
  53. package/boilerplate/gitignore-template +1 -1
  54. package/boilerplate/npm-dependencies.json +1 -0
  55. package/eslint.config.js +4 -3
  56. package/lib/Builder.js +114 -54
  57. package/lib/Deployer.js +2 -2
  58. package/lib/MetadataTypeDefinitions.js +2 -0
  59. package/lib/MetadataTypeInfo.js +2 -0
  60. package/lib/cli.js +127 -3
  61. package/lib/index.js +217 -164
  62. package/lib/metadataTypes/Asset.js +76 -22
  63. package/lib/metadataTypes/DataExtension.js +10 -2
  64. package/lib/metadataTypes/DomainVerification.js +246 -0
  65. package/lib/metadataTypes/Event.js +21 -9
  66. package/lib/metadataTypes/Journey.js +339 -223
  67. package/lib/metadataTypes/MetadataType.js +153 -106
  68. package/lib/metadataTypes/MobileKeyword.js +5 -2
  69. package/lib/metadataTypes/MobileMessage.js +5 -2
  70. package/lib/metadataTypes/SendClassification.js +5 -0
  71. package/lib/metadataTypes/SenderProfile.js +102 -3
  72. package/lib/metadataTypes/TransactionalEmail.js +3 -1
  73. package/lib/metadataTypes/Verification.js +3 -1
  74. package/lib/metadataTypes/definitions/DomainVerification.definition.js +71 -0
  75. package/lib/metadataTypes/definitions/Journey.definition.js +7 -1
  76. package/lib/metadataTypes/definitions/SendClassification.definition.js +2 -2
  77. package/lib/metadataTypes/definitions/SenderProfile.definition.js +1 -1
  78. package/lib/util/config.js +6 -0
  79. package/lib/util/devops.js +130 -154
  80. package/lib/util/file.js +3 -3
  81. package/lib/util/replaceContentBlockReference.js +10 -3
  82. package/lib/util/util.js +96 -14
  83. package/lib/util/validations.js +34 -14
  84. package/package.json +10 -10
  85. package/test/general.test.js +339 -96
  86. package/test/mockRoot/.mcdev-validations.js +66 -0
  87. package/test/mockRoot/.mcdevrc.json +30 -2
  88. package/test/mockRoot/deploy/testInstance/testBU/asset/block/testNew_asset_htmlblock.asset-block-meta.html +1 -0
  89. package/test/mockRoot/deploy/testInstance/testBU/asset/block/testNew_asset_htmlblock.asset-block-meta.json +39 -0
  90. package/test/mockRoot/deploy/testInstance/testBU/asset/block/testNew_asset_withCBBK_notexisting.asset-block-meta.html +4 -0
  91. package/test/mockRoot/deploy/testInstance/testBU/asset/block/testNew_asset_withCBBK_notexisting.asset-block-meta.json +39 -0
  92. package/test/mockRoot/deploy/testInstance/testBU/asset/block/testNew_asset_withCBBK_preexisting.asset-block-meta.html +4 -0
  93. package/test/mockRoot/deploy/testInstance/testBU/asset/block/testNew_asset_withCBBK_preexisting.asset-block-meta.json +39 -0
  94. package/test/mockRoot/deploy/testInstance/testBU/asset/message/testNew_assetMessage/testNew_assetMessage.asset-message-meta.json +435 -0
  95. package/test/mockRoot/deploy/testInstance/testBU/asset/message/testNew_assetMessage/views.html.content.asset-message-meta.html +150 -0
  96. package/test/mockRoot/deploy/testInstance/testBU/asset/message/testNew_asset_templatebasedemail/testNew_asset_templatebasedemail.asset-message-meta.json +305 -0
  97. package/test/mockRoot/deploy/testInstance/testBU/asset/message/testNew_asset_templatebasedemail/views.html.content.asset-message-meta.html +150 -0
  98. package/test/mockRoot/deploy/testInstance/testBU/asset/template/testNew_asset_template/content.asset-template-meta.html +150 -0
  99. package/test/mockRoot/deploy/testInstance/testBU/asset/template/testNew_asset_template/testNew_asset_template.asset-template-meta.json +116 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/domainVerification/joern.berkefeld.New@accenture.com.domainVerification-meta.json +6 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/domainVerification/joern.berkefeld@accenture.com.domainVerification-meta.json +6 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/domainVerification/mcdev.accenture.com.domainVerification-meta.json +6 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/journey/testNew_temail_notPublished.journey-meta.json +213 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/senderProfile/testExisting_senderProfile.senderProfile-meta.json +1 -0
  105. package/test/mockRoot/deploy/testInstance/testBU/senderProfile/testNew_senderProfile.senderProfile-meta.json +1 -0
  106. package/test/resourceFactory.js +7 -24
  107. package/test/resources/9999999/asset/v1/content/assets/post-response-key=testNew_assetMessage.json +441 -0
  108. package/test/resources/9999999/asset/v1/content/assets/post-response-key=testNew_asset_htmlblock.json +59 -0
  109. package/test/resources/9999999/asset/v1/content/assets/post-response-key=testNew_asset_template.json +147 -0
  110. package/test/resources/9999999/asset/v1/content/assets/post-response-key=testNew_asset_templatebasedemail.json +322 -0
  111. package/test/resources/9999999/asset/v1/content/assets/post-response-key=testNew_asset_withCBBK_notexisting.json +59 -0
  112. package/test/resources/9999999/asset/v1/content/assets/post-response-key=testNew_asset_withCBBK_preexisting.json +59 -0
  113. package/test/resources/9999999/asset-deploy2/block/testBlacklist_asset_htmlblock.asset-block-meta.html +1 -0
  114. package/test/resources/9999999/asset-deploy2/block/testBlacklist_asset_htmlblock.asset-block-meta.json +39 -0
  115. package/test/resources/9999999/automation/clone-expected.json +61 -0
  116. package/test/resources/9999999/dataExtension/retrieve-CustomerKey=testExisting_dataExtension-response.xml +52 -0
  117. package/test/resources/9999999/dataExtension-deploy/testBlacklist_dataExtension.dataExtension-meta.json +20 -0
  118. package/test/resources/9999999/dataFolder/retrieve-ContentTypeINasset,asset-shared,dataextension,hidden,salesforcedataextension,shared_data,shared_dataextension,shared_salesforcedataextension,synchronizeddataextension-response.xml +137 -0
  119. package/test/resources/9999999/domainVerification/create-expected.json +3 -0
  120. package/test/resources/9999999/domainVerification/get-sap-expected.json +6 -0
  121. package/test/resources/9999999/domainVerification/update-expected.json +6 -0
  122. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_temail/put-response-paused.json +219 -0
  123. package/test/resources/9999999/interaction/v1/interactions/key_testNew_temail_notPublished/get-response.json +218 -0
  124. package/test/resources/9999999/interaction/v1/interactions/post-response.json +216 -0
  125. package/test/resources/9999999/journey/create-transactionaEmail-publish-expected.json +217 -0
  126. package/test/resources/9999999/messaging/v1/domainverification/delete/post-response.txt +1 -0
  127. package/test/resources/9999999/messaging/v1/domainverification/get-response.json +43 -0
  128. package/test/resources/9999999/messaging/v1/domainverification/post-response.txt +1 -0
  129. package/test/resources/9999999/messaging/v1/domainverification/update/post-response.txt +1 -0
  130. package/test/resources/9999999/messaging/v1/email/definitions/get-response.json +7 -0
  131. package/test/resources/9999999/messaging/v1/email/definitions/testNew_temail_notPublished/get-response.json +26 -0
  132. package/test/resources/9999999/query/clone-expected.json +8 -0
  133. package/test/resources/9999999/query/clone-expected.sql +7 -0
  134. package/test/resources/9999999/senderProfile/create-response.xml +1 -1
  135. package/test/resources/9999999/senderProfile/post-expected.json +1 -1
  136. package/test/resources/9999999/transactionalEmail/create-publish-expected.json +20 -0
  137. package/test/type.asset.test.js +216 -9
  138. package/test/type.automation.test.js +1 -1
  139. package/test/type.domainVerification.test.js +169 -0
  140. package/test/type.journey.test.js +107 -21
  141. package/test/type.script.test.js +1 -1
  142. package/test/type.sendClassification.test.js +3 -3
  143. package/test/type.senderProfile.test.js +26 -6
  144. package/test/type.transactionalEmail.test.js +5 -5
  145. package/test/type.triggeredSend.test.js +1 -1
  146. package/test/utils.js +8 -0
  147. package/types/mcdev.d.js +12 -0
@@ -47,10 +47,6 @@ class Journey extends MetadataType {
47
47
  */
48
48
  static async retrieve(retrieveDir, _, __, key) {
49
49
  const extrasDefault = 'activities';
50
- if (retrieveDir) {
51
- // only print this during retrieve, not during retrieveForCache
52
- Util.logBeta(this.definition.type);
53
- }
54
50
 
55
51
  let singleKey = '';
56
52
  let mode = 'all';
@@ -294,31 +290,48 @@ class Journey extends MetadataType {
294
290
  );
295
291
  // break;
296
292
  }
297
- default: {
298
- // Quicksend, Transactional dont have versions
299
- const response = await super.deleteByKeyREST(
293
+ case 'Quicksend': {
294
+ // Quicksend doesnt have versions
295
+ const isDeleted = await super.deleteByKeyREST(
296
+ '/interaction/v1/interactions/' + id,
297
+ key,
298
+ false
299
+ );
300
+ return isDeleted;
301
+ }
302
+ case 'Transactional': {
303
+ // Transactional dont have versions
304
+ const isDeleted = await super.deleteByKeyREST(
300
305
  '/interaction/v1/interactions/' + id,
301
306
  key,
302
307
  false
303
308
  );
304
- if (response && cachedJourney.definitionType === 'Transactional') {
309
+ const transactionalEmailKey =
310
+ cachedJourney.activities[0]?.configurationArguments?.triggeredSendKey;
311
+ if (isDeleted) {
305
312
  const msg = [];
306
313
  if (cachedJourney.activities[0]?.configurationArguments?.triggeredSendKey) {
307
314
  msg.push(
308
315
  `transactionalEmail "${cachedJourney.activities[0].configurationArguments.triggeredSendKey}"`
309
316
  );
310
317
  }
311
- if (msg.length) {
312
- Util.logger.info(` - Remember to also delete linked ${msg.join(' and ')}`);
313
- }
314
318
  }
315
- return response;
319
+ if (isDeleted && transactionalEmailKey) {
320
+ Util.logger.info(
321
+ ` - deleted ${TransactionalEmail.definition.type}: ${transactionalEmailKey} (SFMC auto-deletes the related transactionalEmail of ${this.definition.type} ${key})`
322
+ );
323
+ TransactionalEmail.buObject = this.buObject;
324
+ TransactionalEmail.properties = this.properties;
325
+ TransactionalEmail.client = this.client;
326
+ TransactionalEmail.postDeleteTasks(transactionalEmailKey);
327
+ }
328
+ return isDeleted;
316
329
  }
317
330
  }
318
331
  }
319
332
 
320
333
  /**
321
- * Deploys metadata - merely kept here to be able to print {@link Util.logBeta} once per deploy
334
+ * Deploys metadata
322
335
  *
323
336
  * @param {MetadataTypeMap} metadataMap metadata mapped by their keyField
324
337
  * @param {string} deployDir directory where deploy metadata are saved
@@ -420,6 +433,10 @@ class Journey extends MetadataType {
420
433
  */
421
434
  static async postRetrieveTasks(metadata) {
422
435
  // folder
436
+ if (metadata.r__folder_Path && Util.OPTIONS.publish) {
437
+ // if we re-retrieve this as part of deploy with --publish then the cached version will already have been processed
438
+ return metadata;
439
+ }
423
440
  super.setFolderPath(metadata);
424
441
 
425
442
  switch (metadata.definitionType) {
@@ -536,7 +553,7 @@ class Journey extends MetadataType {
536
553
  );
537
554
  }
538
555
  activity.configurationArguments.r__transactionalEmail_key =
539
- activity.configurationArguments.triggeredSendKey;
556
+ tEmailKey;
540
557
  delete activity.configurationArguments.triggeredSendKey;
541
558
  delete activity.configurationArguments.triggeredSendId;
542
559
  } catch (ex) {
@@ -1298,8 +1315,31 @@ class Journey extends MetadataType {
1298
1315
  }
1299
1316
  case 'Transactional': {
1300
1317
  const cachedVersion = cache.getByKey('journey', metadata.key);
1301
- if (cachedVersion.status === 'Published') {
1302
- throw new Error(`Cannot update transactional-send journey in Published status`);
1318
+ if (cachedVersion?.status === 'Published') {
1319
+ if (Util.OPTIONS.publish) {
1320
+ Util.logger.info(
1321
+ ` - ${this.definition.type} ${metadata.key}: --publish option used. Automatically pausing journey to allow for updates.`
1322
+ );
1323
+
1324
+ const pausedKeys = await this.pause([metadata.key], {
1325
+ metadata: cache.getCache().journey,
1326
+ type: this.definition.type,
1327
+ });
1328
+ if (!pausedKeys?.length || pausedKeys[0] !== metadata.key) {
1329
+ Util.logger.error(
1330
+ ` - failed to pause ${this.definition.typeName}: ${metadata.key}`
1331
+ );
1332
+ throw new Error(
1333
+ `Cannot update transactional-send journey in Published status. Automatic pausing failed.`
1334
+ );
1335
+ }
1336
+
1337
+ cachedVersion.status = 'Paused';
1338
+ } else {
1339
+ throw new Error(
1340
+ `Cannot update transactional-send journey in Published status. Run depoy with --publish to auto-pause & republish the journey.`
1341
+ );
1342
+ }
1303
1343
  }
1304
1344
 
1305
1345
  // Transactional Send Journey
@@ -1353,6 +1393,7 @@ class Journey extends MetadataType {
1353
1393
  }
1354
1394
 
1355
1395
  if (activity.metaData?.highThroughput?.r__dataExtension_key) {
1396
+ // if this is set during create, we expect that the DE is already present (part of current deployment or already existing)
1356
1397
  activity.metaData.highThroughput.dataExtensionId = cache.searchForField(
1357
1398
  'dataExtension',
1358
1399
  activity.metaData.highThroughput.r__dataExtension_key,
@@ -1880,7 +1921,7 @@ class Journey extends MetadataType {
1880
1921
  * a function to publish the journey via API
1881
1922
  *
1882
1923
  * @param {string[]} keyArr keys or ids of the metadata
1883
- * @param {MetadataTypeMap} [upsertResults] metadata mapped by their keyField as returned by update/create
1924
+ * @param {MetadataTypeMap} [upsertResults] metadata mapped by their keyField as returned by update/create; needs to be refreshed after publish
1884
1925
  * @returns {Promise.<string[]>} Returns list of updated keys/ids that were published. Success could only be seen with a delay in the UI because the publish-endpoint is async
1885
1926
  */
1886
1927
  static async publish(keyArr, upsertResults) {
@@ -1892,243 +1933,286 @@ class Journey extends MetadataType {
1892
1933
  const metadataMap = upsertResults
1893
1934
  ? { metadata: upsertResults }
1894
1935
  : await this.retrieveForCache();
1895
- const spinnerTransactional = yoctoSpinner({
1896
- text: `Publishing transactional journey…`,
1897
- });
1898
1936
 
1899
- for (let key of keyArr) {
1900
- let objectId;
1901
- let version;
1902
- let journey;
1903
- if (!key) {
1904
- continue;
1905
- }
1906
- if (key.startsWith('%23')) {
1907
- // if the key started with %23 assume an ID was copied from the URL but the user forgot to prefix it with id:
1908
- // correct the format
1909
- key = 'id:' + key.slice(3);
1910
- }
1911
- if (key.startsWith('id:')) {
1912
- // ! allow selecting journeys by ID because that's what users see in the URL
1913
- // remove id
1914
- objectId = key.slice(3);
1915
- if (objectId.startsWith('%23')) {
1916
- // in the journey URL the Id is prefixed with an HTML-encoded "#" which could accidentally be copied by users
1917
- // 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
1918
- objectId = objectId.slice(3);
1919
- // correct the format to ensure we show sth readable in the "Downloaded" log
1920
- // objectId = objectId;
1921
- // update this here to show it in the log
1922
- key = 'id:' + objectId;
1937
+ const publishedJourneyCounter = {
1938
+ multiStep: 0,
1939
+ transactional: 0,
1940
+ };
1941
+
1942
+ // does not matter definitionType we run first, but the logs look bad if we dont sort them
1943
+ const processingOrder = {
1944
+ Transactional: `Publishing transactional journey…`,
1945
+ Multistep: `Publishing multi-step journey…`,
1946
+ };
1947
+ for (const definitionType of Object.keys(processingOrder)) {
1948
+ const spinner = yoctoSpinner({
1949
+ text: processingOrder[definitionType],
1950
+ });
1951
+
1952
+ for (let key of keyArr) {
1953
+ let objectId;
1954
+ let version;
1955
+ let journey;
1956
+ if (!key) {
1957
+ continue;
1923
1958
  }
1924
- if (objectId.includes('/')) {
1925
- version = objectId.split('/')[1];
1926
- // in the journey URL the version is appended after the ID, separated by a forward-slash. Needs to be removed from the ID for caching as we always aim to retrieve the latest version only
1927
- objectId = objectId.split('/')[0];
1959
+ if (key.startsWith('%23')) {
1960
+ // if the key started with %23 assume an ID was copied from the URL but the user forgot to prefix it with id:
1961
+ // correct the format
1962
+ key = 'id:' + key.slice(3);
1963
+ }
1964
+ if (key.startsWith('id:')) {
1965
+ // ! allow selecting journeys by ID because that's what users see in the URL
1966
+ // remove id
1967
+ objectId = key.slice(3);
1968
+ if (objectId.startsWith('%23')) {
1969
+ // in the journey URL the Id is prefixed with an HTML-encoded "#" which could accidentally be copied by users
1970
+ // 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
1971
+ objectId = objectId.slice(3);
1972
+ // correct the format to ensure we show sth readable in the "Downloaded" log
1973
+ // objectId = objectId;
1974
+ // update this here to show it in the log
1975
+ key = 'id:' + objectId;
1976
+ }
1977
+ if (objectId.includes('/')) {
1978
+ version = objectId.split('/')[1];
1979
+ // in the journey URL the version is appended after the ID, separated by a forward-slash. Needs to be removed from the ID for caching as we always aim to retrieve the latest version only
1980
+ objectId = objectId.split('/')[0];
1981
+ } else {
1982
+ // if we didn't find a version we need to cache this from the API after all
1983
+ if (key.includes('/')) {
1984
+ // in the journey URL the version is appended after the ID, separated by a forward-slash. Needs to be removed from the key for caching as we always aim to retrieve the latest version only
1985
+ key = key.split('/')[0];
1986
+ }
1987
+ }
1988
+ journey = Object.values(metadataMap.metadata).find((el) => el.id === objectId);
1989
+ if (!journey) {
1990
+ Util.logger.info(
1991
+ ` ☇ skipping ${this.definition.type} ${key}: not found on server (1)`
1992
+ );
1993
+ continue;
1994
+ }
1928
1995
  } else {
1929
- // if we didn't find a version we need to cache this from the API after all
1930
- if (key.includes('/')) {
1931
- // in the journey URL the version is appended after the ID, separated by a forward-slash. Needs to be removed from the key for caching as we always aim to retrieve the latest version only
1932
- key = key.split('/')[0];
1996
+ // key assumed
1997
+ journey = metadataMap.metadata[key];
1998
+ }
1999
+
2000
+ if (journey && journey.definitionType !== definitionType) {
2001
+ // to allow consistent log output we enforce publishing is sorted by definitionType
2002
+ if (!processingOrder[journey.definitionType]) {
2003
+ throw new Error(
2004
+ `${this.definition.type} type ${journey.definitionType} not supported yet by publish method`
2005
+ );
1933
2006
  }
2007
+ continue;
1934
2008
  }
1935
- journey = Object.values(metadataMap.metadata).find((el) => el.id === objectId);
2009
+
1936
2010
  if (!journey) {
1937
2011
  Util.logger.info(
1938
- ` ☇ skipping ${this.definition.type} ${key}: not found on server (1)`
2012
+ ` ☇ skipping ${this.definition.type} ${key}: not found on server (2)`
1939
2013
  );
1940
2014
  continue;
1941
2015
  }
1942
- } else {
1943
- // key assumed
1944
- journey = metadataMap.metadata[key];
1945
- }
1946
2016
 
1947
- if (!journey) {
1948
- Util.logger.info(
1949
- ` ☇ skipping ${this.definition.type} ${key}: not found on server (2)`
1950
- );
1951
- continue;
1952
- }
1953
- if (!version) {
1954
- version = journey.version;
1955
- }
1956
- if (journey.status === 'Published') {
1957
- // api would return error code 30000 and ask to open a support case when in fact we simply already have a transactionalEmail created based on this status
1958
- if (journey.definitionType === 'Transactional') {
2017
+ if (!version) {
2018
+ version = journey.version;
2019
+ }
2020
+ if (journey.status === 'Published') {
2021
+ // api would return error code 30000 and ask to open a support case when in fact we simply already have a transactionalEmail created based on this status
2022
+ if (journey.definitionType === 'Transactional') {
2023
+ Util.logger.info(
2024
+ ` skipping ${this.definition.type} ${
2025
+ journey[this.definition.nameField]
2026
+ } (${journey[this.definition.keyField]}): already published. Queueing for refresh.`
2027
+ );
2028
+ refreshTransactionalKeys.push(journey.key);
2029
+ } else {
2030
+ Util.logger.warn(
2031
+ ` ☇ skipping ${this.definition.type} ${
2032
+ journey[this.definition.nameField]
2033
+ } (${journey[this.definition.keyField]}): already published.`
2034
+ );
2035
+ }
2036
+ continue;
2037
+ } else if (
2038
+ journey.status === 'Paused' &&
2039
+ journey.definitionType === 'Transactional'
2040
+ ) {
1959
2041
  Util.logger.info(
1960
2042
  ` ☇ skipping ${this.definition.type} ${
1961
2043
  journey[this.definition.nameField]
1962
- } (${journey[this.definition.keyField]}): already published. Queueing for refresh.`
2044
+ } (${journey[this.definition.keyField]}): currently paused. Queueing for refresh.`
1963
2045
  );
1964
2046
  refreshTransactionalKeys.push(journey.key);
1965
- } else {
1966
- Util.logger.warn(
1967
- ` ☇ skipping ${this.definition.type} ${
1968
- journey[this.definition.nameField]
1969
- } (${journey[this.definition.keyField]}): already published.`
1970
- );
2047
+ continue;
1971
2048
  }
1972
- continue;
1973
- }
1974
2049
 
1975
- switch (journey.definitionType) {
1976
- case 'Transactional': {
1977
- resultsTransactional.push(
1978
- (async () => {
1979
- spinnerTransactional.start();
1980
- try {
1981
- const response = await this.client.rest.post(
1982
- `/interaction/v1/interactions/transactional/create`,
1983
- { definitionId: journey.id }
1984
- );
1985
- if (response.errors?.length) {
1986
- throw new Error(JSON.stringify(response));
1987
- } else {
1988
- spinnerTransactional.stop();
1989
- Util.logger.info(
1990
- ` - published ${this.definition.type}: ${
1991
- journey[this.definition.nameField]
1992
- } (${journey[this.definition.keyField]}) by creating the matching transactionalEmail`
1993
- );
1994
- statusUrls.push({ key, statusUrl: response.statusUrl });
1995
- }
1996
- spinnerTransactional.start();
1997
- return journey[this.definition.keyField];
1998
- } catch (ex) {
1999
- spinnerTransactional.stop();
2000
- if (
2001
- ex.response.status === 400 &&
2002
- ex.response?.data?.errors?.length === 1 &&
2003
- ex.response?.data?.errors?.[0]?.errorCode === '121500'
2004
- ) {
2005
- Util.logger.error(
2006
- `Failed to publish ${
2007
- journey[this.definition.nameField]
2008
- } (${journey[this.definition.keyField]}): Make sure the Event Definition Key, Data Extension and E-Mail are saved to the journey`
2009
- );
2010
- } else {
2011
- Util.logger.error(
2012
- `Failed to publish ${
2013
- journey[this.definition.nameField]
2014
- } (${journey[this.definition.keyField]}): ${ex.message}`
2050
+ switch (journey.definitionType) {
2051
+ case 'Transactional': {
2052
+ resultsTransactional.push(
2053
+ (async () => {
2054
+ spinner.start();
2055
+ try {
2056
+ const response = await this.client.rest.post(
2057
+ `/interaction/v1/interactions/transactional/create`,
2058
+ { definitionId: journey.id }
2015
2059
  );
2016
- if (ex.response?.data?.errors?.length) {
2060
+ if (response.errors?.length) {
2061
+ throw new Error(JSON.stringify(response));
2062
+ } else {
2063
+ spinner.stop();
2064
+ Util.logger.info(
2065
+ ` - published ${this.definition.type}: ${
2066
+ journey[this.definition.nameField]
2067
+ } (${journey[this.definition.keyField]}) by creating the matching transactionalEmail`
2068
+ );
2069
+ statusUrls.push({ key, statusUrl: response.statusUrl });
2070
+ }
2071
+ spinner.start();
2072
+ return journey[this.definition.keyField];
2073
+ } catch (ex) {
2074
+ spinner.stop();
2075
+ if (
2076
+ ex.response.status === 400 &&
2077
+ ex.response?.data?.errors?.length === 1 &&
2078
+ ex.response?.data?.errors?.[0]?.errorCode === '121500'
2079
+ ) {
2080
+ Util.logger.error(
2081
+ `Failed to publish ${
2082
+ journey[this.definition.nameField]
2083
+ } (${journey[this.definition.keyField]}): Make sure the Event Definition Key, Data Extension and E-Mail are saved to the journey`
2084
+ );
2085
+ } else {
2017
2086
  Util.logger.error(
2018
- JSON.stringify(ex.response?.data?.errors, null, 2)
2087
+ `Failed to publish ${
2088
+ journey[this.definition.nameField]
2089
+ } (${journey[this.definition.keyField]}): ${ex.message}`
2019
2090
  );
2091
+ if (ex.response?.data?.errors?.length) {
2092
+ Util.logger.error(
2093
+ JSON.stringify(ex.response?.data?.errors, null, 2)
2094
+ );
2095
+ }
2020
2096
  }
2097
+ spinner.start();
2021
2098
  }
2022
- spinnerTransactional.start();
2023
- }
2024
- })()
2025
- );
2026
- break;
2027
- }
2028
- case 'Multistep': {
2029
- // SF Event, Api Event Journeys
2030
- // ! for SF-triggered journeys this cannot be asynchronous or it will cause a race-condition (see #1627 for details); the requests are accepted but then processed sequentually anyways, eliminating potential speed gains.
2031
- // It is unknown if the same would happen for API-event journeys but given that it's the same endpoint, lets not risk it and run this sequentially
2032
- let statusUrl;
2033
- try {
2034
- const response = await this.client.rest.post(
2035
- `/interaction/v1/interactions/publishAsync/${journey.id}?versionNumber=${version}`,
2036
- {}
2037
- ); // payload is empty for this request
2038
- if (response.statusUrl && response.statusId) {
2039
- Util.logger.info(
2040
- ` - ${this.definition.type} queued for publishing: ${journey[this.definition.keyField]}/${version} / ${journey[this.definition.nameField]}`
2041
- );
2042
- statusUrl = response.statusUrl;
2043
- } else {
2044
- throw new Error(response);
2045
- }
2046
- if (Util.OPTIONS.skipStatusCheck) {
2047
- Util.logger.warn(
2048
- ` - Skipping status check for publishing journey ${key} due to --skipStatusCheck flag`
2049
- );
2050
- }
2051
- if (!Util.OPTIONS.skipStatusCheck && statusUrl) {
2052
- const spinner = yoctoSpinner({
2053
- text: `Publishing journey…`,
2054
- }).start();
2055
-
2056
- await Util.sleep(1000);
2057
- executedKeyArr.push(
2058
- await this._checkPublishStatus(
2059
- statusUrl,
2060
- journey[this.definition.keyField],
2061
- journey[this.definition.nameField],
2062
- spinner
2063
- )
2064
- );
2065
- } else {
2066
- // no guarantees if the journey was actually published
2067
- executedKeyArr.push(key);
2068
- }
2069
- } catch (ex) {
2070
- switch (ex.message) {
2071
- case 'Cannot publish interaction in Published status.': {
2099
+ })()
2100
+ );
2101
+ break;
2102
+ }
2103
+ case 'Multistep': {
2104
+ // SF Event, Api Event Journeys
2105
+ // ! for SF-triggered journeys this cannot be asynchronous or it will cause a race-condition (see #1627 for details); the requests are accepted but then processed sequentually anyways, eliminating potential speed gains.
2106
+ // It is unknown if the same would happen for API-event journeys but given that it's the same endpoint, lets not risk it and run this sequentially
2107
+ let statusUrl;
2108
+ try {
2109
+ const response = await this.client.rest.post(
2110
+ `/interaction/v1/interactions/publishAsync/${journey.id}?versionNumber=${version}`,
2111
+ {}
2112
+ ); // payload is empty for this request
2113
+ if (response.statusUrl && response.statusId) {
2072
2114
  Util.logger.info(
2073
- ` - ${this.definition.type} ${key}/${version} is already published.`
2115
+ ` - ${this.definition.type} queued for publishing: ${journey[this.definition.keyField]}/${version} / ${journey[this.definition.nameField]}`
2074
2116
  );
2075
-
2076
- break;
2117
+ statusUrl = response.statusUrl;
2118
+ } else {
2119
+ throw new Error(response);
2077
2120
  }
2078
- case 'Cannot publish interaction in Stopped status.': {
2121
+ if (Util.OPTIONS.skipStatusCheck) {
2079
2122
  Util.logger.warn(
2080
- ` - ${this.definition.type} ${key}/${version} is stopped. Please create a new version and publish that.`
2123
+ ` - Skipping status check for publishing journey ${key} due to --skipStatusCheck flag`
2081
2124
  );
2082
-
2083
- break;
2084
2125
  }
2085
- case 'Cannot publish interaction in Paused status.': {
2086
- Util.logger.warn(
2087
- ` - ${this.definition.type} ${key}/${version} is already published but currently paused. Run 'mcdev resume' instead.`
2126
+ if (!Util.OPTIONS.skipStatusCheck && statusUrl) {
2127
+ spinner.start();
2128
+
2129
+ await Util.sleep(1000);
2130
+ executedKeyArr.push(
2131
+ await this._checkPublishStatus(
2132
+ statusUrl,
2133
+ journey[this.definition.keyField],
2134
+ journey[this.definition.nameField],
2135
+ spinner
2136
+ )
2088
2137
  );
2089
-
2090
- break;
2138
+ publishedJourneyCounter.multiStep++;
2139
+ } else {
2140
+ // no guarantees if the journey was actually published
2141
+ executedKeyArr.push(key);
2142
+ publishedJourneyCounter.multiStep++;
2091
2143
  }
2092
- default: {
2093
- Util.logger.error(
2094
- `Failed to publish ${this.definition.type} ${key}: ${ex.message}`
2095
- );
2144
+ } catch (ex) {
2145
+ switch (ex.message) {
2146
+ case 'Cannot publish interaction in Published status.': {
2147
+ Util.logger.info(
2148
+ ` - ${this.definition.type} ${key}/${version} is already published.`
2149
+ );
2150
+
2151
+ break;
2152
+ }
2153
+ case 'Cannot publish interaction in Stopped status.': {
2154
+ Util.logger.warn(
2155
+ ` - ${this.definition.type} ${key}/${version} is stopped. Please create a new version and publish that.`
2156
+ );
2157
+
2158
+ break;
2159
+ }
2160
+ case 'Cannot publish interaction in Paused status.': {
2161
+ Util.logger.warn(
2162
+ ` - ${this.definition.type} ${key}/${version} is already published but currently paused. Run 'mcdev resume' instead.`
2163
+ );
2164
+
2165
+ break;
2166
+ }
2167
+ default: {
2168
+ Util.logger.error(
2169
+ `Failed to publish ${this.definition.type} ${key}: ${ex.message}`
2170
+ );
2171
+ }
2096
2172
  }
2097
2173
  }
2174
+ break;
2175
+ }
2176
+ default: {
2177
+ throw new Error(
2178
+ `${this.definition.type} type ${journey.definitionType} not supported yet by publish method`
2179
+ );
2180
+ }
2181
+ }
2182
+ } // for loop
2183
+
2184
+ // when all relevant keys have been iterated over per definitionType, ensure we wait for async tasks and update the counter
2185
+ switch (definitionType) {
2186
+ case 'Transactional': {
2187
+ if (resultsTransactional.length) {
2188
+ const transactionalKeyArr = (
2189
+ await Promise.all(resultsTransactional)
2190
+ ).filter(Boolean);
2191
+ spinner.stop();
2192
+
2193
+ // if all publish actions failed, we don't need to re-retrieve anything here
2194
+ if (transactionalKeyArr.length) {
2195
+ executedKeyArr.push(...transactionalKeyArr);
2196
+ publishedJourneyCounter.transactional = transactionalKeyArr.length;
2197
+ // reset transactionalEmail cache to trigger re-caching it.
2198
+ cache.clearCache(this.buObject.mid, 'transactionalEmail');
2199
+ }
2098
2200
  }
2099
2201
  break;
2100
2202
  }
2101
- default: {
2102
- throw new Error(
2103
- `${this.definition.type} type ${journey.definitionType} not supported yet by publish method`
2104
- );
2203
+ case 'Multistep': {
2204
+ // all done; executed synchronously
2205
+ break;
2105
2206
  }
2106
2207
  }
2107
- } // for loop
2108
-
2109
- const publishedJourneyCounter = {
2110
- multiStep: executedKeyArr.filter(Boolean).length,
2111
- transactional: 0,
2112
- };
2113
- // Transactional Send Journeys
2114
- if (resultsTransactional.length) {
2115
- const transactionalKeyArr = (await Promise.all(resultsTransactional)).filter(Boolean);
2116
- spinnerTransactional.stop();
2117
-
2118
- // if all publish actions failed, we don't need to re-retrieve anything here
2119
- if (transactionalKeyArr.length) {
2120
- executedKeyArr.push(...transactionalKeyArr);
2121
- publishedJourneyCounter.transactional = transactionalKeyArr.length;
2122
- // reset transactionalEmail cache to trigger re-caching it.
2123
- cache.clearCache(this.buObject.mid, 'transactionalEmail');
2124
- }
2125
- }
2208
+ } // processing order
2126
2209
 
2127
2210
  // reload published journeys including their events/transactionalEmails
2128
2211
  await this._reRetrieve(
2129
2212
  executedKeyArr,
2130
2213
  publishedJourneyCounter.transactional,
2131
- publishedJourneyCounter.multiStep
2214
+ publishedJourneyCounter.multiStep,
2215
+ upsertResults
2132
2216
  );
2133
2217
 
2134
2218
  Util.logger.info(
@@ -2137,9 +2221,13 @@ class Journey extends MetadataType {
2137
2221
 
2138
2222
  if (refreshTransactionalKeys.length) {
2139
2223
  // in case we tried to publish a transactional journey that was already published we will instead run a refresh for those
2140
- executedKeyArr.push(...(await this.refresh(refreshTransactionalKeys)));
2224
+ executedKeyArr.push(
2225
+ ...(await this.refresh(refreshTransactionalKeys, null, upsertResults))
2226
+ );
2141
2227
  }
2142
- return executedKeyArr.filter(Boolean);
2228
+
2229
+ // good practice to return the published keys in alphabetical order
2230
+ return executedKeyArr.filter(Boolean).sort();
2143
2231
  }
2144
2232
 
2145
2233
  /**
@@ -2147,9 +2235,15 @@ class Journey extends MetadataType {
2147
2235
  * @param {string[]} executedKeyArr list of journey keys
2148
2236
  * @param {number} transactionalCounter how many transactiona-send journeys did we expect to refresh
2149
2237
  * @param {number} multiStepCounter how many multi-step journeys did we expect to refresh
2238
+ * @param {MetadataTypeMap} [upsertResults] metadata mapped by their keyField returned by update/create; needs to be refreshed after publish
2150
2239
  * @returns {Promise.<void>} -
2151
2240
  */
2152
- static async _reRetrieve(executedKeyArr, transactionalCounter, multiStepCounter) {
2241
+ static async _reRetrieve(
2242
+ executedKeyArr,
2243
+ transactionalCounter,
2244
+ multiStepCounter,
2245
+ upsertResults
2246
+ ) {
2153
2247
  if (!executedKeyArr.filter(Boolean).length) {
2154
2248
  return;
2155
2249
  }
@@ -2179,6 +2273,11 @@ class Journey extends MetadataType {
2179
2273
  const updatedEvents = [];
2180
2274
  const updatedTransactionalEmails = [];
2181
2275
  for (const journey of updatedJourneys) {
2276
+ if (upsertResults?.[journey[this.definition.keyField]]) {
2277
+ // update upsertResults with journeys retrieved here to ensure we save the right thing to disk - if it was provided to the method
2278
+ upsertResults[journey[this.definition.keyField]] = journey;
2279
+ }
2280
+
2182
2281
  // multi-step journeys
2183
2282
  updatedEvents.push(journey.triggers?.[0]?.metaData?.r__event_key);
2184
2283
  // transactional-send journeys
@@ -2210,6 +2309,7 @@ class Journey extends MetadataType {
2210
2309
  toBeRetrievedTypes.map((item) => item + 's').join(', ')
2211
2310
  );
2212
2311
  await retriever.retrieve(toBeRetrievedTypes, eventTransEmailCombo);
2312
+ // TODO find r__automation_key in events and retrieve these automations as well
2213
2313
  }
2214
2314
  }
2215
2315
  } catch (ex) {
@@ -2290,14 +2390,15 @@ class Journey extends MetadataType {
2290
2390
  messages[type].push(` ${types[type]}:`);
2291
2391
  for (const msg of response[type]) {
2292
2392
  messages[type].push(
2293
- ` #${counter++}`,
2294
- ` Code: ${msg.errorCode}`,
2295
- ` Details: ${msg.errorDetail.split(' EmailID: ').join('\n EmailID: ').split(' Personalization error: ').join('\n Personalization error: ')}`
2393
+ ` #${counter++}: ${msg.errorDetail.split(' EmailID: ').join('\n EmailID: ').split(' Personalization error: ').join('\n Personalization error: ')}`,
2394
+ Util.getGrayMsg(` Code: ${msg.errorCode}`)
2296
2395
  );
2297
2396
  if (msg.additionalInfo && Object.keys(msg.additionalInfo).length) {
2298
- messages[type].push(` Additional Info:`);
2397
+ messages[type].push(Util.getGrayMsg(` Additional Info:`));
2299
2398
  for (const key in msg.additionalInfo) {
2300
- messages[type].push(` ${key}: ${msg.additionalInfo[key]}`);
2399
+ messages[type].push(
2400
+ Util.getGrayMsg(` ${key}: ${msg.additionalInfo[key]}`)
2401
+ );
2301
2402
  }
2302
2403
  }
2303
2404
  // add spacer line
@@ -2610,7 +2711,16 @@ class Journey extends MetadataType {
2610
2711
  apiLimit(async () => {
2611
2712
  [key, version] = key.split('/');
2612
2713
  if (journeyCache.metadata[key]) {
2613
- if (journeyCache.metadata[key].status !== 'Published') {
2714
+ if (journeyCache.metadata[key].status === 'Paused') {
2715
+ Util.logger.info(
2716
+ ` ${this.definition.type} ${
2717
+ key
2718
+ } (${journeyCache.metadata[key].name}): already paused`
2719
+ );
2720
+ // still add this key because technically this method is supposed to pause a journey and this journey is paused. mission accomplished. Also, we need that for _refreshItem() to function
2721
+ pausedKeyArr.push(key);
2722
+ return;
2723
+ } else if (journeyCache.metadata[key].status !== 'Published') {
2614
2724
  Util.logger.error(
2615
2725
  ` - Pausing ${this.definition.type} ${key} / ${journeyCache.metadata[key].name} failed: Cannot pause a journey in status ${journeyCache.metadata[key].status}`
2616
2726
  );
@@ -2768,9 +2878,11 @@ class Journey extends MetadataType {
2768
2878
  * TSD-specific refresh method that finds active TSDs and refreshes them
2769
2879
  *
2770
2880
  * @param {string[]} keyArr metadata keys
2881
+ * @param {boolean} [_] whether to check if the key is valid
2882
+ * @param {MetadataTypeMap} [upsertResults] metadata mapped by their keyField as returned by update/create; needs to be refreshed after publish
2771
2883
  * @returns {Promise.<string[]>} Returns list of keys that were refreshed
2772
2884
  */
2773
- static async refresh(keyArr) {
2885
+ static async refresh(keyArr, _, upsertResults) {
2774
2886
  console.time('Time'); // eslint-disable-line no-console
2775
2887
  if (!Array.isArray(keyArr) || !keyArr.length) {
2776
2888
  Util.logger.error('No refresh-keys provided');
@@ -2778,7 +2890,9 @@ class Journey extends MetadataType {
2778
2890
  // keyArr = await this.getKeysForValidTSDs((await this.findRefreshableItems()).metadata);
2779
2891
  // checkKey = false;
2780
2892
  }
2781
- const journeyCache = await this.retrieveForCache();
2893
+ const journeyCache = upsertResults
2894
+ ? { metadata: upsertResults, type: this.definition.type }
2895
+ : await this.retrieveForCache();
2782
2896
  // then executes pause, publish, start on them.
2783
2897
  Util.logger.info(`Refreshing ${keyArr.length} ${this.definition.typeName}...`);
2784
2898
  Util.logger.debug(`Refreshing keys: ${keyArr.join(', ')}`);
@@ -2797,7 +2911,9 @@ class Journey extends MetadataType {
2797
2911
  }
2798
2912
  switch (journeyCache.metadata[key].definitionType) {
2799
2913
  case 'Transactional': {
2800
- if (journeyCache.metadata[key]?.status === 'Published') {
2914
+ if (
2915
+ ['Paused', 'Published'].includes(journeyCache.metadata[key]?.status)
2916
+ ) {
2801
2917
  const result = await this._refreshItem(key, journeyCache);
2802
2918
  if (result) {
2803
2919
  refreshedKeyArr.push(key);