mcdev 5.0.2 → 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 (88) hide show
  1. package/.coverage-comment-template.svelte +177 -161
  2. package/.github/ISSUE_TEMPLATE/bug.yml +1 -0
  3. package/.github/dependabot.yml +8 -0
  4. package/.github/workflows/coverage-base-update.yml +6 -2
  5. package/.github/workflows/coverage-develop-branch.yml +7 -6
  6. package/.github/workflows/coverage-main-branch.yml +7 -6
  7. package/.github/workflows/coverage.yml +7 -2
  8. package/.husky/post-checkout +3 -2
  9. package/docs/dist/documentation.md +162 -47
  10. package/lib/Deployer.js +3 -3
  11. package/lib/cli.js +28 -0
  12. package/lib/index.js +173 -2
  13. package/lib/metadataTypes/Automation.js +400 -193
  14. package/lib/metadataTypes/DataExtension.js +5 -5
  15. package/lib/metadataTypes/MetadataType.js +42 -15
  16. package/lib/metadataTypes/Query.js +26 -0
  17. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  18. package/lib/metadataTypes/definitions/Automation.definition.js +52 -6
  19. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -0
  20. package/lib/metadataTypes/definitions/DataExtract.definition.js +1 -0
  21. package/lib/metadataTypes/definitions/EmailSend.definition.js +1 -0
  22. package/lib/metadataTypes/definitions/Event.definition.js +1 -0
  23. package/lib/metadataTypes/definitions/Filter.definition.js +1 -0
  24. package/lib/metadataTypes/definitions/ImportFile.definition.js +1 -0
  25. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +1 -0
  26. package/lib/metadataTypes/definitions/Query.definition.js +1 -0
  27. package/lib/metadataTypes/definitions/Role.definition.js +1 -0
  28. package/lib/metadataTypes/definitions/TriggeredSend.definition.js +1 -0
  29. package/lib/metadataTypes/definitions/User.definition.js +1 -0
  30. package/lib/util/devops.js +13 -11
  31. package/lib/util/util.js +152 -129
  32. package/package.json +5 -5
  33. package/test/general.test.js +26 -0
  34. package/test/mockRoot/.mcdevrc.json +1 -1
  35. package/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json +53 -0
  36. package/test/mockRoot/deploy/testInstance/testBU/automation/testNew_automation.automation-meta.json +46 -0
  37. package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.json → testExisting_query.query-meta.json} +2 -2
  38. package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.json → testNew_query.query-meta.json} +2 -2
  39. package/test/resourceFactory.js +64 -21
  40. package/test/resources/9999999/automation/build-expected.json +58 -0
  41. package/test/resources/9999999/automation/create-expected.json +46 -0
  42. package/test/resources/9999999/automation/create-testNew_automation-expected.md +28 -0
  43. package/test/resources/9999999/automation/delete-response.xml +40 -0
  44. package/test/resources/9999999/automation/retrieve-expected.json +58 -0
  45. package/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md +30 -0
  46. package/test/resources/9999999/automation/template-expected.json +58 -0
  47. package/test/resources/9999999/automation/update-expected.json +46 -0
  48. package/test/resources/9999999/automation/update-testExisting_automation-expected.md +28 -0
  49. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  50. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json +85 -0
  51. package/test/resources/9999999/automation/v1/automations/a8afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  52. package/test/resources/9999999/automation/v1/automations/post-response.json +85 -0
  53. package/test/resources/9999999/automation/v1/dataextracts/56c5370a-f988-4f36-b0ee-0f876573f6d7/get-response.json +38 -0
  54. package/test/resources/9999999/automation/v1/dataextracts/get-response.json +20 -0
  55. package/test/resources/9999999/automation/v1/filetransfers/72c328ac-f5b0-4e37-91d3-a775666f15a6/get-response.json +18 -0
  56. package/test/resources/9999999/automation/v1/filetransfers/get-response.json +15 -0
  57. package/test/resources/9999999/automation/v1/imports/get-response.json +38 -0
  58. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/actions/start/post-response.txt +1 -0
  59. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/get-response.json +2 -2
  60. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +2 -2
  61. package/test/resources/9999999/automation/v1/queries/get-response.json +4 -4
  62. package/test/resources/9999999/automation/v1/queries/post-response.json +2 -2
  63. package/test/resources/9999999/automation/v1/scripts/get-response.json +17 -0
  64. package/test/resources/9999999/dataFolder/retrieve-ContentType=automations-response.xml +48 -0
  65. package/test/resources/9999999/dataFolder/retrieve-ContentType=queryactivity-response.xml +48 -0
  66. package/test/resources/9999999/dataFolder/retrieve-response.xml +22 -0
  67. package/test/resources/9999999/emailSendDefinition/retrieve-response.xml +85 -0
  68. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json +21 -0
  69. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/post-response.json +0 -0
  70. package/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json +30 -0
  71. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation-response.xml +30 -0
  72. package/test/resources/9999999/program/retrieve-CustomerKey=testNew_automation-response.xml +30 -0
  73. package/test/resources/9999999/program/retrieve-Name=testExisting_automation-response.xml +31 -0
  74. package/test/resources/9999999/program/retrieve-response.xml +32 -0
  75. package/test/resources/9999999/query/build-expected.json +2 -2
  76. package/test/resources/9999999/query/get-expected.json +2 -2
  77. package/test/resources/9999999/query/get2-expected.json +2 -2
  78. package/test/resources/9999999/query/patch-expected.json +2 -2
  79. package/test/resources/9999999/query/post-expected.json +2 -2
  80. package/test/resources/9999999/query/template-expected.json +2 -2
  81. package/test/type.automation.test.js +259 -0
  82. package/test/type.dataExtension.test.js +3 -0
  83. package/test/type.query.test.js +39 -26
  84. package/test/type.user.test.js +17 -3
  85. package/test/utils.js +7 -6
  86. package/.coverage-comment-template.md +0 -20
  87. /package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.sql → testExisting_query.query-meta.sql} +0 -0
  88. /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
  }
