mcdev 5.0.1 → 5.1.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 (138) hide show
  1. package/.coverage-comment-template.svelte +177 -161
  2. package/.eslintrc.json +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  4. package/.github/dependabot.yml +8 -0
  5. package/.github/workflows/coverage-base-update.yml +6 -2
  6. package/.github/workflows/coverage-develop-branch.yml +7 -6
  7. package/.github/workflows/coverage-main-branch.yml +7 -6
  8. package/.github/workflows/coverage.yml +7 -2
  9. package/.husky/commit-msg +1 -1
  10. package/.husky/post-checkout +35 -3
  11. package/.husky/post-merge +21 -0
  12. package/.husky/pre-commit +1 -0
  13. package/docs/dist/documentation.md +222 -62
  14. package/lib/Deployer.js +3 -4
  15. package/lib/cli.js +36 -8
  16. package/lib/index.js +175 -3
  17. package/lib/metadataTypes/Asset.js +4 -2
  18. package/lib/metadataTypes/Automation.js +413 -195
  19. package/lib/metadataTypes/DataExtension.js +6 -8
  20. package/lib/metadataTypes/DataExtensionField.js +5 -5
  21. package/lib/metadataTypes/Journey.js +11 -10
  22. package/lib/metadataTypes/MetadataType.js +76 -22
  23. package/lib/metadataTypes/MobileKeyword.js +165 -20
  24. package/lib/metadataTypes/MobileMessage.js +20 -28
  25. package/lib/metadataTypes/Query.js +26 -0
  26. package/lib/metadataTypes/Role.js +2 -3
  27. package/lib/metadataTypes/TransactionalSMS.js +5 -5
  28. package/lib/metadataTypes/User.js +3 -17
  29. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  30. package/lib/metadataTypes/definitions/Automation.definition.js +52 -6
  31. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -0
  32. package/lib/metadataTypes/definitions/DataExtract.definition.js +1 -0
  33. package/lib/metadataTypes/definitions/EmailSend.definition.js +1 -0
  34. package/lib/metadataTypes/definitions/Event.definition.js +1 -0
  35. package/lib/metadataTypes/definitions/Filter.definition.js +1 -0
  36. package/lib/metadataTypes/definitions/ImportFile.definition.js +1 -0
  37. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +20 -7
  38. package/lib/metadataTypes/definitions/MobileMessage.definition.js +50 -8
  39. package/lib/metadataTypes/definitions/Query.definition.js +1 -0
  40. package/lib/metadataTypes/definitions/Role.definition.js +1 -0
  41. package/lib/metadataTypes/definitions/TriggeredSend.definition.js +1 -0
  42. package/lib/metadataTypes/definitions/User.definition.js +2 -0
  43. package/lib/util/auth.js +4 -1
  44. package/lib/util/cli.js +1 -1
  45. package/lib/util/devops.js +13 -11
  46. package/lib/util/file.js +5 -3
  47. package/lib/util/util.js +153 -129
  48. package/package.json +11 -11
  49. package/test/general.test.js +26 -0
  50. package/test/mockRoot/.mcdevrc.json +3 -1
  51. package/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json +53 -0
  52. package/test/mockRoot/deploy/testInstance/testBU/automation/testNew_automation.automation-meta.json +46 -0
  53. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword.mobileKeyword-meta.json → 4912312345678.TESTNEW_KEYWORD.mobileKeyword-meta.json} +1 -4
  54. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword_blocked.mobileKeyword-meta.json → 4912312345678.TESTNEW_KEYWORD_BLOCKED.mobileKeyword-meta.json} +1 -4
  55. package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.json → testExisting_query.query-meta.json} +2 -2
  56. package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.json → testNew_query.query-meta.json} +2 -2
  57. package/test/mockRoot/deploy/testInstance/testBU/transactionalSMS/testExisting_tsms.transactionalSMS-meta.json +1 -1
  58. package/test/mockRoot/deploy/testInstance/testBU/transactionalSMS/testNew_tsms.transactionalSMS-meta.json +1 -1
  59. package/test/resourceFactory.js +64 -21
  60. package/test/resources/1111111/user/retrieve-expected.md +19 -0
  61. package/test/resources/9999999/automation/build-expected.json +58 -0
  62. package/test/resources/9999999/automation/create-expected.json +46 -0
  63. package/test/resources/9999999/automation/create-testNew_automation-expected.md +28 -0
  64. package/test/resources/9999999/automation/delete-response.xml +40 -0
  65. package/test/resources/9999999/automation/retrieve-expected.json +58 -0
  66. package/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md +30 -0
  67. package/test/resources/9999999/automation/template-expected.json +58 -0
  68. package/test/resources/9999999/automation/update-expected.json +46 -0
  69. package/test/resources/9999999/automation/update-testExisting_automation-expected.md +28 -0
  70. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  71. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json +85 -0
  72. package/test/resources/9999999/automation/v1/automations/a8afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  73. package/test/resources/9999999/automation/v1/automations/post-response.json +85 -0
  74. package/test/resources/9999999/automation/v1/dataextracts/56c5370a-f988-4f36-b0ee-0f876573f6d7/get-response.json +38 -0
  75. package/test/resources/9999999/automation/v1/dataextracts/get-response.json +20 -0
  76. package/test/resources/9999999/automation/v1/filetransfers/72c328ac-f5b0-4e37-91d3-a775666f15a6/get-response.json +18 -0
  77. package/test/resources/9999999/automation/v1/filetransfers/get-response.json +15 -0
  78. package/test/resources/9999999/automation/v1/imports/get-response.json +38 -0
  79. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/actions/start/post-response.txt +1 -0
  80. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/get-response.json +2 -2
  81. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +2 -2
  82. package/test/resources/9999999/automation/v1/queries/get-response.json +4 -4
  83. package/test/resources/9999999/automation/v1/queries/post-response.json +2 -2
  84. package/test/resources/9999999/automation/v1/scripts/get-response.json +17 -0
  85. package/test/resources/9999999/dataExtension/retrieve-expected.md +18 -0
  86. package/test/resources/9999999/dataFolder/retrieve-ContentType=automations-response.xml +48 -0
  87. package/test/resources/9999999/dataFolder/retrieve-ContentType=queryactivity-response.xml +48 -0
  88. package/test/resources/9999999/dataFolder/retrieve-response.xml +22 -0
  89. package/test/resources/9999999/emailSendDefinition/retrieve-response.xml +85 -0
  90. package/test/resources/9999999/journey/build-expected.json +1 -1
  91. package/test/resources/9999999/journey/get-expected.json +1 -1
  92. package/test/resources/9999999/journey/put-expected.json +1 -1
  93. package/test/resources/9999999/journey/template-expected.json +1 -1
  94. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json +21 -0
  95. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/post-response.json +0 -0
  96. package/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json +30 -0
  97. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/NXV4ZFMwTEFwRVczd3RaLUF5X3p5dzo4Njow/get-response.json +1 -1
  98. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/get-response.json +1 -1
  99. package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/get-response.json +1 -1
  100. package/test/resources/9999999/legacy/v1/beta/mobile/message/get-response.json +1 -1
  101. package/test/resources/9999999/messaging/v1/sms/definitions/post-response.json +1 -1
  102. package/test/resources/9999999/messaging/v1/sms/definitions/testExisting_tsms/get-response.json +1 -1
  103. package/test/resources/9999999/messaging/v1/sms/definitions/testExisting_tsms/patch-response.json +1 -1
  104. package/test/resources/9999999/mobileKeyword/build-expected.json +1 -4
  105. package/test/resources/9999999/mobileKeyword/get-expected.json +1 -4
  106. package/test/resources/9999999/mobileKeyword/post-create-expected.json +1 -4
  107. package/test/resources/9999999/mobileKeyword/template-expected.json +1 -4
  108. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation-response.xml +30 -0
  109. package/test/resources/9999999/program/retrieve-CustomerKey=testNew_automation-response.xml +30 -0
  110. package/test/resources/9999999/program/retrieve-Name=testExisting_automation-response.xml +31 -0
  111. package/test/resources/9999999/program/retrieve-response.xml +32 -0
  112. package/test/resources/9999999/query/build-expected.json +2 -2
  113. package/test/resources/9999999/query/build-expected.sql +1 -1
  114. package/test/resources/9999999/query/get-expected.json +2 -2
  115. package/test/resources/9999999/query/get-expected.sql +1 -1
  116. package/test/resources/9999999/query/get2-expected.json +2 -2
  117. package/test/resources/9999999/query/patch-expected.json +2 -2
  118. package/test/resources/9999999/query/patch-expected.sql +1 -1
  119. package/test/resources/9999999/query/post-expected.json +2 -2
  120. package/test/resources/9999999/query/post-expected.sql +1 -1
  121. package/test/resources/9999999/query/template-expected.json +2 -2
  122. package/test/resources/9999999/query/template-expected.sql +1 -1
  123. package/test/resources/9999999/transactionalSMS/build-expected.json +1 -1
  124. package/test/resources/9999999/transactionalSMS/get-expected.json +1 -1
  125. package/test/resources/9999999/transactionalSMS/patch-expected.json +1 -1
  126. package/test/resources/9999999/transactionalSMS/post-expected.json +1 -1
  127. package/test/resources/9999999/transactionalSMS/template-expected.json +1 -1
  128. package/test/type.automation.test.js +259 -0
  129. package/test/type.dataExtension.test.js +16 -1
  130. package/test/type.mobileKeyword.test.js +57 -19
  131. package/test/type.query.test.js +39 -26
  132. package/test/type.user.test.js +44 -1
  133. package/test/utils.js +16 -5
  134. package/.coverage-comment-template.md +0 -20
  135. /package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword.mobileKeyword-meta.amp → 4912312345678.TESTNEW_KEYWORD.mobileKeyword-meta.amp} +0 -0
  136. /package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword_blocked.mobileKeyword-meta.amp → 4912312345678.TESTNEW_KEYWORD_BLOCKED.mobileKeyword-meta.amp} +0 -0
  137. /package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.sql → testExisting_query.query-meta.sql} +0 -0
  138. /package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.sql → testNew_query.query-meta.sql} +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,11 +37,13 @@ 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
  }
