mcdev 3.1.3 → 4.0.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 (134) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +2 -1
  3. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  4. package/.github/dependabot.yml +14 -0
  5. package/.github/workflows/code-analysis.yml +57 -0
  6. package/.husky/commit-msg +10 -0
  7. package/.husky/post-checkout +5 -0
  8. package/.husky/pre-commit +2 -1
  9. package/.prettierrc +8 -0
  10. package/.vscode/settings.json +1 -1
  11. package/LICENSE +2 -2
  12. package/README.md +134 -45
  13. package/boilerplate/config.json +5 -11
  14. package/boilerplate/files/.prettierrc +8 -0
  15. package/boilerplate/files/.vscode/extensions.json +0 -1
  16. package/boilerplate/files/.vscode/settings.json +28 -2
  17. package/boilerplate/files/README.md +2 -2
  18. package/boilerplate/forcedUpdates.json +10 -0
  19. package/boilerplate/npm-dependencies.json +5 -5
  20. package/docs/dist/documentation.md +2795 -1724
  21. package/jsconfig.json +1 -1
  22. package/lib/Builder.js +166 -75
  23. package/lib/Deployer.js +244 -96
  24. package/lib/MetadataTypeDefinitions.js +2 -0
  25. package/lib/MetadataTypeInfo.js +2 -0
  26. package/lib/Retriever.js +61 -84
  27. package/lib/cli.js +116 -11
  28. package/lib/index.js +241 -561
  29. package/lib/metadataTypes/AccountUser.js +101 -95
  30. package/lib/metadataTypes/Asset.js +677 -248
  31. package/lib/metadataTypes/AttributeGroup.js +23 -12
  32. package/lib/metadataTypes/Automation.js +451 -354
  33. package/lib/metadataTypes/Campaign.js +33 -93
  34. package/lib/metadataTypes/ContentArea.js +31 -11
  35. package/lib/metadataTypes/DataExtension.js +387 -372
  36. package/lib/metadataTypes/DataExtensionField.js +131 -54
  37. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  38. package/lib/metadataTypes/DataExtract.js +61 -48
  39. package/lib/metadataTypes/DataExtractType.js +14 -8
  40. package/lib/metadataTypes/Discovery.js +21 -16
  41. package/lib/metadataTypes/Email.js +32 -12
  42. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  43. package/lib/metadataTypes/EventDefinition.js +61 -43
  44. package/lib/metadataTypes/FileTransfer.js +72 -52
  45. package/lib/metadataTypes/Filter.js +11 -4
  46. package/lib/metadataTypes/Folder.js +149 -117
  47. package/lib/metadataTypes/FtpLocation.js +14 -8
  48. package/lib/metadataTypes/ImportFile.js +61 -64
  49. package/lib/metadataTypes/Interaction.js +19 -4
  50. package/lib/metadataTypes/List.js +54 -13
  51. package/lib/metadataTypes/MetadataType.js +668 -454
  52. package/lib/metadataTypes/MobileCode.js +46 -0
  53. package/lib/metadataTypes/MobileKeyword.js +114 -0
  54. package/lib/metadataTypes/Query.js +204 -103
  55. package/lib/metadataTypes/Role.js +76 -61
  56. package/lib/metadataTypes/Script.js +145 -81
  57. package/lib/metadataTypes/SetDefinition.js +20 -8
  58. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  59. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  60. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  61. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  62. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  63. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  64. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  65. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  66. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  67. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  68. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  69. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  70. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  72. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  73. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  74. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  75. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  76. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  77. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  78. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  79. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  80. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  81. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  82. package/lib/retrieveChangelog.js +7 -6
  83. package/lib/util/auth.js +117 -0
  84. package/lib/util/businessUnit.js +55 -66
  85. package/lib/util/cache.js +194 -0
  86. package/lib/util/cli.js +90 -116
  87. package/lib/util/config.js +302 -0
  88. package/lib/util/devops.js +240 -50
  89. package/lib/util/file.js +120 -191
  90. package/lib/util/init.config.js +195 -69
  91. package/lib/util/init.git.js +45 -50
  92. package/lib/util/init.js +72 -59
  93. package/lib/util/init.npm.js +48 -39
  94. package/lib/util/util.js +280 -564
  95. package/package.json +44 -33
  96. package/test/dataExtension.test.js +152 -0
  97. package/test/mockRoot/.mcdev-auth.json +8 -0
  98. package/test/mockRoot/.mcdevrc.json +67 -0
  99. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  105. package/test/query.test.js +149 -0
  106. package/test/resourceFactory.js +142 -0
  107. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  108. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  109. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  110. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  111. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  112. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  113. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  114. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  115. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  116. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  117. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  118. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  119. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  120. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  121. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  122. package/test/resources/9999999/query/build-expected.json +8 -0
  123. package/test/resources/9999999/query/get-expected.json +11 -0
  124. package/test/resources/9999999/query/patch-expected.json +11 -0
  125. package/test/resources/9999999/query/post-expected.json +11 -0
  126. package/test/resources/9999999/query/template-expected.json +8 -0
  127. package/test/resources/auth.json +32 -0
  128. package/test/resources/rest404-response.json +5 -0
  129. package/test/resources/retrieve-response.xml +21 -0
  130. package/test/utils.js +107 -0
  131. package/types/mcdev.d.js +301 -0
  132. package/CHANGELOG.md +0 -126
  133. package/PULL_REQUEST_TEMPLATE.md +0 -19
  134. package/test/util/file.js +0 -51
@@ -1,165 +1,96 @@
1
1
  'use strict';
2
2
 
3
3
  const MetadataType = require('./MetadataType');
4
+ const TYPE = require('../../types/mcdev.d');
4
5
  const Util = require('../util/util');
5
6
  const File = require('../util/file');
6
7
  const Definitions = require('../MetadataTypeDefinitions');
