mcdev 3.1.1 → 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 (135) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +5 -1
  3. package/.github/ISSUE_TEMPLATE/task.md +1 -1
  4. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  5. package/.github/dependabot.yml +14 -0
  6. package/.github/workflows/code-analysis.yml +57 -0
  7. package/.husky/commit-msg +10 -0
  8. package/.husky/post-checkout +5 -0
  9. package/.husky/pre-commit +2 -1
  10. package/.prettierrc +8 -0
  11. package/.vscode/settings.json +1 -1
  12. package/LICENSE +2 -2
  13. package/README.md +134 -45
  14. package/boilerplate/config.json +5 -11
  15. package/boilerplate/files/.prettierrc +8 -0
  16. package/boilerplate/files/.vscode/extensions.json +0 -1
  17. package/boilerplate/files/.vscode/settings.json +28 -2
  18. package/boilerplate/files/README.md +2 -2
  19. package/boilerplate/forcedUpdates.json +10 -0
  20. package/boilerplate/npm-dependencies.json +5 -5
  21. package/docs/dist/documentation.md +2795 -1724
  22. package/jsconfig.json +1 -1
  23. package/lib/Builder.js +166 -75
  24. package/lib/Deployer.js +244 -96
  25. package/lib/MetadataTypeDefinitions.js +2 -0
  26. package/lib/MetadataTypeInfo.js +2 -0
  27. package/lib/Retriever.js +61 -84
  28. package/lib/cli.js +133 -25
  29. package/lib/index.js +242 -563
  30. package/lib/metadataTypes/AccountUser.js +101 -95
  31. package/lib/metadataTypes/Asset.js +677 -248
  32. package/lib/metadataTypes/AttributeGroup.js +23 -12
  33. package/lib/metadataTypes/Automation.js +456 -357
  34. package/lib/metadataTypes/Campaign.js +33 -93
  35. package/lib/metadataTypes/ContentArea.js +31 -11
  36. package/lib/metadataTypes/DataExtension.js +391 -376
  37. package/lib/metadataTypes/DataExtensionField.js +131 -54
  38. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  39. package/lib/metadataTypes/DataExtract.js +67 -50
  40. package/lib/metadataTypes/DataExtractType.js +14 -8
  41. package/lib/metadataTypes/Discovery.js +21 -16
  42. package/lib/metadataTypes/Email.js +32 -12
  43. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  44. package/lib/metadataTypes/EventDefinition.js +69 -47
  45. package/lib/metadataTypes/FileTransfer.js +78 -54
  46. package/lib/metadataTypes/Filter.js +11 -4
  47. package/lib/metadataTypes/Folder.js +149 -117
  48. package/lib/metadataTypes/FtpLocation.js +14 -8
  49. package/lib/metadataTypes/ImportFile.js +69 -69
  50. package/lib/metadataTypes/Interaction.js +19 -4
  51. package/lib/metadataTypes/List.js +54 -13
  52. package/lib/metadataTypes/MetadataType.js +687 -479
  53. package/lib/metadataTypes/MobileCode.js +46 -0
  54. package/lib/metadataTypes/MobileKeyword.js +114 -0
  55. package/lib/metadataTypes/Query.js +204 -103
  56. package/lib/metadataTypes/Role.js +76 -61
  57. package/lib/metadataTypes/Script.js +146 -82
  58. package/lib/metadataTypes/SetDefinition.js +20 -8
  59. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  60. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  61. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  62. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  63. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  64. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  65. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  66. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  67. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  68. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  69. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  70. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  72. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  73. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  74. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  75. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  76. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  77. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  78. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  79. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  80. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  81. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  82. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  83. package/lib/retrieveChangelog.js +7 -6
  84. package/lib/util/auth.js +117 -0
  85. package/lib/util/businessUnit.js +55 -66
  86. package/lib/util/cache.js +194 -0
  87. package/lib/util/cli.js +90 -116
  88. package/lib/util/config.js +302 -0
  89. package/lib/util/devops.js +240 -50
  90. package/lib/util/file.js +120 -191
  91. package/lib/util/init.config.js +195 -69
  92. package/lib/util/init.git.js +45 -50
  93. package/lib/util/init.js +72 -59
  94. package/lib/util/init.npm.js +48 -39
  95. package/lib/util/util.js +280 -564
  96. package/package.json +44 -33
  97. package/test/dataExtension.test.js +152 -0
  98. package/test/mockRoot/.mcdev-auth.json +8 -0
  99. package/test/mockRoot/.mcdevrc.json +67 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  105. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  106. package/test/query.test.js +149 -0
  107. package/test/resourceFactory.js +142 -0
  108. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  109. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  110. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  111. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  112. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  113. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  114. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  115. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  116. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  117. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  118. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  119. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  120. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  121. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  122. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  123. package/test/resources/9999999/query/build-expected.json +8 -0
  124. package/test/resources/9999999/query/get-expected.json +11 -0
  125. package/test/resources/9999999/query/patch-expected.json +11 -0
  126. package/test/resources/9999999/query/post-expected.json +11 -0
  127. package/test/resources/9999999/query/template-expected.json +8 -0
  128. package/test/resources/auth.json +32 -0
  129. package/test/resources/rest404-response.json +5 -0
  130. package/test/resources/retrieve-response.xml +21 -0
  131. package/test/utils.js +107 -0
  132. package/types/mcdev.d.js +301 -0
  133. package/CHANGELOG.md +0 -126
  134. package/PULL_REQUEST_TEMPLATE.md +0 -19
  135. 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,54 +124,42 @@ 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} variables 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