@@ -70,19 +74,96 @@ class Automation extends MetadataType {
70
74
  })
71
75
  )
72
76
  : [];
73
- const parsed = this.parseResponseBody({ items: details });
77
+ let metadataMap = this.parseResponseBody({ items: details });
78
+
79
+ if (Object.keys(metadataMap).length) {
80
+ // attach notification information to each automation that has any
81
+ await this.#getAutomationNotificationsREST(metadataMap);
82
+ }
83
+
84
+ // * retrieveDir can be empty when we use it in the context of postDeployTasks
85
+ if (retrieveDir) {
86
+ metadataMap = await this.saveResults(metadataMap, retrieveDir, null, null);
87
+ Util.logger.info(
88
+ `Downloaded: ${this.definition.type} (${Object.keys(metadataMap).length})` +
89
+ Util.getKeysString(key)
90
+ );
91
+
92
+ await this.runDocumentOnRetrieve(key, metadataMap);
93
+ }
94
+ return { metadata: metadataMap, type: this.definition.type };
95
+ }
96
+
97
+ /**
98
+ * helper for {@link Automation.retrieve} to get Automation Notifications
99
+ *
100
+ * @private
101
+ * @param {TYPE.MetadataTypeMap} metadataMap keyField => metadata map
102
+ * @returns {Promise.<void>} Promise of nothing
103
+ */
104
+ static async #getAutomationNotificationsREST(metadataMap) {
105
+ Util.logger.info(Util.getGrayMsg(` Retrieving Automation Notification information...`));
106
+
107
+ // get list of keys that we retrieved so far
108
+ const foundKeys = Object.keys(metadataMap);
74
109
 