7
-
8
- /**
9
- * @typedef {Object} AutomationActivity
10
- * @property {string} name name (not key) of activity
11
- * @property {string} [objectTypeId] Id of assoicated activity type; see this.definition.activityTypeMapping
12
- * @property {string} [activityObjectId] Object Id of assoicated metadata item
13
- * @property {number} displayOrder order within step; starts with 1 or higher number
14
- * @property {string} r__type see this.definition.activityTypeMapping
15
- *
16
- * @typedef {Object} AutomationStep
17
- * @property {string} name description
18
- * @property {string} [annotation] equals AutomationStep.name
19
- * @property {number} step step iterator
20
- * @property {number} [stepNumber] step iterator, automatically set during deployment
21
- * @property {AutomationActivity[]} activities -
22
- *
23
- * @typedef {Object} AutomationSchedule REST format
24
- * @property {number} typeId ?
25
- * @property {string} startDate example: '2021-05-07T09:00:00'
26
- * @property {string} endDate example: '2021-05-07T09:00:00'
27
- * @property {string} icalRecur example: 'FREQ=DAILY;UNTIL=20790606T160000;INTERVAL=1'
28
- * @property {string} timezoneName example: 'W. Europe Standard Time'; see this.definition.timeZoneMapping
29
- * @property {number} [timezoneId] see this.definition.timeZoneMapping
30
- *
31
- * @typedef {Object} AutomationScheduleSoap SOAP format
32
- * @property {Object} Recurrence -
33
- * @property {Object} Recurrence.$ {'xsi:type': keyStem + 'lyRecurrence'}
34
- * @property {'ByYear'} [Recurrence.YearlyRecurrencePatternType] * currently not supported by tool *
35
- * @property {'ByMonth'} [Recurrence.MonthlyRecurrencePatternType] * currently not supported by tool *
36
- * @property {'ByWeek'} [Recurrence.WeeklyRecurrencePatternType] * currently not supported by tool *
37
- * @property {'ByDay'} [Recurrence.DailyRecurrencePatternType] -
38
- * @property {'Interval'} [Recurrence.MinutelyRecurrencePatternType] -
39
- * @property {'Interval'} [Recurrence.HourlyRecurrencePatternType] -
40
- * @property {number} [Recurrence.YearInterval] 1..n * currently not supported by tool *
41
- * @property {number} [Recurrence.MonthInterval] 1..n * currently not supported by tool *
42
- * @property {number} [Recurrence.WeekInterval] 1..n * currently not supported by tool *
43
- * @property {number} [Recurrence.DayInterval] 1..n
44
- * @property {number} [Recurrence.HourInterval] 1..n
45
- * @property {number} [Recurrence.MinuteInterval] 1..n
46
- * @property {number} _interval internal variable for CLI output only
47
- * @property {Object} TimeZone -
48
- * @property {number} TimeZone.ID AutomationSchedule.timezoneId
49
- * @property {string} _timezoneString internal variable for CLI output only
50
- * @property {string} StartDateTime AutomationSchedule.startDate
51
- * @property {string} EndDateTime AutomationSchedule.endDate
52
- * @property {string} _StartDateTime AutomationSchedule.startDate; internal variable for CLI output only
53
- * @property {'EndOn'|'EndAfter'} RecurrenceRangeType set to 'EndOn' if AutomationSchedule.icalRecur contains 'UNTIL'; otherwise to 'EndAfter'
54
- * @property {number} Occurrences only exists if RecurrenceRangeType=='EndAfter'
55
- *
56
- * @typedef {Object} AutomationItem
57
- * @property {string} [id] Object Id
58
- * @property {string} key key
59
- * @property {string} name name
60
- * @property {string} description -
61
- * @property {'scheduled'|'triggered'} type Starting Source = Schedule / File Drop
62
- * @property {'Scheduled'|'Running'} status -
63
- * @property {AutomationSchedule} [schedule] only existing if type=scheduled
64
- * @property {Object} [fileTrigger] only existing if type=triggered
65
- * @property {string} fileTrigger.fileNamingPattern -
66
- * @property {string} fileTrigger.fileNamePatternTypeId -
67
- * @property {string} fileTrigger.folderLocationText -
68
- * @property {string} fileTrigger.queueFiles -
69
- * @property {Object} [startSource] -
70
- * @property {AutomationSchedule} [startSource.schedule] rewritten to AutomationItem.schedule
71
- * @property {Object} [startSource.fileDrop] rewritten to AutomationItem.fileTrigger
72
- * @property {string} startSource.fileDrop.fileNamingPattern -
73
- * @property {string} startSource.fileDrop.fileNamePatternTypeId -
74
- * @property {string} startSource.fileDrop.folderLocation -
75
- * @property {string} startSource.fileDrop.queueFiles -
76
- * @property {number} startSource.typeId -
77
- * @property {AutomationStep[]} steps -
78
- * @property {string} r__folder_Path folder path
79
- * @property {string} [categoryId] holds folder ID, replaced with r__folder_Path during retrieve
80
- *
81
- * @typedef {Object.<string, AutomationItem>} AutomationMap
82
- */
8
+ const cache = require('../util/cache');
83
9
 
84
10
  /**
85
11
  * Automation MetadataType
12
+ *
86
13
  * @augments MetadataType
87
14
  */