- static async retrieveAsTemplate(templateDir, name, variables) {
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
- );
133
+ static async retrieveAsTemplate(templateDir, name, templateVariables) {
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;
152
+ let originalKey;
242
153
  // if parsing fails, we should just save what we get
243
154
  try {
244
155
  const parsedDetails = this.parseMetadata(details);
156
+ originalKey = parsedDetails[this.definition.keyField];
245
157
  if (parsedDetails !== null) {
246
158
  val = JSON.parse(
247
- Util.replaceByObject(JSON.stringify(parsedDetails), variables)
159
+ Util.replaceByObject(JSON.stringify(parsedDetails), templateVariables)
248
160
  );
249
161
  }
250
- } catch (ex) {
162
+ } catch {
251
163
  val = JSON.parse(JSON.stringify(details));
252
164
  }
253
165
  if (val === null) {
@@ -257,14 +169,12 @@ class Automation extends MetadataType {
257
169
  }
258
170
  // remove all fields not listed in Definition for templating
259
171
  this.keepTemplateFields(val);
260
- File.writeJSONToFile(
172
+ await File.writeJSONToFile(
261
173
  [templateDir, this.definition.type].join('/'),
262
- val[this.definition.keyField] + '.' + this.definition.type + '-meta',
174
+ originalKey + '.' + this.definition.type + '-meta',
263
175
  val
264
176
  );
265
- Util.logger.info(
266
- `Automation.retrieveAsTemplate:: Written Metadata to filesystem (${name})`
267
- );
177
+ Util.logger.info(`- templated ${this.definition.type}: ${name}`);
268
178
  return { metadata: val, type: this.definition.type };
269
179
  } else if (results) {
270
180
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
@@ -276,26 +186,21 @@ class Automation extends MetadataType {
276
186
  }
277
187
  /**
278
188
  * manages post retrieve steps
279
- * @param {AutomationItem} metadata a single automation
280
- * @param {string} [_] unused
281
- * @param {Boolean} [isTemplating] signals that we are retrieving templates
282
- * @returns {AutomationItem} metadata
189
+ *
190
+ * @param {TYPE.AutomationItem} metadata a single automation
191
+ * @returns {TYPE.AutomationItem} metadata
283
192
  */
284
- static postRetrieveTasks(metadata, _, isTemplating) {
285
- // if retrieving template, replace the name with customer key if that wasn't already the case
286
- if (isTemplating) {
287
- const warningMsg = null;
288
- this.overrideKeyWithName(metadata, warningMsg);
289
- }
193
+ static postRetrieveTasks(metadata) {
290
194
  return this.parseMetadata(metadata);
291
195
  }
292
196
 
293
197
  /**
294
198
  * Deploys automation - the saved file is the original one due to large differences required for deployment
295
- * @param {AutomationMap} metadata metadata mapped by their keyField
199
+ *
200
+ * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
296
201
  * @param {string} targetBU name/shorthand of target businessUnit for mapping
297
202
  * @param {string} retrieveDir directory where metadata after deploy should be saved
298
- * @returns {Promise<AutomationMap>} Promise
203
+ * @returns {Promise.<TYPE.AutomationMap>} Promise
299
204
  */
300
205
  static async deploy(metadata, targetBU, retrieveDir) {
301
206
  const orignalMetadata = JSON.parse(JSON.stringify(metadata));
@@ -307,7 +212,8 @@ class Automation extends MetadataType {
307
212
 
308
213
  /**
309
214
  * Creates a single automation
310
- * @param {AutomationItem} metadata single metadata entry
215
+ *
216
+ * @param {TYPE.AutomationItem} metadata single metadata entry
311
217
  * @returns {Promise} Promise
312
218
  */
313
219
  static create(metadata) {
@@ -317,8 +223,9 @@ class Automation extends MetadataType {
317
223
 
318
224
  /**
319
225
  * Updates a single automation
320
- * @param {AutomationItem} metadata single metadata entry
321
- * @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
322
229
  * @returns {Promise} Promise
323
230
  */
324
231
  static update(metadata, metadataBefore) {
@@ -329,14 +236,14 @@ class Automation extends MetadataType {
329
236
 
330
237
  /**
331
238
  * Gets executed before deploying metadata
332
- * @param {AutomationItem} metadata metadata mapped by their keyField
333
- * @returns {Promise<AutomationItem>} Promise
239
+ *
240
+ * @param {TYPE.AutomationItem} metadata metadata mapped by their keyField
241
+ * @returns {Promise.<TYPE.AutomationItem>} Promise
334
242
  */
335
243
  static async preDeployTasks(metadata) {
336
244
  if (this.validateDeployMetadata(metadata)) {
337
245
  try {
338
- metadata.categoryId = Util.getFromCache(
339
- this.cache,
246
+ metadata.categoryId = cache.searchForField(
340
247
  'folder',
341
248
  metadata.r__folder_Path,
342
249
  'Path',
@@ -344,7 +251,7 @@ class Automation extends MetadataType {
344
251
  );
345
252
  if (metadata.r__folder_Path !== 'my automations') {
346
253
  Util.logger.warn(
347
- `Automation '${
254
+ ` - Automation '${
348
255
  metadata[this.definition.nameField]
349
256
  }' is located in subfolder ${
350
257
  metadata.r__folder_Path
@@ -352,12 +259,12 @@ class Automation extends MetadataType {
352
259
  );
353
260
  }
354
261
  delete metadata.r__folder_Path;
355
- } catch (ex) {
262
+ } catch {
356
263
  throw new Error(
357
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.`
358
265
  );
359
266
  }
360
- if (metadata.type === 'scheduled' && metadata.schedule && metadata.schedule.startDate) {
267
+ if (metadata.type === 'scheduled' && metadata?.schedule?.startDate) {
361
268
  // Starting Source == 'Schedule'
362
269
 
363
270
  delete metadata.schedule.rangeTypeId;
@@ -397,26 +304,33 @@ class Automation extends MetadataType {
397
304
  delete metadata.schedule;
398
305
  delete metadata.type;
399
306
  let i = 0;
400
- for (const step of metadata.steps) {
401
- for (const activity of step.activities) {
402
- if (activity.name && this.definition.dependencies.includes(activity.r__type)) {
403
- // automations can have empty placeholder for activities with only their type defined
404
- activity.activityObjectId = Util.getFromCache(
405
- this.cache,
406
- activity.r__type,
407
- activity.name,
408
- Definitions[activity.r__type].nameField,
409
- Definitions[activity.r__type].idField
410
- );
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;
411
327
  }
412
- activity.objectTypeId = this.definition.activityTypeMapping[activity.r__type];
413
- delete activity.r__type;
328
+ step.annotation = step.name;
329
+ step.stepNumber = i;
330
+ delete step.name;
331
+ delete step.step;
332
+ i++;
414
333
  }
415
- step.annotation = step.name;
416
- step.stepNumber = i;
417
- delete step.name;
418
- delete step.step;
419
- i++;
420
334
  }
421
335
  return metadata;
422
336
  } else {
@@ -426,52 +340,62 @@ class Automation extends MetadataType {
426
340
  /**
427
341
  * Validates the automation to be sure it can be deployed.
428
342
  * Whitelisted Activites are deployed but require configuration
429
- * @param {AutomationItem} metadata single automation record
430
- * @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
431
346
  */
432
347
  static validateDeployMetadata(metadata) {
433
348
  let deployable = true;
349
+ const errors = [];
434
350
  if (metadata.steps) {
351
+ let stepNumber = 0;
435
352
  for (const step of metadata.steps) {
353
+ stepNumber++;
354
+ let displayOrder = 0;
355
+
436
356
  for (const activity of step.activities) {
357
+ displayOrder++;
437
358
  // check if manual deploy required. if so then log warning
438
359
  if (this.definition.manualDeployTypes.includes(activity.r__type)) {
439
360
  Util.logger.warn(
440
- `Automation '${
441
- metadata.name
442
- }' requires additional manual configuration: '${
443
- activity.name
444
- }' in step ${step.stepNumber || step.step}.${activity.displayOrder}`
361
+ `- ${this.definition.type} '${metadata.name}' requires additional manual configuration: '${activity.name}' in step ${stepNumber}.${displayOrder}`
445
362
  );
446
363
  }
447
364
  // cannot deploy because it is not supported
448
365
  else if (!this.definition.dependencies.includes(activity.r__type)) {
449
- Util.logger.error(
450
- `Automation '${
451
- metadata.name
452
- }' cannot be deployed as the following activity is not supported: '${
453
- activity.name
454
- }' in step ${step.stepNumber || step.step}.${activity.displayOrder}`
366
+ errors.push(
367
+ ` not supported ${activity.r__type} activity '${activity.name}' in step ${stepNumber}.${displayOrder}`
455
368
  );
456
369
  deployable = false;
457
370
  }
458
371
  }
459
372
  }
460
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
+ }
461
384
  return deployable;
462
385
  }
463
386
 
464
387
  /**
465
388
  * Gets executed after deployment of metadata type
466
- * @param {AutomationMap} metadata metadata mapped by their keyField
467
- * @param {AutomationMap} originalMetadata metadata to be updated (contains additioanl fields)
468
- * @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>} -
469
393
  */
470
394
  static async postDeployTasks(metadata, originalMetadata) {
471
395
  for (const key in metadata) {
472
396
  // need to put schedule on here if status is scheduled
473
397
 
474
- if (originalMetadata[key] && originalMetadata[key].type === 'scheduled') {
398
+ if (originalMetadata[key]?.type === 'scheduled') {
475
399
  // Starting Source == 'Schedule': Try starting the automation
476
400
  if (originalMetadata[key].status === 'Scheduled') {
477
401
  let schedule = null;
@@ -479,70 +403,45 @@ class Automation extends MetadataType {
479
403
  schedule = this._buildSchedule(originalMetadata[key].schedule);
480
404
  } catch (ex) {
481
405
  Util.logger.error(
482
- `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}`
483
407
  );
484
408
  }
485
409
  if (schedule !== null) {
486
410
  try {
487
- await Util.retryOnError(
488
- `Retrying ${this.definition.type}`,
489
- async () => {
490
- await new Promise((resolve, reject) => {
491
- this.client.SoapClient.schedule(
492
- 'Automation',
493
- schedule,
494
- {
495
- Interaction: {
496
- ObjectID: metadata[key].id,
497
- },
498
- },
499
- 'start',
500
- null,
501
- (error, response) => {
502
- if (
503
- error ||
504
- (response.body.Results &&
505
- response.body.Results[0] &&
506
- response.body.Results[0].StatusCode &&
507
- response.body.Results[0].StatusCode ===
508
- 'Error')
509
- ) {
510
- reject(
511
- error ||
512
- response.body.Results[0].StatusMessage
513
- );
514
- } else {
515
- resolve(response.body.Results);
516
- }
517
- }
518
- );
519
- });
520
- const intervalString =
521
- (schedule._interval > 1 ? `${schedule._interval} ` : '') +
522
- (schedule.RecurrenceType === 'Daily'
523
- ? 'Day'
524
- : schedule.RecurrenceType.slice(0, -2) +
525
- (schedule._interval > 1 ? 's' : ''));
526
- Util.logger.warn(
527
- `Automation '${
528
- originalMetadata[key].name
529
- }' deployed Active: runs every ${intervalString} starting ${
530
- schedule._StartDateTime
531
- .split('T')
532
- .join(' ')
533
- .split('.')[0]
534
- } ${schedule._timezoneString}`
535
- );
536
- }
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}`
537
434
  );
538
435
  } catch (ex) {
539
436
  Util.logger.error(
540
- `Could not start automation '${originalMetadata[key].name}': ${ex.message}`
437
+ `- Could not start scheduled automation '${originalMetadata[key].name}': ${ex.message}`
541
438
  );
542
439
  }
543
440
  }
544
441
  } else {
545
- Util.logger.warn(`Automation '${originalMetadata[key].name}' deployed Paused`);
442
+ Util.logger.warn(
443
+ ` - scheduled automation '${originalMetadata[key].name}' deployed Paused`
444
+ );
546
445
  }
547
446
  }
548
447
  if (metadata[key].startSource) {
@@ -559,14 +458,14 @@ class Automation extends MetadataType {
559
458
 
560
459
  /**
561
460
  * parses retrieved Metadata before saving
562
- * @param {AutomationItem} metadata a single automation definition
563
- * @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
564
464
  */
565
465
  static parseMetadata(metadata) {
566
466
  // automations are often skipped due to lack of support.
567
467
  try {
568
- metadata.r__folder_Path = Util.getFromCache(
569
- this.cache,
468
+ metadata.r__folder_Path = cache.searchForField(
570
469
  'folder',
571
470
  metadata.categoryId,
572
471
  'ID',
@@ -575,7 +474,9 @@ class Automation extends MetadataType {
575
474
  delete metadata.categoryId;
576
475
  if (metadata.r__folder_Path !== 'my automations') {
577
476
  Util.logger.verbose(
578
- `Automation '${metadata[this.definition.nameField]}' is located in subfolder ${
477
+ `- automation '${
478
+ metadata[this.definition.nameField]
479
+ }' is located in subfolder ${
579
480
  metadata.r__folder_Path
580
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.`
581
482
  );
@@ -583,13 +484,13 @@ class Automation extends MetadataType {
583
484
  } catch (ex) {
584
485
  // * don't exit on missing folder for automation
585
486
  Util.logger.warn(
586
- `${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
487
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
587
488
  ex.message
588
489
  }`
589
490
  );
590
491
  }
591
492
  try {
592
- if (metadata.type === 'scheduled' && metadata.schedule && metadata.schedule.startDate) {
493
+ if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
593
494
  // Starting Source == 'Schedule'
594
495
 
595
496
  try {
@@ -597,24 +498,21 @@ class Automation extends MetadataType {
597
498
  // if we found the id in our list, remove the redundant data
598
499
  delete metadata.schedule.timezoneId;
599
500
  }
600
- } catch (ex) {
501
+ } catch {
601
502
  Util.logger.debug(
602
- `Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
503
+ `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
603
504
  );
604
505
  }
605
506
  try {
606
507
  // type 'Running' is temporary status only, overwrite with Scheduled for storage.
607
508
  if (metadata.type === 'scheduled' && metadata.status === 'Running') {
608
- metadata.status === 'Scheduled';
509
+ metadata.status = 'Scheduled';
609
510
  }
610
- } catch (ex) {
611
- Util.metadataLogger(
612
- 'error',
613
- this.definition.type,
614
- 'parseMetadata',
615
- `${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.`
616
514
  );
617
- return null;
515
+ return;
618
516
  }
619
517
  } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
620
518
  // Starting Source == 'File Drop'
@@ -642,32 +540,30 @@ class Automation extends MetadataType {
642
540
  }
643
541
  // / if managed by cache we can update references to support deployment
644
542
  else if (
645
- Definitions[activity.r__type] &&
646
- Definitions[activity.r__type]['idField'] &&
647
- this.cache[activity.r__type]
543
+ Definitions[activity.r__type]?.['idField'] &&
544
+ cache.getCache(this.buObject.mid)[activity.r__type]
648
545
  ) {
649
546
  try {
650
- activity.activityObjectId = Util.getFromCache(
651
- this.cache,
547
+ activity.activityObjectId = cache.searchForField(
652
548
  activity.r__type,
653
549
  activity.activityObjectId,
654
550
  Definitions[activity.r__type].idField,
655
551
  Definitions[activity.r__type].nameField
656
552
  );
657
- } catch (e) {
553
+ } catch (ex) {
658
554
  // getFromCache throws error where the dependent metadata is not found
659
- Util.logger.error(
660
- `Missing ${activity.r__type} activity '${activity.name}'` +
555
+ Util.logger.warn(
556
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
661
557
  ` in step ${step.stepNumber || step.step}.${
662
558
  activity.displayOrder
663
559
  }` +
664
- ` of Automation '${metadata.name}' (${e.message})`
560
+ ` of Automation '${metadata.name}' (${ex.message})`
665
561
  );
666
562
  return null;
667
563
  }
668
564
  } else {
669
- Util.logger.error(
670
- `Missing ${activity.r__type} activity '${activity.name}'` +
565
+ Util.logger.warn(
566
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
671
567
  ` in step ${step.stepNumber || step.step}.${
672
568
  activity.displayOrder
673
569
  }` +
@@ -675,19 +571,21 @@ class Automation extends MetadataType {
675
571
  );
676
572
  return null;
677
573
  }
678
- } catch (ex) {
574
+ } catch {
679
575
  Util.logger.warn(
680
- `Excluding automation '${metadata.name}' from retrieve (ObjectType ${activity.objectTypeId} is unknown)`
576
+ ` - Excluding automation '${metadata.name}' from retrieve (ObjectType ${activity.objectTypeId} is unknown)`
681
577
  );
682
578
  return null;
683
579
  }
684
580
  }
581
+ delete step.stepNumber;
582
+ delete step.step;
685
583
  }
686
584
  }
687
585
  return JSON.parse(JSON.stringify(metadata));
688
586
  } catch (ex) {
689
587
  Util.logger.warn(
690
- `${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
588
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
691
589
  ex.message
692
590
  }`
693
591
  );
@@ -698,12 +596,13 @@ class Automation extends MetadataType {
698
596
  /**
699
597
  * Builds a schedule object to be used for scheduling an automation
700
598
  * based on combination of ical string and start/end dates.
701
- * @param {AutomationSchedule} scheduleObject child of automation metadata used for scheduling
702
- * @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)
703
602
  */
704
603
  static _buildSchedule(scheduleObject) {
705
604
  /**
706
- * @type {AutomationScheduleSoap}
605
+ * @type {TYPE.AutomationScheduleSoap}
707
606
  */
708
607
  const schedule = { Recurrence: {}, TimeZone: { IDSpecified: true } };
709
608
  // build recurrence
@@ -749,7 +648,7 @@ class Automation extends MetadataType {
749
648
  this.definition.timeZoneMapping[scheduleObject.timezoneName];
750
649
  } else {
751
650
  Util.logger.error(
752
- `Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
651
+ `- Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
753
652
  );
754
653
  }
755
654
  schedule.TimeZone.ID = scheduleObject.timezoneId;
@@ -767,25 +666,38 @@ class Automation extends MetadataType {
767
666
  const scheduledDate = new Date(inputStartDateString);
768
667
  const futureDate = new Date();
769
668
 
770
- if (keyStem === 'Dai') {
771
- // keep time from template and start today if possible
772
- if (scheduledDate.getHours() <= futureDate.getHours()) {
773
- // hour on template has already passed today, start tomorrow
774
- 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;
775
689
  }
776
- futureDate.setHours(scheduledDate.getHours());
777
- futureDate.setMinutes(scheduledDate.getMinutes());
778
- } else if (keyStem === 'Hour') {
779
- // keep minute and start next possible hour
780
- if (scheduledDate.getMinutes() <= futureDate.getMinutes()) {
781
- 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;
782
699
  }
783
- futureDate.setMinutes(scheduledDate.getMinutes());
784
- } else if (keyStem === 'Minute') {
785
- // schedule in next 15 minutes randomly to avoid that all automations run at exactly
786
- // earliest start 1 minute from now
787
- // the same time which would slow performance
788
- futureDate.setMinutes(futureDate.getMinutes() + 1 + Math.ceil(Math.random() * 15));
700
+ // No default
789
701
  }
790
702
  // return time as Dateobject
791
703
  schedule.StartDateTime = futureDate;
@@ -822,12 +734,10 @@ class Automation extends MetadataType {
822
734
  */
823
735
  static _calcTime(offsetServer, dateInput, offsetInput) {
824
736
  // get UTC time in msec
825
- let utc;
826
- if ('string' === typeof dateInput) {
827
- utc = new Date(dateInput + offsetInput).getTime();
828
- } else {
829
- utc = dateInput.getTime();
830
- }
737
+ const utc =
738
+ 'string' === typeof dateInput
739
+ ? new Date(dateInput + offsetInput).getTime()
740
+ : dateInput.getTime();
831
741
 
832
742
  // create new Date object reflecting SFMC's servertime
833
743
  const dateServer = new Date(utc + 3600000 * offsetServer);
@@ -835,14 +745,203 @@ class Automation extends MetadataType {
835
745
  // return time as a string without trailing "Z"
836
746
  return dateServer.toISOString().slice(0, -1);
837
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
+ }
838
942
  }
839
943
 
840
944
  // Assign definition to static attributes
841
945
  Automation.definition = Definitions.automation;
842
- Automation.cache = {};
843
- /**
844
- * @type {Util.ET_Client}
845
- */
846
- Automation.client = undefined;
847
946
 
848
947
  module.exports = Automation;