75
- // * retrieveDir is mandatory in this method as it is not used for caching (there is a seperate method for that)
76
- const savedMetadata = await this.saveResults(parsed, retrieveDir, null, null);
110
+ // get encodedAutomationID to retrieve notification information
111
+ const iteratorBackup = this.definition.bodyIteratorField;
112
+ this.definition.bodyIteratorField = 'entry';
113
+ const automationLegacyMapObj = await super.retrieveREST(
114
+ undefined,
115
+ `/legacy/v1/beta/bulk/automations/automation/definition/`
116
+ );
117
+ this.definition.bodyIteratorField = iteratorBackup;
118
+ const automationLegacyMap = Object.keys(automationLegacyMapObj.metadata)
119
+ .filter((key) => foundKeys.includes(key))
120
+ // ! using the `id` field to retrieve notifications does not work. instead one needs to use the URL in the `notifications` field
121
+ .map((key) => ({
122
+ id: automationLegacyMapObj.metadata[key].id,
123
+ key,
124
+ }));
125
+
126
+ // get notifications for each automation
127
+ const rateLimit = pLimit(5);
128
+ let found = 0;
129
+ let skipped = 0;
130
+ const promiseMap = await Promise.all(
131
+ automationLegacyMap.map((automationLegacy) =>
132
+ rateLimit(async () => {
133
+ // this is a file so extended is at another endpoint
134
+ try {
135
+ const notificationsResult = await this.client.rest.get(
136
+ '/legacy/v1/beta/automations/notifications/' + automationLegacy.id
137
+ );
138
+ if (Array.isArray(notificationsResult?.workers)) {
139
+ metadataMap[automationLegacy.key].notifications =
140
+ notificationsResult.workers.map((n) => ({
141
+ email: n.definition.split(',').map((item) => item.trim()),
142
+ message: n.body,
143
+ type: n.notificationType,
144
+ }));
145
+ found++;
146
+ } else {
147
+ throw new TypeError(JSON.stringify(notificationsResult));
148
+ }
149
+ } catch (ex) {
150
+ Util.logger.debug(
151
+ ` ☇ skipping Notifications for Automation ${automationLegacy.key}: ${ex.message} ${ex.code}`
152
+ );
153
+ skipped++;
154
+ }
155
+ })
156
+ )
157
+ );
77
158
  Util.logger.info(
78
- `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
79
- Util.getKeysString(key)
159
+ Util.getGrayMsg(` Notifications found for ${found} automation${found === 1 ? '' : 's'}`)
80
160
  );
81
- if (this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
82
- await this.document(savedMetadata);
83
- }
84
- return { metadata: savedMetadata, type: this.definition.type };
161
+ Util.logger.debug(
162
+ `Notifications not found for ${skipped} automation${skipped === 1 ? '' : 's'}`
163
+ );
164
+ return promiseMap;
85
165
  }
166
+
86
167
  /**
87
168
  * Retrieves Metadata of Automation
88
169
  *
@@ -134,6 +215,7 @@ class Automation extends MetadataType {
134
215
  * @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
135
216
  */
136
217
  static async retrieveForCache() {
218
+ // get automations for cache
137
219
  const results = await this.client.soap.retrieveBulk('Program', [
138
220
  'ObjectID',
139
221
  'CustomerKey',
@@ -141,14 +223,29 @@ class Automation extends MetadataType {
141
223
  ]);
142
224
  const resultsConverted = {};
143
225
  if (Array.isArray(results?.Results)) {
226
+ // get encodedAutomationID to retrieve notification information
227
+ const keyBackup = this.definition.keyField;
228
+ const iteratorBackup = this.definition.bodyIteratorField;
229
+ this.definition.keyField = 'key';
230
+ this.definition.bodyIteratorField = 'entry';
231
+ const automationsLegacy = await super.retrieveREST(
232
+ undefined,
233
+ `/legacy/v1/beta/bulk/automations/automation/definition/`
234
+ );
235
+ this.definition.keyField = keyBackup;
236
+ this.definition.bodyIteratorField = iteratorBackup;
237
+
238
+ // merge encodedAutomationID into results
144
239
  for (const m of results.Results) {
145
240
  resultsConverted[m.CustomerKey] = {
146
241
  id: m.ObjectID,
147
242
  key: m.CustomerKey,
148
243
  name: m.Name,
244
+ programId: automationsLegacy.metadata[m.CustomerKey]?.id,
149
245
  };
150
246
  }
151
247
  }
248
+
152
249
  return { metadata: resultsConverted, type: this.definition.type };
153
250
  }
154
251
 
@@ -175,14 +272,21 @@ class Automation extends MetadataType {
175
272
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
176
273
  return;
177
274
  }
178
- const details = await this.client.rest.get(
275
+ let details = await this.client.rest.get(
179
276
  '/automation/v1/automations/' + metadata.ObjectID
180
277
  );
278
+ const metadataMap = this.parseResponseBody({ items: [details] });
279
+ if (Object.keys(metadataMap).length) {
280
+ // attach notification information to each automation that has any
281
+ await this.#getAutomationNotificationsREST(metadataMap);
282
+ details = Object.values(metadataMap)[0];
283
+ }
284
+
181
285
  let val = null;
182
286
  let originalKey;
183
287
  // if parsing fails, we should just save what we get
184
288
  try {
185
- const parsedDetails = this.parseMetadata(details);
289
+ const parsedDetails = this.postRetrieveTasks(details);
186
290
  originalKey = parsedDetails[this.definition.keyField];
187
291
  if (parsedDetails !== null) {
188
292
  val = JSON.parse(
@@ -218,10 +322,128 @@ class Automation extends MetadataType {
218
322
  * manages post retrieve steps
219
323
  *
220
324
  * @param {TYPE.AutomationItem} metadata a single automation
221
- * @returns {TYPE.AutomationItem} metadata
325
+ * @returns {TYPE.AutomationItem | void} parsed item
222
326
  */
223
327
  static postRetrieveTasks(metadata) {
224
- return this.parseMetadata(metadata);
328
+ // folder
329
+ this.setFolderPath(metadata);
330
+ // automations are often skipped due to lack of support.
331
+ try {
332
+ if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
333
+ // Starting Source == 'Schedule'
334
+
335
+ try {
336
+ if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
337
+ // if we found the id in our list, remove the redundant data
338
+ delete metadata.schedule.timezoneId;
339
+ }
340
+ } catch {
341
+ Util.logger.debug(
342
+ `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
343
+ );
344
+ }
345
+ try {
346
+ // type 'Running' is temporary status only, overwrite with Scheduled for storage.
347
+ if (metadata.type === 'scheduled' && metadata.status === 'Running') {
348
+ metadata.status = 'Scheduled';
349
+ }
350
+ } catch {
351
+ Util.logger.error(
352
+ `- ${this.definition.type} ${metadata.name} does not have a valid schedule setting.`
353
+ );
354
+ return;
355
+ }
356
+ } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
357
+ // Starting Source == 'File Drop'
358
+ // Do nothing for now
359
+ }
360
+ if (metadata.steps) {
361
+ for (const step of metadata.steps) {
362
+ const stepNumber = step.stepNumber || step.step;
363
+ delete step.stepNumber;
364
+ delete step.step;
365
+
366
+ for (const activity of step.activities) {
367
+ try {
368
+ // get metadata type of activity
369
+ activity.r__type = Util.inverseGet(
370
+ this.definition.activityTypeMapping,
371
+ activity.objectTypeId
372
+ );
373
+ delete activity.objectTypeId;
374
+ } catch {
375
+ Util.logger.warn(
376
+ ` - Unknown activity type '${activity.objectTypeId}'` +
377
+ ` in step ${stepNumber}.${activity.displayOrder}` +
378
+ ` of Automation '${metadata.name}'`
379
+ );
380
+ continue;
381
+ }
382
+
383
+ // if no activityObjectId then either serialized activity
384
+ // (config in Automation ) or unconfigured so no further action to be taken
385
+ if (
386
+ activity.activityObjectId === '00000000-0000-0000-0000-000000000000' ||
387
+ activity.activityObjectId == null
388
+ ) {
389
+ Util.logger.debug(
390
+ ` - skip parsing of activity due to missing activityObjectId: ${JSON.stringify(
391
+ activity
392
+ )}`
393
+ );
394
+ // empty if block
395
+ } else if (!this.definition.dependencies.includes(activity.r__type)) {
396
+ Util.logger.debug(
397
+ ` - skip parsing because the type is not set up as a dependecy for ${this.definition.type}`
398
+ );
399
+ }
400
+ // / if managed by cache we can update references to support deployment
401
+ else if (
402
+ Definitions[activity.r__type]?.['idField'] &&
403
+ cache.getCache(this.buObject.mid)[activity.r__type]
404
+ ) {
405
+ try {
406
+ // 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
407
+ const name = cache.searchForField(
408
+ activity.r__type,
409
+ activity.activityObjectId,
410
+ Definitions[activity.r__type].idField,
411
+ Definitions[activity.r__type].nameField
412
+ );
413
+ if (name !== activity.name) {
414
+ Util.logger.debug(
415
+ ` - updated name of step ${stepNumber}.${activity.displayOrder}` +
416
+ ` in Automation '${metadata.name}' from ${activity.name} to ${name}`
417
+ );
418
+ activity.name = name;
419
+ }
420
+ } catch (ex) {
421
+ // getFromCache throws error where the dependent metadata is not found
422
+ Util.logger.warn(
423
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
424
+ ` in step ${stepNumber}.${activity.displayOrder}` +
425
+ ` of Automation '${metadata.name}' (${ex.message})`
426
+ );
427
+ }
428
+ } else {
429
+ Util.logger.warn(
430
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
431
+ ` in step ${stepNumber}.${activity.displayOrder}` +
432
+ ` of Automation '${metadata.name}' (Not Found in Cache)`
433
+ );
434
+ }
435
+ }
436
+ }
437
+ }
438
+ return JSON.parse(JSON.stringify(metadata));
439
+ } catch (ex) {
440
+ Util.logger.warn(
441
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
442
+ ex.message
443
+ }`
444
+ );
445
+ return null;
446
+ }
225
447
  }
226
448
 
227
449
  /**
@@ -235,7 +457,15 @@ class Automation extends MetadataType {
235
457
  */
236
458
  static async deploy(metadata, targetBU, retrieveDir, isRefresh) {
237
459
  const upsertResults = await this.upsert(metadata, targetBU, isRefresh);
238
- await this.saveResults(upsertResults, retrieveDir, null);
460
+ const savedMetadata = await this.saveResults(upsertResults, retrieveDir, null);
461
+ if (
462
+ this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
463
+ !this.definition.documentInOneFile
464
+ ) {
465
+ const count = Object.keys(savedMetadata).length;
466
+ Util.logger.debug(` - Running document for ${count} record${count === 1 ? '' : 's'}`);
467
+ await this.document(savedMetadata);
468
+ }
239
469
  return upsertResults;
240
470
  }
241
471
 
@@ -270,6 +500,15 @@ class Automation extends MetadataType {
270
500
  * @returns {Promise.<TYPE.AutomationItem>} Promise
271
501
  */
272
502
  static async preDeployTasks(metadata) {
503
+ if (metadata.notifications) {
504
+ this.notificationUpdates[metadata.key] = metadata.notifications;
505
+ } else {
506
+ const cached = cache.getByKey(metadata.key);
507
+ if (cached?.notifications) {
508
+ // if notifications existed but are no longer present in the deployment package, we need to run an empty update call to remove them
509
+ this.notificationUpdates[metadata.key] = [];
510
+ }
511
+ }
273
512
  if (this.validateDeployMetadata(metadata)) {
274
513
  // folder
275
514
  this.setFolderId(metadata);
@@ -397,82 +636,153 @@ class Automation extends MetadataType {
397
636
  /**
398
637
  * Gets executed after deployment of metadata type
399
638
  *
400
- * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
401
- * @param {TYPE.AutomationMap} originalMetadata metadata to be updated (contains additioanl fields)
639
+ * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
640
+ * @param {TYPE.AutomationMap} originalMetadataMap metadata to be updated (contains additioanl fields)
402
641
  * @returns {Promise.<void>} -
403
642
  */
404
- static async postDeployTasks(metadata, originalMetadata) {
405
- for (const key in metadata) {
643
+ static async postDeployTasks(metadataMap, originalMetadataMap) {
644
+ for (const key in metadataMap) {
406
645
  // need to put schedule on here if status is scheduled
646
+ await Automation.#scheduleAutomation(metadataMap, originalMetadataMap, key);
407
647
 
408
- if (originalMetadata[key]?.type === 'scheduled') {
409
- // Starting Source == 'Schedule': Try starting the automation
410
- if (originalMetadata[key].status === 'Scheduled') {
411
- let schedule = null;
648
+ // need to update notifications separately if there are any
649
+ await Automation.#updateNotificationInfoREST(metadataMap, key);
650
+
651
+ // rewrite upsert to retrieve fields
652
+ const metadata = metadataMap[key];
653
+ if (metadata.steps) {
654
+ for (const step of metadata.steps) {
655
+ step.name = step.annotation;
656
+ delete step.annotation;
657
+ }
658
+ }
659
+ }
660
+ }
661
+ /**
662
+ * helper for {@link Automation.postDeployTasks}
663
+ *
664
+ * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
665
+ * @param {string} key current customer key
666
+ * @returns {Promise.<void>} -
667
+ */
668
+ static async #updateNotificationInfoREST(metadataMap, key) {
669
+ if (this.notificationUpdates[key]) {
670
+ // create & update automation calls return programId as 'legacyId'; retrieve does not return it
671
+ const programId = metadataMap[key]?.legacyId;
672
+ if (programId) {
673
+ const notificationBody = {
674
+ programId,
675
+ workers: this.notificationUpdates[key].map((notification) => ({
676
+ programId,
677
+ notificationType: notification.type,
678
+ definition: Array.isArray(notification.email)
679
+ ? notification.email.join(',')
680
+ : notification.email,
681
+ body: notification.message,
682
+ channelType: 'Account',
683
+ })),
684
+ };
685
+ try {
686
+ const result = await this.client.rest.post(
687
+ '/legacy/v1/beta/automations/notifications/' + programId,
688
+ notificationBody
689
+ );
690
+ if (result) {
691
+ // should be empty if all OK
692
+ throw new Error(result);
693
+ }
694
+ } catch (ex) {
695
+ Util.logger.error(
696
+ `Error updating notifications for automation '${metadataMap[key].name}': ${ex.message} (${ex.code}))`
697
+ );
698
+ }
699
+ Util.logger.info(
700
+ Util.getGrayMsg(
701
+ ` - updated notifications for automation '${metadataMap[key].name}'`
702
+ )
703
+ );
704
+ }
705
+ }
706
+ }
707
+
708
+ /**
709
+ * helper for {@link postDeployTasks}
710
+ *
711
+ * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
712
+ * @param {TYPE.AutomationMap} originalMetadataMap metadata to be updated (contains additioanl fields)
713
+ * @param {string} key current customer key
714
+ */
715
+ static async #scheduleAutomation(metadataMap, originalMetadataMap, key) {
716
+ if (originalMetadataMap[key]?.type === 'scheduled') {
717
+ // Starting Source == 'Schedule': Try starting the automation
718
+ if (originalMetadataMap[key].status === 'Scheduled') {
719
+ let schedule = null;
720
+ try {
721
+ schedule = this._buildSchedule(originalMetadataMap[key].schedule);
722
+ } catch (ex) {
723
+ Util.logger.error(
724
+ `- Could not create schedule for automation '${originalMetadataMap[key].name}' to start it: ${ex.message}`
725
+ );
726
+ }
727
+ if (schedule !== null) {
412
728
  try {
413
- schedule = this._buildSchedule(originalMetadata[key].schedule);
729
+ // remove the fields that are not needed for the schedule but only for CLI output
730
+ const schedule_StartDateTime = schedule._StartDateTime;
731
+ delete schedule._StartDateTime;
732
+ const schedule_interval = schedule._interval;
733
+ delete schedule._interval;
734
+ const schedule_timezoneString = schedule._timezoneString;
735
+ delete schedule._timezoneString;
736
+ // start the automation
737
+ await this.client.soap.schedule(
738
+ 'Automation',
739
+ schedule,
740
+ {
741
+ Interaction: {
742
+ ObjectID: metadataMap[key].id,
743
+ },
744
+ },
745
+ 'start',
746
+ {}
747
+ );
748
+ const intervalString =
749
+ (schedule_interval > 1 ? `${schedule_interval} ` : '') +
750
+ (schedule.RecurrenceType === 'Daily'
751
+ ? 'Day'
752
+ : schedule.RecurrenceType.slice(0, -2) +
753
+ (schedule_interval > 1 ? 's' : ''));
754
+ Util.logger.warn(
755
+ ` - scheduled automation '${
756
+ originalMetadataMap[key].name
757
+ }' deployed as Active: runs every ${intervalString} starting ${
758
+ schedule_StartDateTime.split('T').join(' ').split('.')[0]
759
+ } ${schedule_timezoneString}`
760
+ );
414
761
  } catch (ex) {
415
762
  Util.logger.error(
416
- `- Could not create schedule for automation ${originalMetadata[key].name} to start it: ${ex.message}`
763
+ `- Could not start scheduled automation '${originalMetadataMap[key].name}': ${ex.message}`
417
764
  );
418
765
  }
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: metadata[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
- originalMetadata[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 '${originalMetadata[key].name}': ${ex.message}`
456
- );
457
- }
458
- }
459
- } else {
460
- Util.logger.warn(
461
- ` - scheduled automation '${originalMetadata[key].name}' deployed Paused`
462
- );
463
766
  }