88
15
  class Automation extends MetadataType {
89
16
  /**
90
17
  * Retrieves Metadata of Automation
18
+ *
91
19
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
92
- * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
20
+ * @param {void} [_] unused parameter
21
+ * @param {void} [__] unused parameter
22
+ * @param {void} [___] unused parameter
23
+ * @param {string} [key] customer key of single item to retrieve
24
+ * @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
93
25
  */
94
- static async retrieve(retrieveDir) {
95
- const results = await new Promise((resolve, reject) => {
96
- this.client.SoapClient.retrieve('Program', ['ObjectID'], (ex, response) =>
97
- ex ? reject(ex) : resolve(response.body.Results)
98
- );
99
- });
100
- const details = (
101
- await Promise.all(
102
- results.map((a) =>
103
- this.client.RestClient.get({
104
- uri: '/automation/v1/automations/' + a.ObjectID,
105
- })
106
- )
107
- )
108
- ).map((b) => b.body);
26
+ static async retrieve(retrieveDir, _, __, ___, key) {
27
+ /** @type {TYPE.SoapRequestParams} */
28
+ let requestParams = null;
29
+ if (key) {
30
+ requestParams = {
31
+ filter: {
32
+ leftOperand: 'CustomerKey',
33
+ operator: 'equals',
34
+ rightOperand: key,
35
+ },
36
+ };
37
+ }
38
+ const results = await this.client.soap.retrieveBulk('Program', ['ObjectID'], requestParams);
39
+
40
+ const details = results.Results
41
+ ? await Promise.all(
42
+ results.Results.map((a) =>
43
+ this.client.rest.get('/automation/v1/automations/' + a.ObjectID)
44
+ )
45
+ )
46
+ : [];
109
47
  const parsed = this.parseResponseBody({ items: details });
110
48
 
49
+ // * retrieveDir is mandatory in this method as it is not used for caching (there is a seperate method for that)
111
50
  const savedMetadata = await this.saveResults(parsed, retrieveDir, null, null);
112
51
  Util.logger.info(
113
52
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
114
53
  );
54
+ if (this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
55
+ await this.document(this.buObject, savedMetadata);
56
+ }
115
57
  return { metadata: savedMetadata, type: this.definition.type };
116
58
  }
117
59
  /**
118
60
  * Retrieves Metadata of Automation
119
- * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
61
+ *
62
+ * @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
120
63
  */
121
64
  static async retrieveChangelog() {
122
- const results = await new Promise((resolve, reject) => {
123
- this.client.SoapClient.retrieve(
124
- 'Program',
125
- ['ObjectID'],
126
-
127
- (ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
128
- );
129
- });
65
+ const results = await this.client.soap.retrieveBulk('Program', ['ObjectID']);
130
66
  const details = [];
131
- (
132
- await Promise.all(
133
- results.map(async (a) => {
134
- const options = {
135
- filter: {
136
- leftOperand: 'ProgramID',
137
- operator: 'equals',
138
- rightOperand: a.ObjectID,
139
- },
140
- };
141
-
142
- return new Promise((resolve, reject) => {
143
- this.client.SoapClient.retrieve(
144
- 'Automation',
145
- [
146
- 'ProgramID',
147
- 'Name',
148
- 'CustomerKey',
149
- 'LastSaveDate',
150
- 'LastSavedBy',
151
- 'CreatedBy',
152
- 'CreatedDate',
153
- ],
154
- options,
155
- (ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
156
- );
157
- });
158
- })
159
- )
160
- ).forEach((item) => {
161
- details.push(...item);
162
- });
67
+ for (const item of results.Results
68
+ ? await Promise.all(
69
+ results.Results.map((a) =>
70
+ this.client.soap.retrieveBulk(
71
+ 'Automation',
72
+ [
73
+ 'ProgramID',
74
+ 'Name',
75
+ 'CustomerKey',
76
+ 'LastSaveDate',
77
+ 'LastSavedBy',
78
+ 'CreatedBy',
79
+ 'CreatedDate',
80
+ ],
81
+ {
82
+ filter: {
83
+ leftOperand: 'ProgramID',
84
+ operator: 'equals',
85
+ rightOperand: a.ObjectID,
86
+ },
87
+ }
88
+ )
89
+ )
90
+ )
91
+ : []) {
92
+ details.push(...item.Results);
93
+ }
163
94
  details.map((item) => {
164
95
  item.key = item.CustomerKey;
165
96
  });
@@ -171,24 +102,17 @@ class Automation extends MetadataType {
171
102
 
172
103
  /**
173
104
  * Retrieves automation metadata for caching
174
- * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
105
+ *
106
+ * @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
175
107
  */
176
108
  static async retrieveForCache() {
177
- const results = await new Promise((resolve) => {
178
- this.client.SoapClient.retrieve(
179
- 'Program',
180
- ['ObjectID', 'CustomerKey', 'Name'],
181
- (error, response) => {
182
- if (error) {
183
- throw new Error(error);
184
- } else {
185
- resolve(response.body.Results);
186
- }
187
- }
188
- );
189
- });
109
+ const results = await this.client.soap.retrieveBulk('Program', [
110
+ 'ObjectID',
111
+ 'CustomerKey',
112
+ 'Name',
113
+ ]);
190
114
  const resultsConverted = {};
191
- for (const m of results) {
115
+ for (const m of results.Results) {
192
116
  resultsConverted[m.CustomerKey] = {
193
117
  id: m.ObjectID,
194
118
  key: m.CustomerKey,
@@ -200,44 +124,30 @@ class Automation extends MetadataType {
200
124
 
201
125
  /**
202
126
  * Retrieve a specific Automation Definition by Name
127
+ *
203
128
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
204
129
  * @param {string} name name of the metadata file
205
- * @param {Util.TemplateMap} templateVariables variables to be replaced in the metadata
206
- * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
130
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
131
+ * @returns {Promise.<TYPE.AutomationItemObj>} Promise of metadata
207
132
  */
208
133
  static async retrieveAsTemplate(templateDir, name, templateVariables) {
209
- const results = await new Promise((resolve) => {
210
- this.client.SoapClient.retrieve(
211
- 'Program',
212
- ['ObjectID', 'Name'],
213
- {
214
- filter: {
215
- leftOperand: 'Name',
216
- operator: 'equals',
217
- rightOperand: name,
218
- },
219
- },
220
- (error, response) => {
221
- if (error) {
222
- throw new Error(error);
223
- } else {
224
- resolve(response.body.Results);
225
- }
226
- }
227
- );
134
+ const results = await this.client.soap.retrieve('Program', ['ObjectID', 'Name'], {
135
+ filter: {
136
+ leftOperand: 'Name',
137
+ operator: 'equals',
138
+ rightOperand: name,
139
+ },
228
140
  });
229
- if (results && results[0]) {
141
+ if (Array.isArray(results?.Results)) {
230
142
  // eq-operator returns a similar, not exact match and hence might return more than 1 entry
231
- const [metadata] = results.filter((item) => item.Name === name);
143
+ const metadata = results.Results.find((item) => item.Name === name);
232
144
  if (!metadata) {
233
145
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
234
146
  return;
235
147
  }
236
- const details = (
237
- await this.client.RestClient.get({
238
- uri: '/automation/v1/automations/' + metadata.ObjectID,
239
- })
240
- ).body;
148
+ const details = await this.client.rest.get(
149
+ '/automation/v1/automations/' + metadata.ObjectID
150
+ );
241
151
  let val = null;
242
152
  let originalKey;
243
153
  // if parsing fails, we should just save what we get
@@ -249,7 +159,7 @@ class Automation extends MetadataType {
249
159
  Util.replaceByObject(JSON.stringify(parsedDetails), templateVariables)
250
160
  );
251
161
  }
252
- } catch (ex) {
162
+ } catch {
253
163
  val = JSON.parse(JSON.stringify(details));
254
164
  }
255
165
  if (val === null) {
@@ -259,14 +169,12 @@ class Automation extends MetadataType {
259
169
  }
260
170
  // remove all fields not listed in Definition for templating
261
171
  this.keepTemplateFields(val);
262
- File.writeJSONToFile(
172
+ await File.writeJSONToFile(
263
173
  [templateDir, this.definition.type].join('/'),
264
174
  originalKey + '.' + this.definition.type + '-meta',
265
175
  val
266
176
  );
267
- Util.logger.info(
268
- `Automation.retrieveAsTemplate:: Written Metadata to filesystem (${name})`
269
- );
177
+ Util.logger.info(`- templated ${this.definition.type}: ${name}`);
270
178
  return { metadata: val, type: this.definition.type };
271
179
  } else if (results) {
272
180
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
@@ -278,26 +186,21 @@ class Automation extends MetadataType {
278
186
  }
279
187
  /**
280
188
  * manages post retrieve steps
281
- * @param {AutomationItem} metadata a single automation
282
- * @param {string} [_] unused
283
- * @param {Boolean} [isTemplating] signals that we are retrieving templates
284
- * @returns {AutomationItem} metadata
189
+ *
190
+ * @param {TYPE.AutomationItem} metadata a single automation
191
+ * @returns {TYPE.AutomationItem} metadata
285
192
  */
286
- static postRetrieveTasks(metadata, _, isTemplating) {
287
- // if retrieving template, replace the name with customer key if that wasn't already the case
288
- if (isTemplating) {
289
- const warningMsg = null;
290
- this.overrideKeyWithName(metadata, warningMsg);
291
- }
193
+ static postRetrieveTasks(metadata) {
292
194
  return this.parseMetadata(metadata);
293
195
  }
294
196
 
295
197
  /**
296
198
  * Deploys automation - the saved file is the original one due to large differences required for deployment
297
- * @param {AutomationMap} metadata metadata mapped by their keyField
199
+ *
200
+ * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
298
201
  * @param {string} targetBU name/shorthand of target businessUnit for mapping
299
202
  * @param {string} retrieveDir directory where metadata after deploy should be saved
300
- * @returns {Promise<AutomationMap>} Promise
203
+ * @returns {Promise.<TYPE.AutomationMap>} Promise
301
204
  */
302
205
  static async deploy(metadata, targetBU, retrieveDir) {
303
206
  const orignalMetadata = JSON.parse(JSON.stringify(metadata));
@@ -309,7 +212,8 @@ class Automation extends MetadataType {
309
212
 
310
213
  /**
311
214
  * Creates a single automation
312
- * @param {AutomationItem} metadata single metadata entry
215
+ *
216
+ * @param {TYPE.AutomationItem} metadata single metadata entry
313
217
  * @returns {Promise} Promise
314
218
  */
315
219
  static create(metadata) {
@@ -319,8 +223,9 @@ class Automation extends MetadataType {
319
223
 
320
224
  /**
321
225
  * Updates a single automation
322
- * @param {AutomationItem} metadata single metadata entry
323
- * @param {AutomationItem} metadataBefore metadata mapped by their keyField
226
+ *
227
+ * @param {TYPE.AutomationItem} metadata single metadata entry
228
+ * @param {TYPE.AutomationItem} metadataBefore metadata mapped by their keyField
324
229
  * @returns {Promise} Promise
325
230
  */
326
231
  static update(metadata, metadataBefore) {
@@ -331,14 +236,14 @@ class Automation extends MetadataType {
331
236
 
332
237
  /**
333
238
  * Gets executed before deploying metadata
334
- * @param {AutomationItem} metadata metadata mapped by their keyField
335
- * @returns {Promise<AutomationItem>} Promise
239
+ *
240
+ * @param {TYPE.AutomationItem} metadata metadata mapped by their keyField
241
+ * @returns {Promise.<TYPE.AutomationItem>} Promise
336
242
  */
337
243
  static async preDeployTasks(metadata) {
338
244
  if (this.validateDeployMetadata(metadata)) {
339
245
  try {
340
- metadata.categoryId = Util.getFromCache(
341
- this.cache,
246
+ metadata.categoryId = cache.searchForField(
342
247
  'folder',
343
248
  metadata.r__folder_Path,
344
249
  'Path',
@@ -346,7 +251,7 @@ class Automation extends MetadataType {
346
251
  );
347
252
  if (metadata.r__folder_Path !== 'my automations') {
348
253
  Util.logger.warn(
349
- `Automation '${
254
+ ` - Automation '${
350
255
  metadata[this.definition.nameField]
351
256
  }' is located in subfolder ${
352
257
  metadata.r__folder_Path
@@ -354,12 +259,12 @@ class Automation extends MetadataType {
354
259
  );
355
260
  }
356
261
  delete metadata.r__folder_Path;
357
- } catch (ex) {
262
+ } catch {
358
263
  throw new Error(
359
264
  `Folder '${metadata.r__folder_Path}' was not found on the server. Please create this manually in the GUI. Automation-folders cannot be deployed automatically.`
360
265
  );
361
266
  }
362
- if (metadata.type === 'scheduled' && metadata.schedule && metadata.schedule.startDate) {
267
+ if (metadata.type === 'scheduled' && metadata?.schedule?.startDate) {
363
268
  // Starting Source == 'Schedule'
364
269
 
365
270
  delete metadata.schedule.rangeTypeId;
@@ -399,26 +304,33 @@ class Automation extends MetadataType {
399
304
  delete metadata.schedule;
400
305
  delete metadata.type;
401
306
  let i = 0;
402
- for (const step of metadata.steps) {
403
- for (const activity of step.activities) {
404
- if (activity.name && this.definition.dependencies.includes(activity.r__type)) {
405
- // automations can have empty placeholder for activities with only their type defined
406
- activity.activityObjectId = Util.getFromCache(
407
- this.cache,
408
- activity.r__type,
409
- activity.name,
410
- Definitions[activity.r__type].nameField,
411
- Definitions[activity.r__type].idField
412
- );
307
+ if (metadata.steps) {
308
+ for (const step of metadata.steps) {
309
+ let displayOrder = 0;
310
+ for (const activity of step.activities) {
311
+ activity.displayOrder = ++displayOrder;
312
+ if (
313
+ activity.name &&
314
+ this.definition.dependencies.includes(activity.r__type)
315
+ ) {
316
+ // automations can have empty placeholder for activities with only their type defined
317
+ activity.activityObjectId = cache.searchForField(
318
+ activity.r__type,
319
+ activity.name,
320
+ Definitions[activity.r__type].nameField,
321
+ Definitions[activity.r__type].idField
322
+ );
323
+ }
324
+ activity.objectTypeId =
325
+ this.definition.activityTypeMapping[activity.r__type];
326
+ delete activity.r__type;
413
327
  }
414
- activity.objectTypeId = this.definition.activityTypeMapping[activity.r__type];
415
- delete activity.r__type;
328
+ step.annotation = step.name;
329
+ step.stepNumber = i;
330
+ delete step.name;
331
+ delete step.step;
332
+ i++;
416
333
  }
417
- step.annotation = step.name;
418
- step.stepNumber = i;
419
- delete step.name;
420
- delete step.step;
421
- i++;
422
334
  }
423
335
  return metadata;
424
336
  } else {
@@ -428,52 +340,62 @@ class Automation extends MetadataType {
428
340
  /**
429
341
  * Validates the automation to be sure it can be deployed.
430
342
  * Whitelisted Activites are deployed but require configuration
431
- * @param {AutomationItem} metadata single automation record
432
- * @returns {Boolean} result if automation can be deployed based on steps
343
+ *
344
+ * @param {TYPE.AutomationItem} metadata single automation record
345
+ * @returns {boolean} result if automation can be deployed based on steps
433
346
  */
434
347
  static validateDeployMetadata(metadata) {
435
348
  let deployable = true;
349
+ const errors = [];
436
350
  if (metadata.steps) {
351
+ let stepNumber = 0;
437
352
  for (const step of metadata.steps) {
353
+ stepNumber++;
354
+ let displayOrder = 0;
355
+
438
356
  for (const activity of step.activities) {
357
+ displayOrder++;
439
358
  // check if manual deploy required. if so then log warning
440
359
  if (this.definition.manualDeployTypes.includes(activity.r__type)) {
441
360
  Util.logger.warn(
442
- `Automation '${
443
- metadata.name
444
- }' requires additional manual configuration: '${
445
- activity.name
446
- }' in step ${step.stepNumber || step.step}.${activity.displayOrder}`
361
+ `- ${this.definition.type} '${metadata.name}' requires additional manual configuration: '${activity.name}' in step ${stepNumber}.${displayOrder}`
447
362
  );
448
363
  }
449
364
  // cannot deploy because it is not supported
450
365
  else if (!this.definition.dependencies.includes(activity.r__type)) {
451
- Util.logger.error(
452
- `Automation '${
453
- metadata.name
454
- }' cannot be deployed as the following activity is not supported: '${
455
- activity.name
456
- }' in step ${step.stepNumber || step.step}.${activity.displayOrder}`
366
+ errors.push(
367
+ ` not supported ${activity.r__type} activity '${activity.name}' in step ${stepNumber}.${displayOrder}`
457
368
  );
458
369
  deployable = false;
459
370
  }
460
371
  }
461
372
  }
462
373
  }
374
+ if (!deployable) {
375
+ Util.logger.error(
376
+ ` ☇ skipping ${this.definition.type} ${metadata[this.definition.keyField]} / ${
377
+ metadata[this.definition.nameField]
378
+ }:`
379
+ );
380
+ for (const error of errors) {
381
+ Util.logger.error(error);
382
+ }
383
+ }
463
384
  return deployable;
464
385
  }
465
386
 
466
387
  /**
467
388
  * Gets executed after deployment of metadata type
468
- * @param {AutomationMap} metadata metadata mapped by their keyField
469
- * @param {AutomationMap} originalMetadata metadata to be updated (contains additioanl fields)
470
- * @returns {Promise<void>} -
389
+ *
390
+ * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
391
+ * @param {TYPE.AutomationMap} originalMetadata metadata to be updated (contains additioanl fields)
392
+ * @returns {Promise.<void>} -
471
393
  */
472
394
  static async postDeployTasks(metadata, originalMetadata) {
473
395
  for (const key in metadata) {
474
396
  // need to put schedule on here if status is scheduled
475
397
 
476
- if (originalMetadata[key] && originalMetadata[key].type === 'scheduled') {
398
+ if (originalMetadata[key]?.type === 'scheduled') {
477
399
  // Starting Source == 'Schedule': Try starting the automation
478
400
  if (originalMetadata[key].status === 'Scheduled') {
479
401
  let schedule = null;
@@ -481,70 +403,45 @@ class Automation extends MetadataType {
481
403
  schedule = this._buildSchedule(originalMetadata[key].schedule);
482
404
  } catch (ex) {
483
405
  Util.logger.error(
484
- `Could not create schedule for automation ${originalMetadata[key].name} to start it: ${ex.message}`
406
+ `- Could not create schedule for automation ${originalMetadata[key].name} to start it: ${ex.message}`
485
407
  );
486
408
  }
487
409
  if (schedule !== null) {
488
410
  try {
489
- await Util.retryOnError(
490
- `Retrying ${this.definition.type}`,
491
- async () => {
492
- await new Promise((resolve, reject) => {
493
- this.client.SoapClient.schedule(
494
- 'Automation',
495
- schedule,
496
- {
497
- Interaction: {
498
- ObjectID: metadata[key].id,
499
- },
500
- },
501
- 'start',
502
- null,
503
- (error, response) => {
504
- if (
505
- error ||
506
- (response.body.Results &&
507
- response.body.Results[0] &&
508
- response.body.Results[0].StatusCode &&
509
- response.body.Results[0].StatusCode ===
510
- 'Error')
511
- ) {
512
- reject(
513
- error ||
514
- response.body.Results[0].StatusMessage
515
- );
516
- } else {
517
- resolve(response.body.Results);
518
- }
519
- }
520
- );
521
- });
522
- const intervalString =
523
- (schedule._interval > 1 ? `${schedule._interval} ` : '') +
524
- (schedule.RecurrenceType === 'Daily'
525
- ? 'Day'
526
- : schedule.RecurrenceType.slice(0, -2) +
527
- (schedule._interval > 1 ? 's' : ''));
528
- Util.logger.warn(
529
- `Automation '${
530
- originalMetadata[key].name
531
- }' deployed Active: runs every ${intervalString} starting ${
532
- schedule._StartDateTime
533
- .split('T')
534
- .join(' ')
535
- .split('.')[0]
536
- } ${schedule._timezoneString}`
537
- );
538
- }
411
+ await this.client.soap.schedule(
412
+ 'Automation',
413
+ schedule,
414
+ {
415
+ Interaction: {
416
+ ObjectID: metadata[key].id,
417
+ },
418
+ },
419
+ 'start',
420
+ {}
421
+ );
422
+ const intervalString =
423
+ (schedule._interval > 1 ? `${schedule._interval} ` : '') +
424
+ (schedule.RecurrenceType === 'Daily'
425
+ ? 'Day'
426
+ : schedule.RecurrenceType.slice(0, -2) +
427
+ (schedule._interval > 1 ? 's' : ''));
428
+ Util.logger.warn(
429
+ ` - scheduled automation '${
430
+ originalMetadata[key].name
431
+ }' deployed Active: runs every ${intervalString} starting ${
432
+ schedule._StartDateTime.split('T').join(' ').split('.')[0]
433
+ } ${schedule._timezoneString}`
539
434
  );
540
435
  } catch (ex) {
541
436
  Util.logger.error(
542
- `Could not start automation '${originalMetadata[key].name}': ${ex.message}`
437
+ `- Could not start scheduled automation '${originalMetadata[key].name}': ${ex.message}`
543
438
  );
544
439
  }
545
440
  }
546
441
  } else {
547
- Util.logger.warn(`Automation '${originalMetadata[key].name}' deployed Paused`);
442
+ Util.logger.warn(
443
+ ` - scheduled automation '${originalMetadata[key].name}' deployed Paused`
444
+ );
548
445
  }
549
446
  }
550
447
  if (metadata[key].startSource) {
@@ -561,14 +458,14 @@ class Automation extends MetadataType {
561
458
 
562
459
  /**
563
460
  * parses retrieved Metadata before saving
564
- * @param {AutomationItem} metadata a single automation definition
565
- * @returns {Array} Array with one metadata object and one sql string
461
+ *
462
+ * @param {TYPE.AutomationItem} metadata a single automation definition
463
+ * @returns {TYPE.AutomationItem} parsed item
566
464
  */
567
465
  static parseMetadata(metadata) {
568
466
  // automations are often skipped due to lack of support.
569
467
  try {
570
- metadata.r__folder_Path = Util.getFromCache(
571
- this.cache,
468
+ metadata.r__folder_Path = cache.searchForField(
572
469
  'folder',
573
470
  metadata.categoryId,
574
471
  'ID',
@@ -577,7 +474,9 @@ class Automation extends MetadataType {
577
474
  delete metadata.categoryId;
578
475
  if (metadata.r__folder_Path !== 'my automations') {
579
476
  Util.logger.verbose(
580
- `Automation '${metadata[this.definition.nameField]}' is located in subfolder ${
477
+ `- automation '${
478
+ metadata[this.definition.nameField]
479
+ }' is located in subfolder ${
581
480
  metadata.r__folder_Path
582
481
  }. Please note that creating automation folders is not supported via API and hence you will have to create it manually in the GUI if you choose to deploy this automation.`
583
482
  );
@@ -585,13 +484,13 @@ class Automation extends MetadataType {
585
484
  } catch (ex) {
586
485
  // * don't exit on missing folder for automation
587
486
  Util.logger.warn(
588
- `${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
487
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
589
488
  ex.message
590
489
  }`
591
490
  );
592
491
  }
593
492
  try {
594
- if (metadata.type === 'scheduled' && metadata.schedule && metadata.schedule.startDate) {
493
+ if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
595
494
  // Starting Source == 'Schedule'
596
495
 
597
496
  try {
@@ -599,24 +498,21 @@ class Automation extends MetadataType {
599
498
  // if we found the id in our list, remove the redundant data
600
499
  delete metadata.schedule.timezoneId;
601
500
  }
602
- } catch (ex) {
501
+ } catch {
603
502
  Util.logger.debug(
604
- `Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
503
+ `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
605
504
  );
606
505
  }
607
506
  try {
608
507
  // type 'Running' is temporary status only, overwrite with Scheduled for storage.
609
508
  if (metadata.type === 'scheduled' && metadata.status === 'Running') {
610
- metadata.status === 'Scheduled';
509
+ metadata.status = 'Scheduled';
611
510
  }
612
- } catch (ex) {
613
- Util.metadataLogger(
614
- 'error',
615
- this.definition.type,
616
- 'parseMetadata',
617
- `${metadata.name} does not have a valid schedule setting. `
511
+ } catch {
512
+ Util.logger.error(
513
+ `- ${this.definition.type} ${metadata.name} does not have a valid schedule setting.`
618
514
  );
619
- return null;
515
+ return;
620
516
  }
621
517
  } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
622
518
  // Starting Source == 'File Drop'
@@ -644,32 +540,30 @@ class Automation extends MetadataType {
644
540
  }
645
541
  // / if managed by cache we can update references to support deployment
646
542
  else if (
647
- Definitions[activity.r__type] &&
648
- Definitions[activity.r__type]['idField'] &&
649
- this.cache[activity.r__type]
543
+ Definitions[activity.r__type]?.['idField'] &&
544
+ cache.getCache(this.buObject.mid)[activity.r__type]
650
545
  ) {
651
546
  try {
652
- activity.activityObjectId = Util.getFromCache(
653
- this.cache,
547
+ activity.activityObjectId = cache.searchForField(
654
548
  activity.r__type,
655
549
  activity.activityObjectId,
656
550
  Definitions[activity.r__type].idField,
657
551
  Definitions[activity.r__type].nameField
658
552
  );
659
- } catch (e) {
553
+ } catch (ex) {
660
554
  // getFromCache throws error where the dependent metadata is not found
661
- Util.logger.error(
662
- `Missing ${activity.r__type} activity '${activity.name}'` +
555
+ Util.logger.warn(
556
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
663
557
  ` in step ${step.stepNumber || step.step}.${
664
558
  activity.displayOrder
665
559
  }` +
666
- ` of Automation '${metadata.name}' (${e.message})`
560
+ ` of Automation '${metadata.name}' (${ex.message})`
667
561
  );
668
562
  return null;
669
563
  }
670
564
  } else {
671
- Util.logger.error(
672
- `Missing ${activity.r__type} activity '${activity.name}'` +
565
+ Util.logger.warn(
566
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
673
567
  ` in step ${step.stepNumber || step.step}.${
674
568
  activity.displayOrder
675
569
  }` +
@@ -677,19 +571,21 @@ class Automation extends MetadataType {
677
571
  );
678
572
  return null;
679
573
  }
680
- } catch (ex) {
574
+ } catch {
681
575
  Util.logger.warn(
682
- `Excluding automation '${metadata.name}' from retrieve (ObjectType ${activity.objectTypeId} is unknown)`
576
+ ` - Excluding automation '${metadata.name}' from retrieve (ObjectType ${activity.objectTypeId} is unknown)`
683
577
  );
684
578
  return null;
685
579
  }
686
580
  }
581
+ delete step.stepNumber;
582
+ delete step.step;
687
583
  }
688
584
  }
689
585
  return JSON.parse(JSON.stringify(metadata));
690
586
  } catch (ex) {
691
587
  Util.logger.warn(
692
- `${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
588
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
693
589
  ex.message
694
590
  }`
695
591
  );
@@ -700,12 +596,13 @@ class Automation extends MetadataType {
700
596
  /**
701
597
  * Builds a schedule object to be used for scheduling an automation
702
598
  * based on combination of ical string and start/end dates.
703
- * @param {AutomationSchedule} scheduleObject child of automation metadata used for scheduling
704
- * @returns {AutomationScheduleSoap} Schedulable object for soap API (currently not rest supported)
599
+ *
600
+ * @param {TYPE.AutomationSchedule} scheduleObject child of automation metadata used for scheduling
601
+ * @returns {TYPE.AutomationScheduleSoap} Schedulable object for soap API (currently not rest supported)
705
602
  */
706
603
  static _buildSchedule(scheduleObject) {
707
604
  /**
708
- * @type {AutomationScheduleSoap}
605
+ * @type {TYPE.AutomationScheduleSoap}
709
606
  */
710
607
  const schedule = { Recurrence: {}, TimeZone: { IDSpecified: true } };
711
608
  // build recurrence
@@ -751,7 +648,7 @@ class Automation extends MetadataType {
751
648
  this.definition.timeZoneMapping[scheduleObject.timezoneName];
752
649
  } else {
753
650
  Util.logger.error(
754
- `Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
651
+ `- Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
755
652
  );
756
653
  }
757
654
  schedule.TimeZone.ID = scheduleObject.timezoneId;
@@ -769,25 +666,38 @@ class Automation extends MetadataType {
769
666
  const scheduledDate = new Date(inputStartDateString);
770
667
  const futureDate = new Date();
771
668
 
772
- if (keyStem === 'Dai') {
773
- // keep time from template and start today if possible
774
- if (scheduledDate.getHours() <= futureDate.getHours()) {
775
- // hour on template has already passed today, start tomorrow
776
- futureDate.setDate(futureDate.getDate() + 1);
669
+ switch (keyStem) {
670
+ case 'Dai': {
671
+ // keep time from template and start today if possible
672
+ if (scheduledDate.getHours() <= futureDate.getHours()) {
673
+ // hour on template has already passed today, start tomorrow
674
+ futureDate.setDate(futureDate.getDate() + 1);
675
+ }
676
+ futureDate.setHours(scheduledDate.getHours());
677
+ futureDate.setMinutes(scheduledDate.getMinutes());
678
+
679
+ break;
680
+ }
681
+ case 'Hour': {
682
+ // keep minute and start next possible hour
683
+ if (scheduledDate.getMinutes() <= futureDate.getMinutes()) {
684
+ futureDate.setHours(futureDate.getHours() + 1);
685
+ }
686
+ futureDate.setMinutes(scheduledDate.getMinutes());
687
+
688
+ break;
777
689
  }
778
- futureDate.setHours(scheduledDate.getHours());
779
- futureDate.setMinutes(scheduledDate.getMinutes());
780
- } else if (keyStem === 'Hour') {
781
- // keep minute and start next possible hour
782
- if (scheduledDate.getMinutes() <= futureDate.getMinutes()) {
783
- futureDate.setHours(futureDate.getHours() + 1);
690
+ case 'Minute': {
691
+ // schedule in next 15 minutes randomly to avoid that all automations run at exactly
692
+ // earliest start 1 minute from now
693
+ // the same time which would slow performance
694
+ futureDate.setMinutes(
695
+ futureDate.getMinutes() + 1 + Math.ceil(Math.random() * 15)
696
+ );
697
+
698
+ break;
784
699
  }
785
- futureDate.setMinutes(scheduledDate.getMinutes());
786
- } else if (keyStem === 'Minute') {
787
- // schedule in next 15 minutes randomly to avoid that all automations run at exactly
788
- // earliest start 1 minute from now
789
- // the same time which would slow performance
790
- futureDate.setMinutes(futureDate.getMinutes() + 1 + Math.ceil(Math.random() * 15));
700
+ // No default
791
701
  }
792
702
  // return time as Dateobject
793
703
  schedule.StartDateTime = futureDate;
@@ -824,12 +734,10 @@ class Automation extends MetadataType {
824
734
  */
825
735
  static _calcTime(offsetServer, dateInput, offsetInput) {
826
736
  // get UTC time in msec
827
- let utc;
828
- if ('string' === typeof dateInput) {
829
- utc = new Date(dateInput + offsetInput).getTime();
830
- } else {
831
- utc = dateInput.getTime();
832
- }
737
+ const utc =
738
+ 'string' === typeof dateInput
739
+ ? new Date(dateInput + offsetInput).getTime()
740
+ : dateInput.getTime();
833
741
 
834
742
  // create new Date object reflecting SFMC's servertime
835
743
  const dateServer = new Date(utc + 3600000 * offsetServer);
@@ -837,14 +745,203 @@ class Automation extends MetadataType {
837
745
  // return time as a string without trailing "Z"
838
746
  return dateServer.toISOString().slice(0, -1);
839
747
  }
748
+ /**
749
+ * Experimental: Only working for DataExtensions:
750
+ * Saves json content to a html table in the local file system. Will create the parent directory if it does not exist.
751
+ * The json's first level of keys must represent the rows and the secend level the columns
752
+ *
753
+ * @private
754
+ * @param {TYPE.AutomationItem} json dataextension
755
+ * @param {object[][]} tabled prepped array for output in tabular format
756
+ * @returns {string} file content
757
+ */
758
+ static _generateDocMd(json, tabled) {
759
+ let output = `## ${json.key}\n\n`;
760
+ if (json.key !== json.name) {
761
+ output += `**Name** (not equal to External Key)**:** ${json.name}\n\n`;
762
+ }
763
+
764
+ output +=
765
+ `**Description:** ${json.description || 'n/a'}\n\n` +
766
+ `**Folder:** ${
767
+ json.r__folder_Path ||
768
+ '_Hidden! Could not find folder with ID ' + json.categoryId + '_'
769
+ }/\n\n`;
770
+ const automationType = { scheduled: 'Schedule', triggered: 'File Drop' };
771
+ output += `**Started by:** ${automationType[json.type] || 'Not defined'}\n\n`;
772
+ output += `**Status:** ${json.status}\n\n`;
773
+ if (json.type === 'scheduled') {
774
+ const tz =
775
+ this.definition.timeZoneDifference[
776
+ this.definition.timeZoneMapping[json?.schedule?.timezoneName]
777
+ ];
778
+
779
+ if (json.schedule?.icalRecur) {
780
+ output += `**Schedule:**\n\n`;
781
+ output += `* Start: ${json.schedule.startDate.split('T').join(' ')} ${tz}\n`;
782
+ output += `* End: ${json.schedule.endDate.split('T').join(' ')} ${tz}\n`;
783
+ output += `* Timezone: ${json.schedule.timezoneName}\n`;
784
+
785
+ const ical = {};
786
+ for (const item of json.schedule.icalRecur.split(';')) {
787
+ const temp = item.split('=');
788
+ ical[temp[0]] = temp[1];
789
+ }
790
+ const frequency = ical.FREQ.slice(0, -2).toLowerCase();
791
+
792
+ output += `* Recurrance: every ${ical.INTERVAL > 1 ? ical.INTERVAL : ''} ${
793
+ frequency === 'dai' ? 'day' : frequency
794
+ }${ical.INTERVAL > 1 ? 's' : ''} ${ical.COUNT ? `for ${ical.COUNT} times` : ''}\n`;
795
+ output += '\n';
796
+ } else if (json.schedule) {
797
+ output += `**Schedule:** Not defined\n\n`;
798
+ }
799
+ } else if (json.type === 'triggered' && json.fileTrigger) {
800
+ output += `**File Trigger:**\n\n`;
801
+ output += `* Queue Files: ${json.fileTrigger.queueFiles}\n`;
802
+ output += `* Published: ${json.fileTrigger.isPublished}\n`;
803
+ output += `* Pattern: ${json.fileTrigger.fileNamingPattern}\n`;
804
+ output += `* Folder: ${json.fileTrigger.folderLocationText}\n`;
805
+ }
806
+ if (tabled && tabled.length) {
807
+ let tableSeparator = '';
808
+ const row1 = [];
809
+ for (const column of tabled[0]) {
810
+ row1.push(
811
+ `| ${column.title}${column.description ? `<br>_${column.description}_` : ''} `
812
+ );
813
+ tableSeparator += '| --- ';
814
+ }
815
+ output += row1.join('') + `|\n${tableSeparator}|\n`;
816
+ for (let i = 1; i < tabled.length; i++) {
817
+ for (const field of tabled[i]) {
818
+ output += field ? `| _${field.i}: ${field.type}_<br>${field.name} ` : '| - ';
819
+ }
820
+ output += '|\n';
821
+ }
822
+ }
823
+ return output;
824
+ }
825
+ /**
826
+ * Saves json content to a html table in the local file system. Will create the parent directory if it does not exist.
827
+ * The json's first level of keys must represent the rows and the secend level the columns
828
+ *
829
+ * @private
830
+ * @param {string} directory directory the file will be written to
831
+ * @param {string} filename name of the file without '.json' ending
832
+ * @param {TYPE.AutomationItem} json dataextension.columns
833
+ * @param {'html'|'md'} mode html or md
834
+ * @returns {Promise.<boolean>} Promise of success of saving the file
835
+ */
836
+ static async _writeDoc(directory, filename, json, mode) {
837
+ await File.ensureDir(directory);
838
+
839
+ const tabled = [];
840
+ if (json.steps && json.steps.length) {
841
+ tabled.push(
842
+ json.steps.map((step, index) => ({
843
+ title: `Step ${index + 1}`,
844
+ description: step.name || '-',
845
+ }))
846
+ );
847
+ let maxActivities = 0;
848
+ for (const step of json.steps) {
849
+ if (step.activities.length > maxActivities) {
850
+ maxActivities = step.activities.length;
851
+ }
852
+ }
853
+ for (let activityIndex = 0; activityIndex < maxActivities; activityIndex++) {
854
+ tabled.push(
855
+ json.steps.map((step, stepIndex) =>
856
+ step.activities[activityIndex]
857
+ ? {
858
+ i: stepIndex + 1 + '.' + (activityIndex + 1),
859
+ name: step.activities[activityIndex].name,
860
+ type: step.activities[activityIndex].r__type,
861
+ }
862
+ : null
863
+ )
864
+ );
865
+ }
866
+ }
867
+ let output;
868
+ if (mode === 'md') {
869
+ output = this._generateDocMd(json, tabled);
870
+ try {
871
+ // write to disk
872
+ await File.writeToFile(directory, filename + '.automation-doc', mode, output);
873
+ } catch (ex) {
874
+ Util.logger.error(`Automation.writeDeToX(${mode}):: error | ` + ex.message);
875
+ }
876
+ }
877
+ }
878
+ /**
879
+ * Parses metadata into a readable Markdown/HTML format then saves it
880
+ *
881
+ * @param {TYPE.BuObject} buObject properties for auth
882
+ * @param {TYPE.AutomationMap} [metadata] a list of dataExtension definitions
883
+ * @returns {Promise.<void>} -
884
+ */
885
+ static async document(buObject, metadata) {
886
+ if (['md', 'both'].includes(this.properties.options.documentType)) {
887
+ if (!metadata) {
888
+ metadata = this.readBUMetadataForType(
889
+ File.normalizePath([
890
+ this.properties.directories.retrieve,
891
+ buObject.credential,
892
+ buObject.businessUnit,
893
+ ]),
894
+ true
895
+ ).automation;
896
+ }
897
+ const docPath = File.normalizePath([
898
+ this.properties.directories.retrieve,
899
+ buObject.credential,
900
+ buObject.businessUnit,
901
+ this.definition.type,
902
+ ]);
903
+ if (!metadata || !Object.keys(metadata).length) {
904
+ // as part of retrieve & manual execution we could face an empty folder
905
+ return;
906
+ }
907
+ await Promise.all(
908
+ Object.keys(metadata).map((key) => {
909
+ this._writeDoc(docPath + '/', key, metadata[key], 'md');
910
+ return metadata[key];
911
+ })
912
+ );
913
+ }
914
+ }
915
+ /**
916
+ * should return only the json for all but asset, query and script that are saved as multiple files
917
+ * additionally, the documentation for dataExtension and automation should be returned
918
+ *
919
+ * @param {string[]} keyArr customerkey of the metadata
920
+ * @returns {string[]} list of all files that need to be committed in a flat array ['path/file1.ext', 'path/file2.ext']
921
+ */
922
+ static getFilesToCommit(keyArr) {
923
+ if (!this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
924
+ // document automation is not active upon retrieve, run default method instead
925
+ return super.getFilesToCommit(keyArr);
926
+ } else {
927
+ // document automation is active. assume we want to commit the MD file as well
928
+ const path = File.normalizePath([
929
+ this.properties.directories.retrieve,
930
+ this.buObject.credential,
931
+ this.buObject.businessUnit,
932
+ this.definition.type,
933
+ ]);
934
+
935
+ const fileList = keyArr.flatMap((key) => [
936
+ File.normalizePath([path, `${key}.${this.definition.type}-meta.json`]),
937
+ File.normalizePath([path, `${key}.${this.definition.type}-doc.md`]),
938
+ ]);
939
+ return fileList;
940
+ }
941
+ }
840
942
  }
841
943
 
842
944
  // Assign definition to static attributes
843
945
  Automation.definition = Definitions.automation;
844
- Automation.cache = {};
845
- /**
846
- * @type {Util.ET_Client}
847
- */
848
- Automation.client = undefined;
849
946
 
850
947
  module.exports = Automation;