mcdev 5.0.2 → 5.2.0

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 (237) hide show
  1. package/.coverage-comment-template.svelte +177 -161
  2. package/.eslintrc.json +4 -4
  3. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +2 -2
  5. package/.github/dependabot.yml +8 -0
  6. package/.github/workflows/coverage-base-update.yml +6 -2
  7. package/.github/workflows/coverage-develop-branch.yml +7 -8
  8. package/.github/workflows/coverage-main-branch.yml +7 -8
  9. package/.github/workflows/coverage.yml +7 -4
  10. package/.husky/post-checkout +4 -2
  11. package/.husky/post-merge +1 -0
  12. package/.vscode/extensions.json +4 -0
  13. package/docs/dist/documentation.md +756 -294
  14. package/lib/Deployer.js +28 -28
  15. package/lib/MetadataTypeDefinitions.js +1 -1
  16. package/lib/MetadataTypeInfo.js +1 -1
  17. package/lib/Retriever.js +1 -1
  18. package/lib/cli.js +184 -6
  19. package/lib/index.js +493 -22
  20. package/lib/metadataTypes/Asset.js +10 -11
  21. package/lib/metadataTypes/AttributeGroup.js +76 -2
  22. package/lib/metadataTypes/AttributeSet.js +260 -0
  23. package/lib/metadataTypes/Automation.js +771 -247
  24. package/lib/metadataTypes/DataExtension.js +7 -7
  25. package/lib/metadataTypes/DataExtensionField.js +1 -1
  26. package/lib/metadataTypes/Event.js +2 -3
  27. package/lib/metadataTypes/Folder.js +1 -1
  28. package/lib/metadataTypes/Journey.js +5 -6
  29. package/lib/metadataTypes/MetadataType.js +187 -60
  30. package/lib/metadataTypes/MobileKeyword.js +8 -8
  31. package/lib/metadataTypes/MobileMessage.js +5 -5
  32. package/lib/metadataTypes/Query.js +47 -5
  33. package/lib/metadataTypes/Script.js +3 -3
  34. package/lib/metadataTypes/TransactionalSMS.js +5 -5
  35. package/lib/metadataTypes/TriggeredSend.js +25 -50
  36. package/lib/metadataTypes/User.js +7 -4
  37. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  38. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +117 -106
  39. package/lib/metadataTypes/definitions/{SetDefinition.definition.js → AttributeSet.definition.js} +54 -27
  40. package/lib/metadataTypes/definitions/Automation.definition.js +74 -21
  41. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -0
  42. package/lib/metadataTypes/definitions/DataExtract.definition.js +1 -0
  43. package/lib/metadataTypes/definitions/EmailSend.definition.js +1 -0
  44. package/lib/metadataTypes/definitions/Event.definition.js +1 -0
  45. package/lib/metadataTypes/definitions/Filter.definition.js +1 -0
  46. package/lib/metadataTypes/definitions/ImportFile.definition.js +37 -6
  47. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +1 -0
  48. package/lib/metadataTypes/definitions/Query.definition.js +1 -0
  49. package/lib/metadataTypes/definitions/Role.definition.js +1 -0
  50. package/lib/metadataTypes/definitions/TriggeredSend.definition.js +2 -0
  51. package/lib/metadataTypes/definitions/User.definition.js +1 -0
  52. package/lib/util/cache.js +9 -4
  53. package/lib/util/cli.js +40 -0
  54. package/lib/util/devops.js +13 -11
  55. package/lib/util/file.js +2 -2
  56. package/lib/util/init.js +84 -0
  57. package/lib/util/util.js +268 -137
  58. package/package.json +11 -11
  59. package/test/general.test.js +26 -0
  60. package/test/mockRoot/.mcdevrc.json +1 -1
  61. package/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json +52 -0
  62. package/test/mockRoot/deploy/testInstance/testBU/automation/testNew_automation.automation-meta.json +45 -0
  63. package/test/mockRoot/deploy/testInstance/testBU/dataExtract/testExisting_dataExtract.dataExtract-meta.json +35 -0
  64. package/test/mockRoot/deploy/testInstance/testBU/dataExtract/testNew_dataExtract.dataExtract-meta.json +35 -0
  65. package/test/mockRoot/deploy/testInstance/testBU/fileTransfer/testExisting_fileTransfer.fileTransfer-meta.json +17 -0
  66. package/test/mockRoot/deploy/testInstance/testBU/fileTransfer/testNew_fileTransfer.fileTransfer-meta.json +17 -0
  67. package/test/mockRoot/deploy/testInstance/testBU/importFile/testExisting_importFile.importFile-meta.json +29 -0
  68. package/test/mockRoot/deploy/testInstance/testBU/importFile/testNew_importFile.importFile-meta.json +29 -0
  69. package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.json → testExisting_query.query-meta.json} +2 -2
  70. package/test/mockRoot/deploy/testInstance/testBU/query/testExisting_query_fixKeys.query-meta.json +11 -0
  71. package/test/mockRoot/deploy/testInstance/testBU/query/testExisting_query_fixKeys.query-meta.sql +6 -0
  72. package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.json → testNew_query.query-meta.json} +2 -2
  73. package/test/mockRoot/deploy/testInstance/testBU/script/testExisting_script.script-meta.json +6 -0
  74. package/test/mockRoot/deploy/testInstance/testBU/script/testExisting_script.script-meta.ssjs +1 -0
  75. package/test/mockRoot/deploy/testInstance/testBU/script/testNew_script.script-meta.json +6 -0
  76. package/test/mockRoot/deploy/testInstance/testBU/script/testNew_script.script-meta.ssjs +1 -0
  77. package/test/mockRoot/deploy/testInstance/testBU/triggeredSend/testExisting_triggeredSend.triggeredSend-meta.json +29 -0
  78. package/test/mockRoot/deploy/testInstance/testBU/triggeredSend/testNew_triggeredSend.triggeredSend-meta.json +29 -0
  79. package/test/resourceFactory.js +132 -24
  80. package/test/resources/1111111/accountUser/retrieve-ActiveFlag=falseANDCustomerKey=testExisting_userANDEmaillike@-response.xml +27 -0
  81. package/test/resources/1111111/accountUser/retrieve-ActiveFlag=falseANDEmaillike@-response.xml +156 -0
  82. package/test/resources/1111111/accountUser/retrieve-ActiveFlag=trueANDEmailisNullORNamelikeapp userANDMustChangePassword=false-response.xml +87 -0
  83. package/test/resources/1111111/accountUser/retrieve-ActiveFlag=trueANDEmaillike@-response.xml +156 -0
  84. package/test/resources/1111111/accountUser/retrieve-CustomerKey=testExisting_userANDActiveFlag=trueANDEmailisNullORNamelikeapp userANDMustChangePassword=false-response.xml +27 -0
  85. package/test/resources/1111111/accountUserAccount/retrieve-AccountUser.AccountUserID=700301950-response.xml +60 -0
  86. package/test/resources/1111111/user/retrieve-expected.md +4 -2
  87. package/test/resources/9999999/attributeGroup/retrieve-expected.json +25 -0
  88. package/test/resources/9999999/attributeSet/retrieve-expected.json +748 -0
  89. package/test/resources/9999999/automation/build-expected.json +57 -0
  90. package/test/resources/9999999/automation/create-expected.json +45 -0
  91. package/test/resources/9999999/automation/create-testNew_automation-expected.md +28 -0
  92. package/test/resources/9999999/automation/delete-response.xml +40 -0
  93. package/test/resources/9999999/automation/patch_fixKeys-pause-expected.json +44 -0
  94. package/test/resources/9999999/automation/patch_fixKeys-schedule-expected.json +44 -0
  95. package/test/resources/9999999/automation/perform-08afb0e2-b00a-4c88-ad2e-1f7f8788c560-response.xml +42 -0
  96. package/test/resources/9999999/automation/perform-08afb0e2-b00a-4c88-fixKey_pause-response.xml +42 -0
  97. package/test/resources/9999999/automation/perform-08afb0e2-b00a-4c88-fixKey_schedule-response.xml +42 -0
  98. package/test/resources/9999999/automation/perform-a8afb0e2-b00a-4c88-ad2e-1f7f8788c560-response.xml +42 -0
  99. package/test/resources/9999999/automation/retrieve-expected.json +57 -0
  100. package/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md +30 -0
  101. package/test/resources/9999999/automation/schedule-08afb0e2-b00a-4c88-ad2e-1f7f8788c560-response.xml +52 -0
  102. package/test/resources/9999999/automation/schedule-08afb0e2-b00a-4c88-ad2e-pause-response.xml +38 -0
  103. package/test/resources/9999999/automation/schedule-08afb0e2-b00a-4c88-fixKey_pause-response.xml +52 -0
  104. package/test/resources/9999999/automation/schedule-08afb0e2-b00a-4c88-fixKey_schedule-response.xml +52 -0
  105. package/test/resources/9999999/automation/schedule-a8afb0e2-b00a-4c88-ad2e-1f7f8788c560-response.xml +52 -0
  106. package/test/resources/9999999/automation/template-expected.json +57 -0
  107. package/test/resources/9999999/automation/update-expected.json +45 -0
  108. package/test/resources/9999999/automation/update-testExisting_automation-expected.md +28 -0
  109. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  110. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json +85 -0
  111. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-pause/get-response.json +85 -0
  112. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-pause/patch-response.json +85 -0
  113. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-fixKey_pause/get-response.json +85 -0
  114. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-fixKey_pause/patch-response.json +85 -0
  115. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-fixKey_schedule/get-response.json +85 -0
  116. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-fixKey_schedule/patch-response.json +85 -0
  117. package/test/resources/9999999/automation/v1/automations/a8afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  118. package/test/resources/9999999/automation/v1/automations/post-response.json +86 -0
  119. package/test/resources/9999999/automation/v1/dataextracts/56c5370a-f988-4f36-b0ee-0f876573f6d7/get-response.json +38 -0
  120. package/test/resources/9999999/automation/v1/dataextracts/56c5370a-f988-4f36-b0ee-0f876573f6d7/patch-response.json +38 -0
  121. package/test/resources/9999999/automation/v1/dataextracts/get-response.json +20 -0
  122. package/test/resources/9999999/automation/v1/dataextracts/post-response.json +38 -0
  123. package/test/resources/9999999/automation/v1/dataextracttypes/get-response.json +50 -0
  124. package/test/resources/9999999/automation/v1/filetransfers/72c328ac-f5b0-4e37-91d3-a775666f15a6/get-response.json +18 -0
  125. package/test/resources/9999999/automation/v1/filetransfers/72c328ac-f5b0-4e37-91d3-a775666f15a6/patch-response.json +18 -0
  126. package/test/resources/9999999/automation/v1/filetransfers/get-response.json +15 -0
  127. package/test/resources/9999999/automation/v1/filetransfers/post-response.json +18 -0
  128. package/test/resources/9999999/automation/v1/ftplocations/get-response.json +18 -0
  129. package/test/resources/9999999/automation/v1/imports/9d16f42c-2260-ed11-b849-48df37d1de8b/patch-response.json +31 -0
  130. package/test/resources/9999999/automation/v1/imports/get-response.json +38 -0
  131. package/test/resources/9999999/automation/v1/imports/post-response.json +30 -0
  132. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dae/actions/start/post-response.txt +1 -0
  133. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/actions/start/post-response.txt +1 -0
  134. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/get-response.json +2 -2
  135. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +2 -2
  136. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat_fixKeys/get-response.json +17 -0
  137. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat_fixKeys/patch-response.json +18 -0
  138. package/test/resources/9999999/automation/v1/queries/get-response.json +22 -5
  139. package/test/resources/9999999/automation/v1/queries/post-response.json +2 -2
  140. package/test/resources/9999999/automation/v1/scripts/39f6a488-20eb-4ba0-b0b9-023725b574e4/patch-response.json +10 -0
  141. package/test/resources/9999999/automation/v1/scripts/get-response.json +27 -0
  142. package/test/resources/9999999/automation/v1/scripts/post-response.json +10 -0
  143. package/test/resources/9999999/dataExtension/retrieve-Name=testExisting_dataExtension-response.xml +52 -0
  144. package/test/resources/9999999/dataExtensionField/retrieve-DataExtension.CustomerKey=testExisting_dataExtension-response.xml +98 -0
  145. package/test/resources/9999999/dataExtensionField/retrieve-DataExtension.CustomerKey=testNew_dataExtensionORDataExtension.CustomerKey=testExisting_dataExtension-response.xml +99 -0
  146. package/test/resources/9999999/dataExtract/build-expected.json +35 -0
  147. package/test/resources/9999999/dataExtract/get-expected.json +39 -0
  148. package/test/resources/9999999/dataExtract/patch-expected.json +37 -0
  149. package/test/resources/9999999/dataExtract/post-expected.json +37 -0
  150. package/test/resources/9999999/dataExtract/template-expected.json +35 -0
  151. package/test/resources/9999999/dataFolder/retrieve-ContentType=automations-response.xml +48 -0
  152. package/test/resources/9999999/dataFolder/retrieve-ContentType=contextual_suppression_listORContentType=publicationORContentType=suppression_listORContentType=mysubsORContentType=list-response.xml +136 -0
  153. package/test/resources/9999999/dataFolder/retrieve-ContentType=queryactivity-response.xml +48 -0
  154. package/test/resources/9999999/dataFolder/retrieve-ContentType=ssjsactivity-response.xml +48 -0
  155. package/test/resources/9999999/dataFolder/retrieve-ContentType=triggered_send_journeybuilderORContentType=triggered_sendORContentType=hidden-response.xml +276 -0
  156. package/test/resources/9999999/dataFolder/retrieve-response.xml +45 -0
  157. package/test/resources/9999999/email/retrieve-response.xml +203 -0
  158. package/test/resources/9999999/emailSendDefinition/retrieve-IsPlatformObject=falseANDDescriptionnotEqualsSFSendDefinition-response.xml +85 -0
  159. package/test/resources/9999999/fileTransfer/build-expected.json +15 -0
  160. package/test/resources/9999999/fileTransfer/get-expected.json +17 -0
  161. package/test/resources/9999999/fileTransfer/patch-expected.json +17 -0
  162. package/test/resources/9999999/fileTransfer/post-expected.json +17 -0
  163. package/test/resources/9999999/fileTransfer/template-expected.json +15 -0
  164. package/test/resources/9999999/hub/v1/contacts/schema/attributeGroups/get-response.json +585 -0
  165. package/test/resources/9999999/hub/v1/contacts/schema/setDefinitions/get-response.json +19807 -0
  166. package/test/resources/9999999/importFile/build-expected.json +27 -0
  167. package/test/resources/9999999/importFile/get-expected.json +29 -0
  168. package/test/resources/9999999/importFile/patch-expected.json +29 -0
  169. package/test/resources/9999999/importFile/post-expected.json +29 -0
  170. package/test/resources/9999999/importFile/template-expected.json +27 -0
  171. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json +21 -0
  172. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/post-response.json +0 -0
  173. package/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json +30 -0
  174. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation-response.xml +30 -0
  175. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation_fixKey_pause-response.xml +32 -0
  176. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation_fixKey_schedule-response.xml +32 -0
  177. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation_fixedKey_paused-response.xml +32 -0
  178. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation_fixedKey_scheduled-response.xml +32 -0
  179. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation_pause-response.xml +30 -0
  180. package/test/resources/9999999/program/retrieve-CustomerKey=testNew_automation-response.xml +30 -0
  181. package/test/resources/9999999/program/retrieve-Name=testExisting_automation-response.xml +31 -0
  182. package/test/resources/9999999/program/retrieve-response.xml +50 -0
  183. package/test/resources/9999999/query/build-expected.json +2 -2
  184. package/test/resources/9999999/query/get-expected.json +2 -2
  185. package/test/resources/9999999/query/get2-expected.json +2 -2
  186. package/test/resources/9999999/query/patch-expected.json +2 -2
  187. package/test/resources/9999999/query/patch_fixKeys-expected.json +11 -0
  188. package/test/resources/9999999/query/patch_fixKeys-expected.sql +6 -0
  189. package/test/resources/9999999/query/post-expected.json +2 -2
  190. package/test/resources/9999999/query/template-expected.json +2 -2
  191. package/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testExisting_query_fixKeysANDStatus=Active-response.xml +30 -0
  192. package/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testExisting_query_fixedKeysANDStatus=Active-response.xml +30 -0
  193. package/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testNew_queryANDStatus=Active-response.xml +30 -0
  194. package/test/resources/9999999/script/build-expected.json +6 -0
  195. package/test/resources/9999999/script/build-expected.ssjs +1 -0
  196. package/test/resources/9999999/script/get-expected.json +8 -0
  197. package/test/resources/9999999/script/get-expected.ssjs +1 -0
  198. package/test/resources/9999999/script/get_noScriptTag-expected.html +1 -0
  199. package/test/resources/9999999/script/get_noScriptTag-expected.json +8 -0
  200. package/test/resources/9999999/script/patch-expected.json +8 -0
  201. package/test/resources/9999999/script/patch-expected.ssjs +1 -0
  202. package/test/resources/9999999/script/post-expected.json +8 -0
  203. package/test/resources/9999999/script/post-expected.ssjs +1 -0
  204. package/test/resources/9999999/script/template-expected.json +6 -0
  205. package/test/resources/9999999/script/template-expected.ssjs +1 -0
  206. package/test/resources/9999999/triggeredSend/build-expected.json +29 -0
  207. package/test/resources/9999999/triggeredSend/get-expected.json +29 -0
  208. package/test/resources/9999999/triggeredSend/patch-expected.json +29 -0
  209. package/test/resources/9999999/triggeredSend/post-expected.json +29 -0
  210. package/test/resources/9999999/triggeredSend/template-expected.json +29 -0
  211. package/test/resources/9999999/triggeredSendDefinition/create-response.xml +75 -0
  212. package/test/resources/9999999/triggeredSendDefinition/delete-response.xml +36 -0
  213. package/test/resources/9999999/triggeredSendDefinition/{retrieve-response.xml → retrieve-TriggeredSendStatusINNew,Active,Inactive,Moved,Canceled-response.xml} +4 -4
  214. package/test/resources/9999999/triggeredSendDefinition/update-response.xml +74 -0
  215. package/test/type.attributeGroup.test.js +55 -0
  216. package/test/type.attributeSet.test.js +55 -0
  217. package/test/type.automation.test.js +886 -0
  218. package/test/type.dataExtension.test.js +3 -1
  219. package/test/type.dataExtract.test.js +187 -0
  220. package/test/type.fileTransfer.test.js +185 -0
  221. package/test/type.importFile.test.js +186 -0
  222. package/test/type.mobileKeyword.test.js +0 -1
  223. package/test/type.query.test.js +497 -33
  224. package/test/type.script.test.js +367 -0
  225. package/test/type.triggeredSend.test.js +152 -0
  226. package/test/type.user.test.js +37 -11
  227. package/test/utils.js +10 -6
  228. package/.coverage-comment-template.md +0 -20
  229. package/lib/metadataTypes/SetDefinition.js +0 -37
  230. /package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.sql → testExisting_query.query-meta.sql} +0 -0
  231. /package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.sql → testNew_query.query-meta.sql} +0 -0
  232. /package/test/resources/1111111/accountUser/{retrieve-response.xml → retrieve-ActiveFlag=trueANDCustomerKey=testExisting_userANDEmaillike@-response.xml} +0 -0
  233. /package/test/resources/1111111/accountUserAccount/{retrieve-response.xml → retrieve-AccountUser.AccountUserIDIN700301950,700301951,7471228-response.xml} +0 -0
  234. /package/test/resources/1111111/businessUnit/{retrieve-response.xml → retrieve-ID=1111111-response.xml} +0 -0
  235. /package/test/resources/1111111/list/{retrieve-response.xml → retrieve-CustomerKey=All SubscribersORListName=All Subscribers-response.xml} +0 -0
  236. /package/test/resources/1111111/role/{retrieve-response.xml → retrieve-IsPrivate=false-response.xml} +0 -0
  237. /package/test/resources/9999999/queryDefinition/{retrieve-response.xml → retrieve-CustomerKey=testExisting_queryANDStatus=Active-response.xml} +0 -0