767
+ } else {
768
+ Util.logger.info(
769
+ Util.getGrayMsg(
770
+ ` - scheduled automation '${originalMetadataMap[key].name}' deployed as Paused`
771
+ )
772
+ );
464
773
  }
465
- if (metadata[key].startSource) {
466
- metadata[key].schedule = metadata[key].startSource.schedule;
774
+ }
775
+ if (metadataMap[key].startSource) {
776
+ metadataMap[key].schedule = metadataMap[key].startSource.schedule;
467
777
 
468
- delete metadata[key].startSource;
469
- }
470
- if (metadata[key].schedule) {
471
- metadata[key].schedule.typeId = metadata[key].schedule.scheduleTypeId;
472
- delete metadata[key].schedule.scheduleTypeId;
473
- }
778
+ delete metadataMap[key].startSource;
779
+ }
780
+ if (metadataMap[key].schedule?.scheduleTypeId) {
781
+ metadataMap[key].schedule.typeId = metadataMap[key].schedule.scheduleTypeId;
782
+ delete metadataMap[key].schedule.scheduleTypeId;
474
783
  }
475
784
  }
785
+
476
786
  /**
477
787
  * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
478
788
  *
@@ -538,118 +848,6 @@ class Automation extends MetadataType {
538
848
  }
539
849
  }
540
850
 
541
- /**
542
- * parses retrieved Metadata before saving
543
- *
544
- * @param {TYPE.AutomationItem} metadata a single automation definition
545
- * @returns {TYPE.AutomationItem | void} parsed item
546
- */
547
- static parseMetadata(metadata) {
548
- // folder
549
- this.setFolderPath(metadata);
550
- // automations are often skipped due to lack of support.
551
- try {
552
- if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
553
- // Starting Source == 'Schedule'
554
-
555
- try {
556
- if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
557
- // if we found the id in our list, remove the redundant data
558
- delete metadata.schedule.timezoneId;
559
- }
560
- } catch {
561
- Util.logger.debug(
562
- `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
563
- );
564
- }
565
- try {
566
- // type 'Running' is temporary status only, overwrite with Scheduled for storage.
567
- if (metadata.type === 'scheduled' && metadata.status === 'Running') {
568
- metadata.status = 'Scheduled';
569
- }
570
- } catch {
571
- Util.logger.error(
572
- `- ${this.definition.type} ${metadata.name} does not have a valid schedule setting.`
573
- );
574
- return;
575
- }
576
- } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
577
- // Starting Source == 'File Drop'
578
- // Do nothing for now
579
- }
580
- if (metadata.steps) {
581
- for (const step of metadata.steps) {
582
- const stepNumber = step.stepNumber || step.step;
583
- delete step.stepNumber;
584
- delete step.step;
585
-
586
- for (const activity of step.activities) {
587
- try {
588
- // get metadata type of activity
589
- activity.r__type = Util.inverseGet(
590
- this.definition.activityTypeMapping,
591
- activity.objectTypeId
592
- );
593
- delete activity.objectTypeId;
594
- } catch {
595
- Util.logger.warn(
596
- ` - Unknown activity type '${activity.objectTypeId}'` +
597
- ` in step ${stepNumber}.${activity.displayOrder}` +
598
- ` of Automation '${metadata.name}'`
599
- );
600
- continue;
601
- }
602
-
603
- // if no activityObjectId then either serialized activity
604
- // (config in Automation ) or unconfigured so no further action to be taken
605
- if (
606
- activity.activityObjectId === '00000000-0000-0000-0000-000000000000' ||
607
- activity.activityObjectId == null ||
608
- !this.definition.dependencies.includes(activity.r__type)
609
- ) {
610
- // empty if block
611
- }
612
- // / if managed by cache we can update references to support deployment
613
- else if (
614
- Definitions[activity.r__type]?.['idField'] &&
615
- cache.getCache(this.buObject.mid)[activity.r__type]
616
- ) {
617
- try {
618
- activity.activityObjectId = cache.searchForField(
619
- activity.r__type,
620
- activity.activityObjectId,
621
- Definitions[activity.r__type].idField,
622
- Definitions[activity.r__type].nameField
623
- );
624
- } catch (ex) {
625
- // getFromCache throws error where the dependent metadata is not found
626
- Util.logger.warn(
627
- ` - Missing ${activity.r__type} activity '${activity.name}'` +
628
- ` in step ${stepNumber}.${activity.displayOrder}` +
629
- ` of Automation '${metadata.name}' (${ex.message})`
630
- );
631
- }
632
- } else {
633
- Util.logger.warn(
634
- ` - Missing ${activity.r__type} activity '${activity.name}'` +
635
- ` in step ${stepNumber}.${activity.displayOrder}` +
636
- ` of Automation '${metadata.name}' (Not Found in Cache)`
637
- );
638
- }
639
- }
640
- }
641
- }
642
- return JSON.parse(JSON.stringify(metadata));
643
- } catch (ex) {
644
- Util.logger.warn(
645
- ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
646
- ex.message
647
- }`
648
- );
649
- return null;
650
- }
651
- }
652
-
653
851
  /**
654
852
  * Builds a schedule object to be used for scheduling an automation
655
853
  * based on combination of ical string and start/end dates.
@@ -857,6 +1055,27 @@ class Automation extends MetadataType {
857
1055
  output += `* Pattern: ${json.fileTrigger.fileNamingPattern}\n`;
858
1056
  output += `* Folder: ${json.fileTrigger.folderLocationText}\n`;
859
1057
  }
1058
+ // add empty line to ensure the following notifications are rendered properly
1059
+ output += '\n';
1060
+ if (json.notifications?.length) {
1061
+ output += `**Notifications:**\n\n`;
1062
+ // ensure notifications are sorted by type regardless of how the API returns it
1063
+ const notifications = {};
1064
+ for (const n of json.notifications) {
1065
+ notifications[n.type] =
1066
+ (Array.isArray(n.email) ? n.email.join(',') : n.email) +
1067
+ (n.message ? ` ("${n.message}")` : '');
1068
+ }
1069
+ if (notifications.Complete) {
1070
+ output += `* Complete: ${notifications.Complete}\n`;
1071
+ }
1072
+ if (notifications.Error) {
1073
+ output += `* Error: ${notifications.Error}\n`;
1074
+ }
1075
+ } else {
1076
+ output += `**Notifications:** _none_\n\n`;
1077
+ }
1078
+
860
1079
  // show table with automation steps
861
1080
  if (tabled && tabled.length) {
862
1081
  // add empty line to ensure the following table is rendered properly
@@ -964,11 +1183,10 @@ class Automation extends MetadataType {
964
1183
  // as part of retrieve & manual execution we could face an empty folder
965
1184
  return;
966
1185
  }
967
- await Promise.all(
968
- Object.keys(metadata).map((key) => {
969
- this._writeDoc(docPath + '/', key, metadata[key], 'md');
970
- return metadata[key];
971
- })
1186
+ return await Promise.all(
1187
+ Object.keys(metadata).map((key) =>
1188
+ this._writeDoc(docPath + '/', key, metadata[key], 'md')
1189
+ )
972
1190
  );
973
1191
  }
974
1192
  }