@@ -71,6 +75,12 @@ class Automation extends MetadataType {
71
75
  )
72
76
  : [];
73
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
+
74
84
  // * retrieveDir can be empty when we use it in the context of postDeployTasks
75
85
  if (retrieveDir) {
76
86
  metadataMap = await this.saveResults(metadataMap, retrieveDir, null, null);
@@ -83,6 +93,77 @@ class Automation extends MetadataType {
83
93
  }
84
94
  return { metadata: metadataMap, type: this.definition.type };
85
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);
109
+
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
+ );
158
+ Util.logger.info(
159
+ Util.getGrayMsg(` Notifications found for ${found} automation${found === 1 ? '' : 's'}`)
160
+ );
161
+ Util.logger.debug(
162
+ `Notifications not found for ${skipped} automation${skipped === 1 ? '' : 's'}`
163
+ );
164
+ return promiseMap;
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);
@@ -404,86 +643,146 @@ class Automation extends MetadataType {
404
643
  static async postDeployTasks(metadataMap, originalMetadataMap) {
405
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);
647
+
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
+ }
407
707
 
408
- if (originalMetadataMap[key]?.type === 'scheduled') {
409
- // Starting Source == 'Schedule': Try starting the automation
410
- if (originalMetadataMap[key].status === 'Scheduled') {
411
- let schedule = null;
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(originalMetadataMap[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 ${originalMetadataMap[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: 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`
462
- );
463
766
  }
464
- }
465
- if (metadataMap[key].startSource) {
466
- metadataMap[key].schedule = metadataMap[key].startSource.schedule;
467
-
468
- delete metadataMap[key].startSource;
469
- }
470
- if (metadataMap[key].schedule) {
471
- metadataMap[key].schedule.typeId = metadataMap[key].schedule.scheduleTypeId;
472
- delete metadataMap[key].schedule.scheduleTypeId;
473
- }
474
-
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})`
767
+ } else {
768
+ Util.logger.info(
769
+ Util.getGrayMsg(
770
+ ` - scheduled automation '${originalMetadataMap[key].name}' deployed as Paused`
771
+ )
483
772
  );
484
773
  }
485
774
  }
775
+ if (metadataMap[key].startSource) {
776
+ metadataMap[key].schedule = metadataMap[key].startSource.schedule;
777
+
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;
783
+ }
486
784
  }
785
+
487
786
  /**
488
787
  * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
489
788
  *
@@ -549,118 +848,6 @@ class Automation extends MetadataType {
549
848
  }
550
849
  }
551
850
 
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
851
  /**
665
852
  * Builds a schedule object to be used for scheduling an automation
666
853
  * based on combination of ical string and start/end dates.
@@ -868,6 +1055,27 @@ class Automation extends MetadataType {
868
1055
  output += `* Pattern: ${json.fileTrigger.fileNamingPattern}\n`;
869
1056
  output += `* Folder: ${json.fileTrigger.folderLocationText}\n`;
870
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
+
871
1079
  // show table with automation steps
872
1080
  if (tabled && tabled.length) {
873
1081
  // add empty line to ensure the following table is rendered properly
@@ -975,11 +1183,10 @@ class Automation extends MetadataType {
975
1183
  // as part of retrieve & manual execution we could face an empty folder
976
1184
  return;
977
1185
  }
978
- await Promise.all(
979
- Object.keys(metadata).map((key) => {
980
- this._writeDoc(docPath + '/', key, metadata[key], 'md');
981
- return metadata[key];
982
- })
1186
+ return await Promise.all(
1187
+ Object.keys(metadata).map((key) =>
1188
+ this._writeDoc(docPath + '/', key, metadata[key], 'md')
1189
+ )
983
1190
  );
984
1191
  }
985
1192
  }