@@ -6,6 +6,7 @@ const Util = require('../util/util');
6
6
  const File = require('../util/file');
7
7
  const Definitions = require('../MetadataTypeDefinitions');
8
8
  const cache = require('../util/cache');
9
+ const pLimit = require('p-limit');
9
10
 
10
11
  /**
11
12
  * Automation MetadataType
@@ -13,6 +14,7 @@ const cache = require('../util/cache');
13
14
  * @augments MetadataType
14
15
  */
15
16
  class Automation extends MetadataType {
17
+ static notificationUpdates = {};
16
18
  /**
17
19
  * Retrieves Metadata of Automation
18
20
  *
@@ -35,42 +37,57 @@ class Automation extends MetadataType {
35
37
  };
36
38
  }
37
39
  const results = await this.client.soap.retrieveBulk('Program', ['ObjectID'], requestParams);
38
- if (results.Results?.length) {
40
+ if (results.Results?.length && !key) {
39
41
  // empty results will come back without "Results" defined
40
42
  Util.logger.info(
41
43
  Util.getGrayMsg(
42
- ` - ${results.Results?.length} Automations found. Retrieving details...`
44
+ ` - ${results.Results?.length} automation${
45
+ results.Results?.length === 1 ? '' : 's'
46
+ } found. Retrieving details...`
43
47
  )
44
48
  );
45
49
  }
50
+ // the API seems to handle 50 concurrent requests nicely
51
+ const rateLimit = pLimit(50);
52
+
46
53
  const details = results.Results
47
54
  ? await Promise.all(
48
- results.Results.map(async (a) => {
49
- try {
50
- return await this.client.rest.get(
51
- '/automation/v1/automations/' + a.ObjectID
52
- );
53
- } catch (ex) {
55
+ results.Results.map(async (item) =>
56
+ rateLimit(async () => {
54
57
  try {
55
- if (ex.message == 'socket hang up') {
56
- // one more retry; it's a rare case but retrying again should solve the issue gracefully
57
- return await this.client.rest.get(
58
- '/automation/v1/automations/' + a.ObjectID
59
- );
58
+ return await this.client.rest.get(
59
+ '/automation/v1/automations/' + item.ObjectID
60
+ );
61
+ } catch (ex) {
62
+ try {
63
+ if (ex.message == 'socket hang up') {
64
+ // one more retry; it's a rare case but retrying again should solve the issue gracefully
65
+ return await this.client.rest.get(
66
+ '/automation/v1/automations/' + item.ObjectID
67
+ );
68
+ }
69
+ } catch {
70
+ // no extra action needed, handled below
60
71
  }
61
- } catch {
62
- // no extra action needed, handled below
72
+ // if we do get here, we should log the error and continue instead of failing to download all automations
73
+ Util.logger.error(
74
+ ` ☇ skipping Automation ${item.ObjectID}: ${ex.message} ${ex.code}`
75
+ );
76
+ return null;
63
77
  }
64
- // if we do get here, we should log the error and continue instead of failing to download all automations
65
- Util.logger.error(
66
- ` ☇ skipping Automation ${a.ObjectID}: ${ex.message} ${ex.code}`
67
- );
68
- return null;
69
- }
70
- })
78
+ })
79
+ )
71
80
  )
72
81
  : [];
73
- let metadataMap = this.parseResponseBody({ items: details });
82
+
83
+ // * if retrieving some automations fails, a null element would remain in the details-array for each of them that needs to be filtered to prevent it from causing issues elsewhere
84
+ let metadataMap = this.parseResponseBody({ items: details.filter(Boolean) });
85
+
86
+ if (Object.keys(metadataMap).length) {
87
+ // attach notification information to each automation that has any
88
+ await this.#getAutomationNotificationsREST(metadataMap);
89
+ }
90
+
74
91
  // * retrieveDir can be empty when we use it in the context of postDeployTasks
75
92
  if (retrieveDir) {
76
93
  metadataMap = await this.saveResults(metadataMap, retrieveDir, null, null);
@@ -83,6 +100,85 @@ class Automation extends MetadataType {
83
100
  }
84
101
  return { metadata: metadataMap, type: this.definition.type };
85
102
  }
103
+
104
+ /**
105
+ * helper for {@link Automation.retrieve} to get Automation Notifications
106
+ *
107
+ * @private
108
+ * @param {TYPE.MetadataTypeMap} metadataMap keyField => metadata map
109
+ * @returns {Promise.<void>} Promise of nothing
110
+ */
111
+ static async #getAutomationNotificationsREST(metadataMap) {
112
+ Util.logger.info(Util.getGrayMsg(` Retrieving Automation Notification information...`));
113
+
114
+ // get list of keys that we retrieved so far
115
+ const foundKeys = Object.keys(metadataMap);
116
+
117
+ // get encodedAutomationID to retrieve notification information
118
+ const iteratorBackup = this.definition.bodyIteratorField;
119
+ this.definition.bodyIteratorField = 'entry';
120
+ const automationLegacyMapObj = await super.retrieveREST(
121
+ undefined,
122
+ `/legacy/v1/beta/bulk/automations/automation/definition/`
123
+ );
124
+ this.definition.bodyIteratorField = iteratorBackup;
125
+ const automationLegacyMap = Object.keys(automationLegacyMapObj.metadata)
126
+ .filter((key) => foundKeys.includes(key))
127
+ // ! using the `id` field to retrieve notifications does not work. instead one needs to use the URL in the `notifications` field
128
+ .map((key) => ({
129
+ id: automationLegacyMapObj.metadata[key].id,
130
+ key,
131
+ }));
132
+
133
+ // get notifications for each automation
134
+ const rateLimit = pLimit(5);
135
+ let found = 0;
136
+ let skipped = 0;
137
+ const promiseMap = await Promise.all(
138
+ automationLegacyMap.map((automationLegacy) =>
139
+ rateLimit(async () => {
140
+ // this is a file so extended is at another endpoint
141
+ try {
142
+ const notificationsResult = await this.client.rest.get(
143
+ '/legacy/v1/beta/automations/notifications/' + automationLegacy.id
144
+ );
145
+ if (Array.isArray(notificationsResult?.workers)) {
146
+ metadataMap[automationLegacy.key].notifications =
147
+ notificationsResult.workers.map((n) => ({
148
+ email: n.definition.split(',').map((item) => item.trim()),
149
+ message: n.body,
150
+ type: n.notificationType,
151
+ }));
152
+ found++;
153
+ } else {
154
+ if (
155
+ !notificationsResult ||
156
+ typeof notificationsResult !== 'object' ||
157
+ Object.keys(notificationsResult).length !== 1 ||
158
+ !notificationsResult?.programId
159
+ ) {
160
+ throw new TypeError(JSON.stringify(notificationsResult));
161
+ }
162
+ // * if there are no automation notifications, the API returns a single object with the programId
163
+ }
164
+ } catch (ex) {
165
+ Util.logger.debug(
166
+ ` ☇ issue retrieving Notifications for automation ${automationLegacy.key}: ${ex.message} ${ex.code}`
167
+ );
168
+ skipped++;
169
+ }
170
+ })
171
+ )
172
+ );
173
+ Util.logger.info(
174
+ Util.getGrayMsg(` Notifications found for ${found} automation${found === 1 ? '' : 's'}`)
175
+ );
176
+ Util.logger.debug(
177
+ `Notifications not found for ${skipped} automation${skipped === 1 ? '' : 's'}`
178
+ );
179
+ return promiseMap;
180
+ }
181
+
86
182
  /**
87
183
  * Retrieves Metadata of Automation
88
184
  *
@@ -134,6 +230,7 @@ class Automation extends MetadataType {
134
230
  * @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
135
231
  */
136
232
  static async retrieveForCache() {
233
+ // get automations for cache
137
234
  const results = await this.client.soap.retrieveBulk('Program', [
138
235
  'ObjectID',
139
236
  'CustomerKey',
@@ -141,14 +238,30 @@ class Automation extends MetadataType {
141
238
  ]);
142
239
  const resultsConverted = {};
143
240
  if (Array.isArray(results?.Results)) {
241
+ // get encodedAutomationID to retrieve notification information
242
+ const keyBackup = this.definition.keyField;
243
+ const iteratorBackup = this.definition.bodyIteratorField;
244
+ this.definition.keyField = 'key';
245
+ this.definition.bodyIteratorField = 'entry';
246
+ const automationsLegacy = await super.retrieveREST(
247
+ undefined,
248
+ `/legacy/v1/beta/bulk/automations/automation/definition/`
249
+ );
250
+ this.definition.keyField = keyBackup;
251
+ this.definition.bodyIteratorField = iteratorBackup;
252
+
253
+ // merge encodedAutomationID into results
144
254
  for (const m of results.Results) {
145
255
  resultsConverted[m.CustomerKey] = {
146
256
  id: m.ObjectID,
147
257
  key: m.CustomerKey,
148
258
  name: m.Name,
259
+ programId: automationsLegacy.metadata[m.CustomerKey]?.id,
260
+ status: automationsLegacy.metadata[m.CustomerKey]?.status,
149
261
  };
150
262
  }
151
263
  }
264
+
152
265
  return { metadata: resultsConverted, type: this.definition.type };
153
266
  }
154
267
 
@@ -175,14 +288,21 @@ class Automation extends MetadataType {
175
288
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
176
289
  return;
177
290
  }
178
- const details = await this.client.rest.get(
291
+ let details = await this.client.rest.get(
179
292
  '/automation/v1/automations/' + metadata.ObjectID
180
293
  );
294
+ const metadataMap = this.parseResponseBody({ items: [details] });
295
+ if (Object.keys(metadataMap).length) {
296
+ // attach notification information to each automation that has any
297
+ await this.#getAutomationNotificationsREST(metadataMap);
298
+ details = Object.values(metadataMap)[0];
299
+ }
300
+
181
301
  let val = null;
182
302
  let originalKey;
183
303
  // if parsing fails, we should just save what we get
184
304
  try {
185
- const parsedDetails = this.parseMetadata(details);
305
+ const parsedDetails = this.postRetrieveTasks(details);
186
306
  originalKey = parsedDetails[this.definition.keyField];
187
307
  if (parsedDetails !== null) {
188
308
  val = JSON.parse(
@@ -214,14 +334,344 @@ class Automation extends MetadataType {
214
334
  throw new Error(JSON.stringify(results));
215
335
  }
216
336
  }
337
+ /**
338
+ * helper for {@link Automation.postRetrieveTasks} and {@link Automation.execute}
339
+ *
340
+ * @param {TYPE.AutomationItem} metadata a single automation
341
+ * @returns {boolean} true if the automation schedule is valid
342
+ */
343
+ static #isValidSchedule(metadata) {
344
+ if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
345
+ try {
346
+ if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
347
+ // if we found the id in our list, remove the redundant data
348
+ delete metadata.schedule.timezoneId;
349
+ }
350
+ } catch {
351
+ Util.logger.debug(
352
+ `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
353
+ );
354
+ }
355
+ return true;
356
+ } else {
357
+ return false;
358
+ }
359
+ }
217
360
  /**
218
361
  * manages post retrieve steps
219
362
  *
220
363
  * @param {TYPE.AutomationItem} metadata a single automation
221
- * @returns {TYPE.AutomationItem} metadata
364
+ * @returns {TYPE.AutomationItem | void} parsed item
222
365
  */
223
366
  static postRetrieveTasks(metadata) {
224
- return this.parseMetadata(metadata);
367
+ // folder
368
+ this.setFolderPath(metadata);
369
+ // automations are often skipped due to lack of support.
370
+ try {
371
+ if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
372
+ // Starting Source == 'Schedule'
373
+
374
+ if (!this.#isValidSchedule(metadata)) {
375
+ return;
376
+ }
377
+ // type 'Running' is temporary status only, overwrite with Scheduled for storage.
378
+ if (metadata.type === 'scheduled' && metadata.status === 'Running') {
379
+ metadata.status = 'Scheduled';
380
+ }
381
+ } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
382
+ // Starting Source == 'File Drop'
383
+ // Do nothing for now
384
+ }
385
+ if (metadata.steps) {
386
+ let i = 0;
387
+
388
+ for (const step of metadata.steps) {
389
+ i++;
390
+
391
+ const stepNumber = step.stepNumber || step.step || i;
392
+ delete step.stepNumber;
393
+ delete step.step;
394
+
395
+ for (const activity of step.activities) {
396
+ try {
397
+ // get metadata type of activity
398
+ activity.r__type = Util.inverseGet(
399
+ this.definition.activityTypeMapping,
400
+ activity.objectTypeId
401
+ );
402
+ delete activity.objectTypeId;
403
+ } catch {
404
+ Util.logger.warn(
405
+ ` - Unknown activity type '${activity.objectTypeId}'` +
406
+ ` in step ${stepNumber}.${activity.displayOrder}` +
407
+ ` of Automation '${metadata.name}'`
408
+ );
409
+ continue;
410
+ }
411
+
412
+ // if no activityObjectId then either serialized activity
413
+ // (config in Automation ) or unconfigured so no further action to be taken
414
+ if (
415
+ activity.activityObjectId === '00000000-0000-0000-0000-000000000000' ||
416
+ activity.activityObjectId == null
417
+ ) {
418
+ Util.logger.debug(
419
+ ` - skipping ${
420
+ metadata[this.definition.keyField]
421
+ } activity ${stepNumber}.${
422
+ activity.displayOrder
423
+ } due to missing activityObjectId: ${JSON.stringify(activity)}`
424
+ );
425
+ // empty if block
426
+ } else if (!this.definition.dependencies.includes(activity.r__type)) {
427
+ Util.logger.debug(
428
+ ` - skipping ${
429
+ metadata[this.definition.keyField]
430
+ } activity ${stepNumber}.${
431
+ activity.displayOrder
432
+ } because the type ${
433
+ activity.r__type
434
+ } is not set up as a dependency for ${this.definition.type}`
435
+ );
436
+ }
437
+ // / if managed by cache we can update references to support deployment
438
+ else if (
439
+ Definitions[activity.r__type]?.['idField'] &&
440
+ cache.getCache(this.buObject.mid)[activity.r__type]
441
+ ) {
442
+ try {
443
+ // this will override the name returned by the API in case this activity's name was changed since the automation was last updated, keeping things nicely in sync for mcdev
444
+ const name = cache.searchForField(
445
+ activity.r__type,
446
+ activity.activityObjectId,
447
+ Definitions[activity.r__type].idField,
448
+ Definitions[activity.r__type].nameField
449
+ );
450
+ if (name !== activity.name) {
451
+ Util.logger.debug(
452
+ ` - updated name of step ${stepNumber}.${activity.displayOrder}` +
453
+ ` in Automation '${metadata.name}' from ${activity.name} to ${name}`
454
+ );
455
+ activity.name = name;
456
+ }
457
+ } catch (ex) {
458
+ // getFromCache throws error where the dependent metadata is not found
459
+ Util.logger.warn(
460
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
461
+ ` in step ${stepNumber}.${activity.displayOrder}` +
462
+ ` of Automation '${metadata.name}' (${ex.message})`
463
+ );
464
+ }
465
+ } else {
466
+ Util.logger.warn(
467
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
468
+ ` in step ${stepNumber}.${activity.displayOrder}` +
469
+ ` of Automation '${metadata.name}' (Not Found in Cache)`
470
+ );
471
+ }
472
+ }
473
+ }
474
+ }
475
+ return JSON.parse(JSON.stringify(metadata));
476
+ } catch (ex) {
477
+ Util.logger.warn(
478
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
479
+ ex.message
480
+ }`
481
+ );
482
+ return null;
483
+ }
484
+ }
485
+ /**
486
+ * a function to start query execution via API
487
+ *
488
+ * @param {string[]} keyArr customerkey of the metadata
489
+ * @returns {Promise.<string[]>} Returns list of keys that were executed
490
+ */
491
+ static async execute(keyArr) {
492
+ const metadataMap = {};
493
+ for (const key of keyArr) {
494
+ if (Util.OPTIONS.schedule) {
495
+ // schedule
496
+ const results = await this.retrieve(undefined, undefined, undefined, key);
497
+ if (Object.keys(results.metadata).length) {
498
+ for (const resultKey of Object.keys(results.metadata)) {
499
+ if (this.#isValidSchedule(results.metadata[resultKey])) {
500
+ metadataMap[resultKey] = results.metadata[resultKey];
501
+ } else {
502
+ Util.logger.error(
503
+ ` - skipping ${this.definition.type} ${results.metadata[resultKey].name}: no valid schedule settings found.`
504
+ );
505
+ }
506
+ }
507
+ }
508
+ } else {
509
+ // runOnce
510
+ const objectId = await this.#getObjectIdForSingleRetrieve(key);
511
+ metadataMap[key] = {};
512
+ metadataMap[key][this.definition.idField] = objectId;
513
+ metadataMap[key][this.definition.keyField] = key;
514
+ }
515
+ }
516
+ if (!Object.keys(metadataMap).length) {
517
+ Util.logger.error(`No ${this.definition.type} to execute`);
518
+ return false;
519
+ }
520
+ Util.logger.info(
521
+ `Starting automations ${
522
+ Util.OPTIONS.schedule
523
+ ? 'according to schedule'
524
+ : 'to run once (use --schedule or --execute=schedule to schedule instead)'
525
+ }: ${Object.keys(metadataMap).length}`
526
+ );
527
+ const promiseResults = [];
528
+ for (const key of Object.keys(metadataMap)) {
529
+ if (Util.OPTIONS.schedule && metadataMap[key].status === 'Scheduled') {
530
+ // schedule
531
+ Util.logger.info(
532
+ ` - skipping ${this.definition.type} ${metadataMap[key].name}: already scheduled.`
533
+ );
534
+ } else {
535
+ // schedule + runOnce
536
+ promiseResults.push(this.#executeItem(metadataMap, key));
537
+ }
538
+ }
539
+ const results = await Promise.all(promiseResults);
540
+ const executedKeyArr = results
541
+ .filter(Boolean)
542
+ .filter((r) => r.response.OverallStatus === 'OK')
543
+ .map((r) => r.key);
544
+ Util.logger.info(`Executed ${executedKeyArr.length} of ${keyArr.length} items`);
545
+ return executedKeyArr;
546
+ }
547
+ /**
548
+ * helper for {@link Automation.execute}
549
+ *
550
+ * @param {TYPE.AutomationMap} metadataMap map of metadata
551
+ * @param {string} key key of the metadata
552
+ * @returns {Promise.<{key:string, response:object}>} metadata key and API response
553
+ */
554
+ static async #executeItem(metadataMap, key) {
555
+ if (Util.OPTIONS.schedule) {
556
+ this.#preDeploySchedule(metadataMap[key]);
557
+ metadataMap[key].status = 'Scheduled';
558
+ return this.#scheduleAutomation(metadataMap, metadataMap, key);
559
+ } else {
560
+ return this.#runOnce(metadataMap[key]);
561
+ }
562
+ }
563
+
564
+ /**
565
+ * helper for {@link Automation.execute}
566
+ *
567
+ * @param {TYPE.AutomationItem} metadataEntry metadata object
568
+ * @returns {Promise.<{key:string, response:object}>} metadata key and API response
569
+ */
570
+ static async #runOnce(metadataEntry) {
571
+ return super.executeSOAP(metadataEntry);
572
+ }
573
+
574
+ /**
575
+ * Standardizes a check for multiple messages but adds query specific filters to error texts
576
+ *
577
+ * @param {object} ex response payload from REST API
578
+ * @returns {string[] | void} formatted Error Message
579
+ */
580
+ static getErrorsREST(ex) {
581
+ const errors = super.getErrorsREST(ex);
582
+ if (errors?.length > 0) {
583
+ return errors.map((msg) =>
584
+ msg
585
+ .split('403 Forbidden')
586
+ .join('403 Forbidden: Please check if the automation is currently running.')
587
+ );
588
+ }
589
+ return errors;
590
+ }
591
+
592
+ /**
593
+ * a function to start query execution via API
594
+ *
595
+ * @param {string[]} keyArr customerkey of the metadata
596
+ * @returns {Promise.<string[]>} Returns list of keys that were paused
597
+ */
598
+ static async pause(keyArr) {
599
+ const metadataMap = {};
600
+ for (const key of keyArr) {
601
+ if (key) {
602
+ const results = await this.retrieve(undefined, undefined, undefined, key);
603
+ if (Object.keys(results.metadata).length) {
604
+ for (const key of Object.keys(results.metadata)) {
605
+ if (this.#isValidSchedule(results.metadata[key])) {
606
+ metadataMap[key] = results.metadata[key];
607
+ } else {
608
+ Util.logger.error(
609
+ ` - skipping ${this.definition.type} ${results.metadata[key].name}: no valid schedule settings found.`
610
+ );
611
+ }
612
+ }
613
+ }
614
+ }
615
+ }
616
+
617
+ Util.logger.info(`Pausing automations: ${Object.keys(metadataMap).length}`);
618
+ const promiseResults = [];
619
+ for (const key of Object.keys(metadataMap)) {
620
+ if (metadataMap[key].status === 'Scheduled') {
621
+ promiseResults.push(this.#pauseItem(metadataMap[key]));
622
+ } else if (metadataMap[key].status === 'PausedSchedule') {
623
+ Util.logger.info(
624
+ ` - skipping ${this.definition.type} ${metadataMap[key].name}: already paused.`
625
+ );
626
+ } else {
627
+ Util.logger.error(
628
+ ` - skipping ${this.definition.type} ${
629
+ metadataMap[key].name
630
+ }: currently ${metadataMap[
631
+ key
632
+ ].status.toLowerCase()}. Please try again in a few minutes.`
633
+ );
634
+ }
635
+ }
636
+ const pausedKeyArr = (await Promise.all(promiseResults))
637
+ .filter(Boolean)
638
+ .filter((r) => r.response.OverallStatus === 'OK')
639
+ .map((r) => r.key);
640
+
641
+ Util.logger.info(`Paused ${pausedKeyArr.length} of ${keyArr.length} items`);
642
+ return pausedKeyArr;
643
+ }
644
+
645
+ /**
646
+ * helper for {@link Automation.pause}
647
+ *
648
+ * @param {TYPE.AutomationItem} metadata automation metadata
649
+ * @returns {Promise.<{key:string, response:object}>} metadata key and API response
650
+ */
651
+ static async #pauseItem(metadata) {
652
+ const schedule = {};
653
+ try {
654
+ const response = await this.client.soap.schedule(
655
+ 'Automation',
656
+ schedule,
657
+ {
658
+ Interaction: {
659
+ ObjectID: metadata[this.definition.idField],
660
+ },
661
+ },
662
+ 'pause',
663
+ {}
664
+ );
665
+ Util.logger.info(
666
+ ` - paused ${this.definition.type}: ${metadata[this.definition.keyField]} / ${
667
+ metadata[this.definition.nameField]
668
+ }`
669
+ );
670
+ return { key: metadata[this.definition.keyField], response };
671
+ } catch (ex) {
672
+ this._handleSOAPErrors(ex, 'pausing', metadata, false);
673
+ return null;
674
+ }
225
675
  }
226
676
 
227
677
  /**
@@ -230,12 +680,19 @@ class Automation extends MetadataType {
230
680
  * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
231
681
  * @param {string} targetBU name/shorthand of target businessUnit for mapping
232
682
  * @param {string} retrieveDir directory where metadata after deploy should be saved
233
- * @param {boolean} [isRefresh] optional flag - so far not used by automation
234
683
  * @returns {Promise.<TYPE.AutomationMap>} Promise
235
684
  */
236
- static async deploy(metadata, targetBU, retrieveDir, isRefresh) {
237
- const upsertResults = await this.upsert(metadata, targetBU, isRefresh);
238
- await this.saveResults(upsertResults, retrieveDir, null);
685
+ static async deploy(metadata, targetBU, retrieveDir) {
686
+ const upsertResults = await this.upsert(metadata, targetBU);
687
+ const savedMetadata = await this.saveResults(upsertResults, retrieveDir, null);
688
+ if (
689
+ this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
690
+ !this.definition.documentInOneFile
691
+ ) {
692
+ const count = Object.keys(savedMetadata).length;
693
+ Util.logger.debug(` - Running document for ${count} record${count === 1 ? '' : 's'}`);
694
+ await this.document(savedMetadata);
695
+ }
239
696
  return upsertResults;
240
697
  }
241
698
 
@@ -258,11 +715,47 @@ class Automation extends MetadataType {
258
715
  * @returns {Promise} Promise
259
716
  */
260
717
  static update(metadata, metadataBefore) {
261
- metadata.id = metadataBefore.id;
718
+ if (metadataBefore.status === 'Running') {
719
+ Util.logger.error(
720
+ ` ☇ error updating ${this.definition.type} ${
721
+ metadata[this.definition.keyField] || metadata[this.definition.nameField]
722
+ } / ${
723
+ metadata[this.definition.nameField]
724
+ }: You cannot update an automation that's currently running. Please wait a bit and retry.`
725
+ );
726
+ return null;
727
+ }
262
728
  const uri = '/automation/v1/automations/' + metadata.id;
263
729
  return super.updateREST(metadata, uri);
264
730
  }
265
731
 
732
+ /**
733
+ * helper for {@link Automation.preDeployTasks} and {@link Automation.execute}
734
+ *
735
+ * @param {TYPE.AutomationItem} metadata metadata mapped by their keyField
736
+ */
737
+ static #preDeploySchedule(metadata) {
738
+ delete metadata.schedule.rangeTypeId;
739
+ delete metadata.schedule.pattern;
740
+ delete metadata.schedule.scheduledTime;
741
+ delete metadata.schedule.scheduledStatus;
742
+ if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
743
+ metadata.schedule.timezoneId =
744
+ this.definition.timeZoneMapping[metadata.schedule.timezoneName];
745
+ } else {
746
+ Util.logger.error(
747
+ `Could not find timezone ${metadata.schedule.timezoneName} in definition.timeZoneMapping`
748
+ );
749
+ }
750
+
751
+ // the upsert API needs this to be named scheduleTypeId; the retrieve API returns it as typeId
752
+ metadata.schedule.scheduleTypeId = metadata.schedule.typeId;
753
+ delete metadata.schedule.typeId;
754
+
755
+ // prep startSource
756
+ metadata.startSource = { schedule: metadata.schedule, typeId: 1 };
757
+ }
758
+
266
759
  /**
267
760
  * Gets executed before deploying metadata
268
761
  *
@@ -270,6 +763,15 @@ class Automation extends MetadataType {
270
763
  * @returns {Promise.<TYPE.AutomationItem>} Promise
271
764
  */
272
765
  static async preDeployTasks(metadata) {
766
+ if (metadata.notifications) {
767
+ this.notificationUpdates[metadata.key] = metadata.notifications;
768
+ } else {
769
+ const cached = cache.getByKey(metadata.key);
770
+ if (cached?.notifications) {
771
+ // if notifications existed but are no longer present in the deployment package, we need to run an empty update call to remove them
772
+ this.notificationUpdates[metadata.key] = [];
773
+ }
774
+ }
273
775
  if (this.validateDeployMetadata(metadata)) {
274
776
  // folder
275
777
  this.setFolderId(metadata);
@@ -277,25 +779,12 @@ class Automation extends MetadataType {
277
779
  if (metadata.type === 'scheduled' && metadata?.schedule?.startDate) {
278
780
  // Starting Source == 'Schedule'
279
781
 
280
- delete metadata.schedule.rangeTypeId;
281
- delete metadata.schedule.pattern;
282
- delete metadata.schedule.scheduledTime;
283
- delete metadata.schedule.scheduledStatus;
284
- if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
285
- metadata.schedule.timezoneId =
286
- this.definition.timeZoneMapping[metadata.schedule.timezoneName];
287
- } else {
288
- Util.logger.error(
289
- `Could not find timezone ${metadata.schedule.timezoneName} in definition.timeZoneMapping`
290
- );
291
- }
292
- delete metadata.schedule.timezoneName;
293
- // the upsert API needs this to be named scheduleTypeId; the retrieve API returns it as typeId
294
- metadata.schedule.scheduleTypeId = metadata.schedule.typeId;
295
- delete metadata.schedule.typeId;
782
+ this.#preDeploySchedule(metadata);
783
+ // * run _buildSchedule here but only to check if things look ok - do not use the returned schedule object for deploy
784
+ this._buildSchedule(metadata.schedule);
296
785
 
297
- // prep startSource
298
- metadata.startSource = { schedule: metadata.schedule, typeId: 1 };
786
+ delete metadata.schedule.timezoneName;
787
+ delete metadata.startSource.schedule.timezoneName;
299
788
  } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
300
789
  // Starting Source == 'File Drop'
301
790
 
@@ -393,6 +882,16 @@ class Automation extends MetadataType {
393
882
  }
394
883
  return deployable;
395
884
  }
885
+ /**
886
+ * helper for {@link MetadataType.updateREST} and {@link MetadataType.updateSOAP} that removes old files after the key was changed
887
+ *
888
+ * @private
889
+ * @param {TYPE.MetadataTypeItem} metadataEntry a single metadata Entry
890
+ * @returns {void}
891
+ */
892
+ static async _postChangeKeyTasks(metadataEntry) {
893
+ super._postChangeKeyTasks(metadataEntry, true);
894
+ }
396
895
 
397
896
  /**
398
897
  * Gets executed after deployment of metadata type
@@ -403,87 +902,185 @@ class Automation extends MetadataType {
403
902
  */
404
903
  static async postDeployTasks(metadataMap, originalMetadataMap) {
405
904
  for (const key in metadataMap) {
406
- // need to put schedule on here if status is scheduled
905
+ const item = metadataMap[key];
407
906
 
408
- if (originalMetadataMap[key]?.type === 'scheduled') {
409
- // Starting Source == 'Schedule': Try starting the automation
410
- if (originalMetadataMap[key].status === 'Scheduled') {
411
- let schedule = null;
412
- try {
413
- schedule = this._buildSchedule(originalMetadataMap[key].schedule);
414
- } catch (ex) {
415
- Util.logger.error(
416
- `- Could not create schedule for automation ${originalMetadataMap[key].name} to start it: ${ex.message}`
417
- );
418
- }
419
- if (schedule !== null) {
420
- try {
421
- // remove the fields that are not needed for the schedule but only for CLI output
422
- const schedule_StartDateTime = schedule._StartDateTime;
423
- delete schedule._StartDateTime;
424
- const schedule_interval = schedule._interval;
425
- delete schedule._interval;
426
- const schedule_timezoneString = schedule._timezoneString;
427
- delete schedule._timezoneString;
428
- // start the automation
429
- await this.client.soap.schedule(
430
- 'Automation',
431
- schedule,
432
- {
433
- Interaction: {
434
- ObjectID: metadataMap[key].id,
435
- },
436
- },
437
- 'start',
438
- {}
439
- );
440
- const intervalString =
441
- (schedule_interval > 1 ? `${schedule_interval} ` : '') +
442
- (schedule.RecurrenceType === 'Daily'
443
- ? 'Day'
444
- : schedule.RecurrenceType.slice(0, -2) +
445
- (schedule_interval > 1 ? 's' : ''));
446
- Util.logger.warn(
447
- ` - scheduled automation '${
448
- originalMetadataMap[key].name
449
- }' deployed Active: runs every ${intervalString} starting ${
450
- schedule_StartDateTime.split('T').join(' ').split('.')[0]
451
- } ${schedule_timezoneString}`
452
- );
453
- } catch (ex) {
454
- Util.logger.error(
455
- `- Could not start scheduled automation '${originalMetadataMap[key].name}': ${ex.message}`
456
- );
457
- }
458
- }
459
- } else {
460
- Util.logger.warn(
461
- ` - scheduled automation '${originalMetadataMap[key].name}' deployed Paused`
907
+ const oldKey = Util.changedKeysMap?.[this.definition.type]?.[key] || key;
908
+ delete Util.changedKeysMap?.[this.definition.type]?.[key];
909
+
910
+ if (!item.type) {
911
+ // create response does not return the type attribute
912
+
913
+ const scheduleHelper = item.schedule || item.startSource.schedule;
914
+
915
+ // el.type
916
+ item.type = scheduleHelper
917
+ ? 'scheduled'
918
+ : item.fileTrigger
919
+ ? 'triggered'
920
+ : undefined;
921
+
922
+ // el.schedule.timezoneName
923
+ if (item.type === 'scheduled') {
924
+ // not existing for triggered automations
925
+ scheduleHelper.timezoneName ||= Util.inverseGet(
926
+ this.definition.timeZoneMapping,
927
+ scheduleHelper.timezoneId
462
928
  );
463
929
  }
930
+
931
+ // el.status
932
+ item.status ||= Util.inverseGet(this.definition.statusMapping, item.statusId);
464
933
  }
465
- if (metadataMap[key].startSource) {
466
- metadataMap[key].schedule = metadataMap[key].startSource.schedule;
934
+ // need to put schedule on here if status is scheduled
935
+ await Automation.#scheduleAutomation(metadataMap, originalMetadataMap, key, oldKey);
936
+
937
+ // need to update notifications separately if there are any
938
+ await Automation.#updateNotificationInfoREST(metadataMap, key);
467
939
 
468
- delete metadataMap[key].startSource;
940
+ // rewrite upsert to retrieve fields
941
+ if (item.steps) {
942
+ for (const step of item.steps) {
943
+ step.name = step.annotation;
944
+ delete step.annotation;
945
+ }
469
946
  }
470
- if (metadataMap[key].schedule) {
471
- metadataMap[key].schedule.typeId = metadataMap[key].schedule.scheduleTypeId;
472
- delete metadataMap[key].schedule.scheduleTypeId;
947
+ }
948
+ if (Util.OPTIONS.execute || Util.OPTIONS.schedule) {
949
+ Util.logger.info(`Executing: ${this.definition.type}`);
950
+ await this.execute(Object.keys(metadataMap));
951
+ }
952
+ }
953
+ /**
954
+ * helper for {@link Automation.postDeployTasks}
955
+ *
956
+ * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
957
+ * @param {string} key current customer key
958
+ * @returns {Promise.<void>} -
959
+ */
960
+ static async #updateNotificationInfoREST(metadataMap, key) {
961
+ if (this.notificationUpdates[key]) {
962
+ // create & update automation calls return programId as 'legacyId'; retrieve does not return it
963
+ const programId = metadataMap[key]?.legacyId;
964
+ if (programId) {
965
+ const notificationBody = {
966
+ programId,
967
+ workers: this.notificationUpdates[key].map((notification) => ({
968
+ programId,
969
+ notificationType: notification.type,
970
+ definition: Array.isArray(notification.email)
971
+ ? notification.email.join(',')
972
+ : notification.email,
973
+ body: notification.message,
974
+ channelType: 'Account',
975
+ })),
976
+ };
977
+ try {
978
+ const result = await this.client.rest.post(
979
+ '/legacy/v1/beta/automations/notifications/' + programId,
980
+ notificationBody
981
+ );
982
+ if (result) {
983
+ // should be empty if all OK
984
+ throw new Error(result);
985
+ }
986
+ } catch (ex) {
987
+ Util.logger.error(
988
+ `Error updating notifications for automation '${metadataMap[key].name}': ${ex.message} (${ex.code}))`
989
+ );
990
+ }
991
+ Util.logger.info(
992
+ Util.getGrayMsg(
993
+ ` - updated notifications for automation '${metadataMap[key].name}'`
994
+ )
995
+ );
473
996
  }
997
+ }
998
+ }
474
999
 
475
- // re-retrieve deployed items because the API does not return any info for them except the new id (api key)
476
- try {
477
- const { metadata } = await this.retrieve(null, null, null, key);
478
- metadataMap[key] = Object.values(metadata)[0];
479
- // postRetrieveTasks will be run automatically on this via super.saveResult
480
- } catch (ex) {
481
- throw new Error(
482
- `Could not get details for new ${this.definition.type} ${key} from server (${ex.message})`
1000
+ /**
1001
+ * helper for {@link Automation.postDeployTasks}
1002
+ *
1003
+ * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
1004
+ * @param {TYPE.AutomationMap} originalMetadataMap metadata to be updated (contains additioanl fields)
1005
+ * @param {string} key current customer key
1006
+ * @param {string} [oldKey] old customer key before fixKey / changeKeyValue / changeKeyField
1007
+ * @returns {Promise.<{key:string, response:object}>} metadata key and API response
1008
+ */
1009
+ static async #scheduleAutomation(metadataMap, originalMetadataMap, key, oldKey) {
1010
+ let response = null;
1011
+ oldKey ||= key;
1012
+ if (originalMetadataMap[oldKey]?.type === 'scheduled') {
1013
+ // Starting Source == 'Schedule': Try starting the automation
1014
+ if (originalMetadataMap[oldKey].status === 'Scheduled') {
1015
+ let schedule = null;
1016
+ try {
1017
+ schedule = this._buildSchedule(originalMetadataMap[oldKey].schedule);
1018
+ } catch (ex) {
1019
+ Util.logger.error(
1020
+ `- Could not create schedule for automation '${originalMetadataMap[oldKey].name}' to start it: ${ex.message}`
1021
+ );
1022
+ }
1023
+ if (schedule !== null) {
1024
+ try {
1025
+ // remove the fields that are not needed for the schedule but only for CLI output
1026
+ const schedule_StartDateTime = schedule._StartDateTime;
1027
+ delete schedule._StartDateTime;
1028
+ const schedule_interval = schedule._interval;
1029
+ delete schedule._interval;
1030
+ const schedule_timezoneString = schedule._timezoneString;
1031
+ delete schedule._timezoneString;
1032
+ // start the automation
1033
+ response = await this.client.soap.schedule(
1034
+ 'Automation',
1035
+ schedule,
1036
+ {
1037
+ Interaction: {
1038
+ ObjectID: metadataMap[key][this.definition.idField],
1039
+ },
1040
+ },
1041
+ 'start',
1042
+ {}
1043
+ );
1044
+ const intervalString =
1045
+ (schedule_interval > 1 ? `${schedule_interval} ` : '') +
1046
+ (schedule.RecurrenceType === 'Daily'
1047
+ ? 'Day'
1048
+ : schedule.RecurrenceType.slice(0, -2) +
1049
+ (schedule_interval > 1 ? 's' : ''));
1050
+ Util.logger.warn(
1051
+ ` - scheduled automation '${
1052
+ originalMetadataMap[oldKey].name
1053
+ }' deployed as Active: runs every ${intervalString} starting ${
1054
+ schedule_StartDateTime.split('T').join(' ').split('.')[0]
1055
+ } ${schedule_timezoneString}`
1056
+ );
1057
+ } catch {
1058
+ // API does not return anything usefull here. We have to know the rules instead
1059
+ Util.logger.error(
1060
+ ` ☇ error starting scheduled ${this.definition.type}${key}: Please check schedule settings`
1061
+ );
1062
+ }
1063
+ }
1064
+ } else {
1065
+ Util.logger.info(
1066
+ Util.getGrayMsg(
1067
+ ` - scheduled automation '${originalMetadataMap[oldKey].name}' deployed as Paused`
1068
+ )
483
1069
  );
484
1070
  }
485
1071
  }
1072
+ if (metadataMap[key].startSource) {
1073
+ metadataMap[key].schedule = metadataMap[key].startSource.schedule;
1074
+
1075
+ delete metadataMap[key].startSource;
1076
+ }
1077
+ if (metadataMap[key].schedule?.scheduleTypeId) {
1078
+ metadataMap[key].schedule.typeId = metadataMap[key].schedule.scheduleTypeId;
1079
+ delete metadataMap[key].schedule.scheduleTypeId;
1080
+ }
1081
+ return { key, response };
486
1082
  }
1083
+
487
1084
  /**
488
1085
  * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
489
1086
  *
@@ -549,118 +1146,6 @@ class Automation extends MetadataType {
549
1146
  }
550
1147
  }
551
1148
 
552
- /**
553
- * parses retrieved Metadata before saving
554
- *
555
- * @param {TYPE.AutomationItem} metadata a single automation definition
556
- * @returns {TYPE.AutomationItem | void} parsed item
557
- */
558
- static parseMetadata(metadata) {
559
- // folder
560
- this.setFolderPath(metadata);
561
- // automations are often skipped due to lack of support.
562
- try {
563
- if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
564
- // Starting Source == 'Schedule'
565
-
566
- try {
567
- if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
568
- // if we found the id in our list, remove the redundant data
569
- delete metadata.schedule.timezoneId;
570
- }
571
- } catch {
572
- Util.logger.debug(
573
- `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
574
- );
575
- }
576
- try {
577
- // type 'Running' is temporary status only, overwrite with Scheduled for storage.
578
- if (metadata.type === 'scheduled' && metadata.status === 'Running') {
579
- metadata.status = 'Scheduled';
580
- }
581
- } catch {
582
- Util.logger.error(
583
- `- ${this.definition.type} ${metadata.name} does not have a valid schedule setting.`
584
- );
585
- return;
586
- }
587
- } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
588
- // Starting Source == 'File Drop'
589
- // Do nothing for now
590
- }
591
- if (metadata.steps) {
592
- for (const step of metadata.steps) {
593
- const stepNumber = step.stepNumber || step.step;
594
- delete step.stepNumber;
595
- delete step.step;
596
-
597
- for (const activity of step.activities) {
598
- try {
599
- // get metadata type of activity
600
- activity.r__type = Util.inverseGet(
601
- this.definition.activityTypeMapping,
602
- activity.objectTypeId
603
- );
604
- delete activity.objectTypeId;
605
- } catch {
606
- Util.logger.warn(
607
- ` - Unknown activity type '${activity.objectTypeId}'` +
608
- ` in step ${stepNumber}.${activity.displayOrder}` +
609
- ` of Automation '${metadata.name}'`
610
- );
611
- continue;
612
- }
613
-
614
- // if no activityObjectId then either serialized activity
615
- // (config in Automation ) or unconfigured so no further action to be taken
616
- if (
617
- activity.activityObjectId === '00000000-0000-0000-0000-000000000000' ||
618
- activity.activityObjectId == null ||
619
- !this.definition.dependencies.includes(activity.r__type)
620
- ) {
621
- // empty if block
622
- }
623
- // / if managed by cache we can update references to support deployment
624
- else if (
625
- Definitions[activity.r__type]?.['idField'] &&
626
- cache.getCache(this.buObject.mid)[activity.r__type]
627
- ) {
628
- try {
629
- activity.activityObjectId = cache.searchForField(
630
- activity.r__type,
631
- activity.activityObjectId,
632
- Definitions[activity.r__type].idField,
633
- Definitions[activity.r__type].nameField
634
- );
635
- } catch (ex) {
636
- // getFromCache throws error where the dependent metadata is not found
637
- Util.logger.warn(
638
- ` - Missing ${activity.r__type} activity '${activity.name}'` +
639
- ` in step ${stepNumber}.${activity.displayOrder}` +
640
- ` of Automation '${metadata.name}' (${ex.message})`
641
- );
642
- }
643
- } else {
644
- Util.logger.warn(
645
- ` - Missing ${activity.r__type} activity '${activity.name}'` +
646
- ` in step ${stepNumber}.${activity.displayOrder}` +
647
- ` of Automation '${metadata.name}' (Not Found in Cache)`
648
- );
649
- }
650
- }
651
- }
652
- }
653
- return JSON.parse(JSON.stringify(metadata));
654
- } catch (ex) {
655
- Util.logger.warn(
656
- ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
657
- ex.message
658
- }`
659
- );
660
- return null;
661
- }
662
- }
663
-
664
1149
  /**
665
1150
  * Builds a schedule object to be used for scheduling an automation
666
1151
  * based on combination of ical string and start/end dates.
@@ -680,6 +1165,9 @@ class Automation extends MetadataType {
680
1165
  const a = obj.split('=');
681
1166
  recurHelper[a[0]] = a[1];
682
1167
  }
1168
+ if (recurHelper.INTERVAL) {
1169
+ recurHelper.INTERVAL = Number.parseInt(recurHelper.INTERVAL);
1170
+ }
683
1171
  // the ical schedule is all in caps but soap objects require Title Case.
684
1172
  const keyStem = recurHelper.FREQ.charAt(0) + recurHelper.FREQ.slice(1, -2).toLowerCase();
685
1173
 
@@ -708,13 +1196,18 @@ class Automation extends MetadataType {
708
1196
  'Scheduling automatically not supported for Weekly, Monthly and Yearly, please configure manually.'
709
1197
  );
710
1198
  }
1199
+ if (recurHelper.FREQ === 'MINUTELY' && recurHelper.INTERVAL && recurHelper.INTERVAL < 5) {
1200
+ throw new Error(
1201
+ 'The smallest interval you can configure is 5 minutes. Please adjust your schedule.'
1202
+ );
1203
+ }
711
1204
 
712
1205
  if (this.definition.timeZoneMapping[scheduleObject.timezoneName]) {
713
1206
  scheduleObject.timezoneId =
714
1207
  this.definition.timeZoneMapping[scheduleObject.timezoneName];
715
1208
  } else {
716
- Util.logger.error(
717
- `- Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
1209
+ throw new Error(
1210
+ `Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
718
1211
  );
719
1212
  }
720
1213
  schedule.TimeZone.ID = scheduleObject.timezoneId;
@@ -808,8 +1301,8 @@ class Automation extends MetadataType {
808
1301
  // create new Date object reflecting SFMC's servertime
809
1302
  const dateServer = new Date(utc + 3600000 * offsetServer);
810
1303
 
811
- // return time as a string without trailing "Z"
812
- return dateServer.toISOString().slice(0, -1);
1304
+ // return time as a string without trailing "Z" and without miliseconds (separated by .)
1305
+ return dateServer.toISOString().slice(0, -1).split('.')[0];
813
1306
  }
814
1307
  /**
815
1308
  * Experimental: Only working for DataExtensions:
@@ -836,7 +1329,7 @@ class Automation extends MetadataType {
836
1329
  const automationType = { scheduled: 'Schedule', triggered: 'File Drop' };
837
1330
  output += `**Started by:** ${automationType[json.type] || 'Not defined'}\n\n`;
838
1331
  output += `**Status:** ${json.status}\n\n`;
839
- if (json.type === 'scheduled') {
1332
+ if (json.type === 'scheduled' || json.schedule) {
840
1333
  const tz =
841
1334
  this.definition.timeZoneDifference[
842
1335
  this.definition.timeZoneMapping[json?.schedule?.timezoneName]
@@ -846,7 +1339,7 @@ class Automation extends MetadataType {
846
1339
  output += `**Schedule:**\n\n`;
847
1340
  output += `* Start: ${json.schedule.startDate.split('T').join(' ')} ${tz}\n`;
848
1341
  output += `* End: ${json.schedule.endDate.split('T').join(' ')} ${tz}\n`;
849
- output += `* Timezone: ${json.schedule.timezoneName}\n`;
1342
+ output += `* Timezone: ${json.schedule.timezoneName}\n`;
850
1343
 
851
1344
  const ical = {};
852
1345
  for (const item of json.schedule.icalRecur.split(';')) {
@@ -855,9 +1348,20 @@ class Automation extends MetadataType {
855
1348
  }
856
1349
  const frequency = ical.FREQ.slice(0, -2).toLowerCase();
857
1350
 
858
- output += `* Recurrance: every ${ical.INTERVAL > 1 ? ical.INTERVAL : ''} ${
859
- frequency === 'dai' ? 'day' : frequency
860
- }${ical.INTERVAL > 1 ? 's' : ''} ${ical.COUNT ? `for ${ical.COUNT} times` : ''}\n`;
1351
+ output += `* Recurrance: `;
1352
+ output +=
1353
+ ical.COUNT == 1
1354
+ ? 'run only once'
1355
+ : `every${ical.INTERVAL > 1 ? ' ' + ical.INTERVAL : ''} ${
1356
+ frequency === 'dai' ? 'day' : frequency
1357
+ }${ical.INTERVAL > 1 ? 's' : ''}${
1358
+ ical.COUNT
1359
+ ? ` for ${ical.COUNT} times`
1360
+ : ical.UNTIL
1361
+ ? ' until end date'
1362
+ : ''
1363
+ }`;
1364
+ output += '\n';
861
1365
  } else if (json.schedule) {
862
1366
  output += `**Schedule:** Not defined\n`;
863
1367
  }
@@ -868,6 +1372,27 @@ class Automation extends MetadataType {
868
1372
  output += `* Pattern: ${json.fileTrigger.fileNamingPattern}\n`;
869
1373
  output += `* Folder: ${json.fileTrigger.folderLocationText}\n`;
870
1374
  }
1375
+ // add empty line to ensure the following notifications are rendered properly
1376
+ output += '\n';
1377
+ if (json.notifications?.length) {
1378
+ output += `**Notifications:**\n\n`;
1379
+ // ensure notifications are sorted by type regardless of how the API returns it
1380
+ const notifications = {};
1381
+ for (const n of json.notifications) {
1382
+ notifications[n.type] =
1383
+ (Array.isArray(n.email) ? n.email.join(',') : n.email) +
1384
+ (n.message ? ` ("${n.message}")` : '');
1385
+ }
1386
+ if (notifications.Complete) {
1387
+ output += `* Complete: ${notifications.Complete}\n`;
1388
+ }
1389
+ if (notifications.Error) {
1390
+ output += `* Error: ${notifications.Error}\n`;
1391
+ }
1392
+ } else {
1393
+ output += `**Notifications:** _none_\n\n`;
1394
+ }
1395
+
871
1396
  // show table with automation steps
872
1397
  if (tabled && tabled.length) {
873
1398
  // add empty line to ensure the following table is rendered properly
@@ -975,11 +1500,10 @@ class Automation extends MetadataType {
975
1500
  // as part of retrieve & manual execution we could face an empty folder
976
1501
  return;
977
1502
  }
978
- await Promise.all(
979
- Object.keys(metadata).map((key) => {
980
- this._writeDoc(docPath + '/', key, metadata[key], 'md');
981
- return metadata[key];
982
- })
1503
+ return await Promise.all(
1504
+ Object.keys(metadata).map((key) =>
1505
+ this._writeDoc(docPath + '/', key, metadata[key], 'md')
1506
+ )
983
1507
  );
984
1508
  }
985
1509
  }
@@ -1017,7 +1541,7 @@ class Automation extends MetadataType {
1017
1541
  * @param {string} key customer key
1018
1542
  * @returns {Promise.<string>} objectId or enpty string
1019
1543
  */
1020
- static async _getObjectIdForSingleRetrieve(key) {
1544
+ static async #getObjectIdForSingleRetrieve(key) {
1021
1545
  const response = await this.client.soap.retrieve('Program', ['ObjectID'], {
1022
1546
  filter: {
1023
1547
  leftOperand: 'CustomerKey',
@@ -1036,7 +1560,7 @@ class Automation extends MetadataType {
1036
1560
  */
1037
1561
  static async deleteByKey(customerKey) {
1038
1562
  // the delete endpoint returns a general exception if the automation does not exist; handle it gracefully instead by adding a retrieve first
1039
- const objectId = customerKey ? await this._getObjectIdForSingleRetrieve(customerKey) : null;
1563
+ const objectId = customerKey ? await this.#getObjectIdForSingleRetrieve(customerKey) : null;
1040
1564
  if (!objectId) {
1041
1565
  Util.logger.error(` - automation not found`);
1042
1566
  return false;