mcdev 3.1.3 → 4.0.1

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 +30 -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 +2807 -1730
  21. package/jsconfig.json +1 -1
  22. package/lib/Builder.js +171 -74
  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 +117 -103
  30. package/lib/metadataTypes/Asset.js +705 -255
  31. package/lib/metadataTypes/AttributeGroup.js +23 -12
  32. package/lib/metadataTypes/Automation.js +489 -392
  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 +664 -454
  52. package/lib/metadataTypes/MobileCode.js +46 -0
  53. package/lib/metadataTypes/MobileKeyword.js +114 -0
  54. package/lib/metadataTypes/Query.js +206 -105
  55. package/lib/metadataTypes/Role.js +76 -61
  56. package/lib/metadataTypes/Script.js +147 -83
  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 +250 -50
  89. package/lib/util/file.js +141 -201
  90. package/lib/util/init.config.js +208 -75
  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 +45 -34
  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,73 +102,54 @@ 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) {
192
- resultsConverted[m.CustomerKey] = {
193
- id: m.ObjectID,
194
- key: m.CustomerKey,
195
- name: m.Name,
196
- };
115
+ if (Array.isArray(results?.Results)) {
116
+ for (const m of results.Results) {
117
+ resultsConverted[m.CustomerKey] = {
118
+ id: m.ObjectID,
119
+ key: m.CustomerKey,
120
+ name: m.Name,
121
+ };
122
+ }
197
123
  }
198
124
  return { metadata: resultsConverted, type: this.definition.type };
199
125
  }
200
126
 
201
127
  /**
202
128
  * Retrieve a specific Automation Definition by Name
129
+ *
203
130
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
204
131
  * @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
132
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
133
+ * @returns {Promise.<TYPE.AutomationItemObj>} Promise of metadata
207
134
  */
208
135
  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
- );
136
+ const results = await this.client.soap.retrieve('Program', ['ObjectID', 'Name'], {
137
+ filter: {
138
+ leftOperand: 'Name',
139
+ operator: 'equals',
140
+ rightOperand: name,
141
+ },
228
142
  });
229
- if (results && results[0]) {
143
+ if (Array.isArray(results?.Results)) {
230
144
  // 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);
145
+ const metadata = results.Results.find((item) => item.Name === name);
232
146
  if (!metadata) {
233
147
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
234
148
  return;
235
149
  }
236
- const details = (
237
- await this.client.RestClient.get({
238
- uri: '/automation/v1/automations/' + metadata.ObjectID,
239
- })
240
- ).body;
150
+ const details = await this.client.rest.get(
151
+ '/automation/v1/automations/' + metadata.ObjectID
152
+ );
241
153
  let val = null;
242
154
  let originalKey;
243
155
  // if parsing fails, we should just save what we get
@@ -249,7 +161,7 @@ class Automation extends MetadataType {
249
161
  Util.replaceByObject(JSON.stringify(parsedDetails), templateVariables)
250
162
  );
251
163
  }
252
- } catch (ex) {
164
+ } catch {
253
165
  val = JSON.parse(JSON.stringify(details));
254
166
  }
255
167
  if (val === null) {
@@ -259,14 +171,12 @@ class Automation extends MetadataType {
259
171
  }
260
172
  // remove all fields not listed in Definition for templating
261
173
  this.keepTemplateFields(val);
262
- File.writeJSONToFile(
174
+ await File.writeJSONToFile(
263
175
  [templateDir, this.definition.type].join('/'),
264
176
  originalKey + '.' + this.definition.type + '-meta',
265
177
  val
266
178
  );
267
- Util.logger.info(
268
- `Automation.retrieveAsTemplate:: Written Metadata to filesystem (${name})`
269
- );
179
+ Util.logger.info(`- templated ${this.definition.type}: ${name}`);
270
180
  return { metadata: val, type: this.definition.type };
271
181
  } else if (results) {
272
182
  Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
@@ -278,26 +188,21 @@ class Automation extends MetadataType {
278
188
  }
279
189
  /**
280
190
  * 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
191
+ *
192
+ * @param {TYPE.AutomationItem} metadata a single automation
193
+ * @returns {TYPE.AutomationItem} metadata
285
194
  */
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
- }
195
+ static postRetrieveTasks(metadata) {
292
196
  return this.parseMetadata(metadata);
293
197
  }
294
198
 
295
199
  /**
296
200
  * 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
201
+ *
202
+ * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
298
203
  * @param {string} targetBU name/shorthand of target businessUnit for mapping
299
204
  * @param {string} retrieveDir directory where metadata after deploy should be saved
300
- * @returns {Promise<AutomationMap>} Promise
205
+ * @returns {Promise.<TYPE.AutomationMap>} Promise
301
206
  */
302
207
  static async deploy(metadata, targetBU, retrieveDir) {
303
208
  const orignalMetadata = JSON.parse(JSON.stringify(metadata));
@@ -309,7 +214,8 @@ class Automation extends MetadataType {
309
214
 
310
215
  /**
311
216
  * Creates a single automation
312
- * @param {AutomationItem} metadata single metadata entry
217
+ *
218
+ * @param {TYPE.AutomationItem} metadata single metadata entry
313
219
  * @returns {Promise} Promise
314
220
  */
315
221
  static create(metadata) {
@@ -319,8 +225,9 @@ class Automation extends MetadataType {
319
225
 
320
226
  /**
321
227
  * Updates a single automation
322
- * @param {AutomationItem} metadata single metadata entry
323
- * @param {AutomationItem} metadataBefore metadata mapped by their keyField
228
+ *
229
+ * @param {TYPE.AutomationItem} metadata single metadata entry
230
+ * @param {TYPE.AutomationItem} metadataBefore metadata mapped by their keyField
324
231
  * @returns {Promise} Promise
325
232
  */
326
233
  static update(metadata, metadataBefore) {
@@ -331,14 +238,14 @@ class Automation extends MetadataType {
331
238
 
332
239
  /**
333
240
  * Gets executed before deploying metadata
334
- * @param {AutomationItem} metadata metadata mapped by their keyField
335
- * @returns {Promise<AutomationItem>} Promise
241
+ *
242
+ * @param {TYPE.AutomationItem} metadata metadata mapped by their keyField
243
+ * @returns {Promise.<TYPE.AutomationItem>} Promise
336
244
  */
337
245
  static async preDeployTasks(metadata) {
338
246
  if (this.validateDeployMetadata(metadata)) {
339
247
  try {
340
- metadata.categoryId = Util.getFromCache(
341
- this.cache,
248
+ metadata.categoryId = cache.searchForField(
342
249
  'folder',
343
250
  metadata.r__folder_Path,
344
251
  'Path',
@@ -346,7 +253,7 @@ class Automation extends MetadataType {
346
253
  );
347
254
  if (metadata.r__folder_Path !== 'my automations') {
348
255
  Util.logger.warn(
349
- `Automation '${
256
+ ` - Automation '${
350
257
  metadata[this.definition.nameField]
351
258
  }' is located in subfolder ${
352
259
  metadata.r__folder_Path
@@ -354,12 +261,12 @@ class Automation extends MetadataType {
354
261
  );
355
262
  }
356
263
  delete metadata.r__folder_Path;
357
- } catch (ex) {
264
+ } catch {
358
265
  throw new Error(
359
266
  `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
267
  );
361
268
  }
362
- if (metadata.type === 'scheduled' && metadata.schedule && metadata.schedule.startDate) {
269
+ if (metadata.type === 'scheduled' && metadata?.schedule?.startDate) {
363
270
  // Starting Source == 'Schedule'
364
271
 
365
272
  delete metadata.schedule.rangeTypeId;
@@ -399,26 +306,33 @@ class Automation extends MetadataType {
399
306
  delete metadata.schedule;
400
307
  delete metadata.type;
401
308
  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
- );
309
+ if (metadata.steps) {
310
+ for (const step of metadata.steps) {
311
+ let displayOrder = 0;
312
+ for (const activity of step.activities) {
313
+ activity.displayOrder = ++displayOrder;
314
+ if (
315
+ activity.name &&
316
+ this.definition.dependencies.includes(activity.r__type)
317
+ ) {
318
+ // automations can have empty placeholder for activities with only their type defined
319
+ activity.activityObjectId = cache.searchForField(
320
+ activity.r__type,
321
+ activity.name,
322
+ Definitions[activity.r__type].nameField,
323
+ Definitions[activity.r__type].idField
324
+ );
325
+ }
326
+ activity.objectTypeId =
327
+ this.definition.activityTypeMapping[activity.r__type];
328
+ delete activity.r__type;
413
329
  }
414
- activity.objectTypeId = this.definition.activityTypeMapping[activity.r__type];
415
- delete activity.r__type;
330
+ step.annotation = step.name;
331
+ step.stepNumber = i;
332
+ delete step.name;
333
+ delete step.step;
334
+ i++;
416
335
  }
417
- step.annotation = step.name;
418
- step.stepNumber = i;
419
- delete step.name;
420
- delete step.step;
421
- i++;
422
336
  }
423
337
  return metadata;
424
338
  } else {
@@ -428,52 +342,62 @@ class Automation extends MetadataType {
428
342
  /**
429
343
  * Validates the automation to be sure it can be deployed.
430
344
  * 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
345
+ *
346
+ * @param {TYPE.AutomationItem} metadata single automation record
347
+ * @returns {boolean} result if automation can be deployed based on steps
433
348
  */
434
349
  static validateDeployMetadata(metadata) {
435
350
  let deployable = true;
351
+ const errors = [];
436
352
  if (metadata.steps) {
353
+ let stepNumber = 0;
437
354
  for (const step of metadata.steps) {
355
+ stepNumber++;
356
+ let displayOrder = 0;
357
+
438
358
  for (const activity of step.activities) {
359
+ displayOrder++;
439
360
  // check if manual deploy required. if so then log warning
440
361
  if (this.definition.manualDeployTypes.includes(activity.r__type)) {
441
362
  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}`
363
+ `- ${this.definition.type} '${metadata.name}' requires additional manual configuration: '${activity.name}' in step ${stepNumber}.${displayOrder}`
447
364
  );
448
365
  }
449
366
  // cannot deploy because it is not supported
450
367
  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}`
368
+ errors.push(
369
+ ` not supported ${activity.r__type} activity '${activity.name}' in step ${stepNumber}.${displayOrder}`
457
370
  );
458
371
  deployable = false;
459
372
  }
460
373
  }
461
374
  }
462
375
  }
376
+ if (!deployable) {
377
+ Util.logger.error(
378
+ ` ☇ skipping ${this.definition.type} ${metadata[this.definition.keyField]} / ${
379
+ metadata[this.definition.nameField]
380
+ }:`
381
+ );
382
+ for (const error of errors) {
383
+ Util.logger.error(error);
384
+ }
385
+ }
463
386
  return deployable;
464
387
  }
465
388
 
466
389
  /**
467
390
  * 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>} -
391
+ *
392
+ * @param {TYPE.AutomationMap} metadata metadata mapped by their keyField
393
+ * @param {TYPE.AutomationMap} originalMetadata metadata to be updated (contains additioanl fields)
394
+ * @returns {Promise.<void>} -
471
395
  */
472
396
  static async postDeployTasks(metadata, originalMetadata) {
473
397
  for (const key in metadata) {
474
398
  // need to put schedule on here if status is scheduled
475
399
 
476
- if (originalMetadata[key] && originalMetadata[key].type === 'scheduled') {
400
+ if (originalMetadata[key]?.type === 'scheduled') {
477
401
  // Starting Source == 'Schedule': Try starting the automation
478
402
  if (originalMetadata[key].status === 'Scheduled') {
479
403
  let schedule = null;
@@ -481,70 +405,45 @@ class Automation extends MetadataType {
481
405
  schedule = this._buildSchedule(originalMetadata[key].schedule);
482
406
  } catch (ex) {
483
407
  Util.logger.error(
484
- `Could not create schedule for automation ${originalMetadata[key].name} to start it: ${ex.message}`
408
+ `- Could not create schedule for automation ${originalMetadata[key].name} to start it: ${ex.message}`
485
409
  );
486
410
  }
487
411
  if (schedule !== null) {
488
412
  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
- }
413
+ await this.client.soap.schedule(
414
+ 'Automation',
415
+ schedule,
416
+ {
417
+ Interaction: {
418
+ ObjectID: metadata[key].id,
419
+ },
420
+ },
421
+ 'start',
422
+ {}
423
+ );
424
+ const intervalString =
425
+ (schedule._interval > 1 ? `${schedule._interval} ` : '') +
426
+ (schedule.RecurrenceType === 'Daily'
427
+ ? 'Day'
428
+ : schedule.RecurrenceType.slice(0, -2) +
429
+ (schedule._interval > 1 ? 's' : ''));
430
+ Util.logger.warn(
431
+ ` - scheduled automation '${
432
+ originalMetadata[key].name
433
+ }' deployed Active: runs every ${intervalString} starting ${
434
+ schedule._StartDateTime.split('T').join(' ').split('.')[0]
435
+ } ${schedule._timezoneString}`
539
436
  );
540
437
  } catch (ex) {
541
438
  Util.logger.error(
542
- `Could not start automation '${originalMetadata[key].name}': ${ex.message}`
439
+ `- Could not start scheduled automation '${originalMetadata[key].name}': ${ex.message}`
543
440
  );
544
441
  }
545
442
  }
546
443
  } else {
547
- Util.logger.warn(`Automation '${originalMetadata[key].name}' deployed Paused`);
444
+ Util.logger.warn(
445
+ ` - scheduled automation '${originalMetadata[key].name}' deployed Paused`
446
+ );
548
447
  }
549
448
  }
550
449
  if (metadata[key].startSource) {
@@ -561,14 +460,14 @@ class Automation extends MetadataType {
561
460
 
562
461
  /**
563
462
  * 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
463
+ *
464
+ * @param {TYPE.AutomationItem} metadata a single automation definition
465
+ * @returns {TYPE.AutomationItem} parsed item
566
466
  */
567
467
  static parseMetadata(metadata) {
568
468
  // automations are often skipped due to lack of support.
569
469
  try {
570
- metadata.r__folder_Path = Util.getFromCache(
571
- this.cache,
470
+ metadata.r__folder_Path = cache.searchForField(
572
471
  'folder',
573
472
  metadata.categoryId,
574
473
  'ID',
@@ -577,7 +476,9 @@ class Automation extends MetadataType {
577
476
  delete metadata.categoryId;
578
477
  if (metadata.r__folder_Path !== 'my automations') {
579
478
  Util.logger.verbose(
580
- `Automation '${metadata[this.definition.nameField]}' is located in subfolder ${
479
+ `- automation '${
480
+ metadata[this.definition.nameField]
481
+ }' is located in subfolder ${
581
482
  metadata.r__folder_Path
582
483
  }. 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
484
  );
@@ -585,13 +486,13 @@ class Automation extends MetadataType {
585
486
  } catch (ex) {
586
487
  // * don't exit on missing folder for automation
587
488
  Util.logger.warn(
588
- `${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
489
+ ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
589
490
  ex.message
590
491
  }`
591
492
  );
592
493
  }
593
494
  try {
594
- if (metadata.type === 'scheduled' && metadata.schedule && metadata.schedule.startDate) {
495
+ if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
595
496
  // Starting Source == 'Schedule'
596
497
 
597
498
  try {
@@ -599,24 +500,21 @@ class Automation extends MetadataType {
599
500
  // if we found the id in our list, remove the redundant data
600
501
  delete metadata.schedule.timezoneId;
601
502
  }
602
- } catch (ex) {
503
+ } catch {
603
504
  Util.logger.debug(
604
- `Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
505
+ `- Schedule name '${metadata.schedule.timezoneName}' not found in definition.timeZoneMapping`
605
506
  );
606
507
  }
607
508
  try {
608
509
  // type 'Running' is temporary status only, overwrite with Scheduled for storage.
609
510
  if (metadata.type === 'scheduled' && metadata.status === 'Running') {
610
- metadata.status === 'Scheduled';
511
+ metadata.status = 'Scheduled';
611
512
  }
612
- } catch (ex) {
613
- Util.metadataLogger(
614
- 'error',
615
- this.definition.type,
616
- 'parseMetadata',
617
- `${metadata.name} does not have a valid schedule setting. `
513
+ } catch {
514
+ Util.logger.error(
515
+ `- ${this.definition.type} ${metadata.name} does not have a valid schedule setting.`
618
516
  );
619
- return null;
517
+ return;
620
518
  }
621
519
  } else if (metadata.type === 'triggered' && metadata.fileTrigger) {
622
520
  // Starting Source == 'File Drop'
@@ -624,6 +522,10 @@ class Automation extends MetadataType {
624
522
  }
625
523
  if (metadata.steps) {
626
524
  for (const step of metadata.steps) {
525
+ const stepNumber = step.stepNumber || step.step;
526
+ delete step.stepNumber;
527
+ delete step.step;
528
+
627
529
  for (const activity of step.activities) {
628
530
  try {
629
531
  // get metadata type of activity
@@ -632,56 +534,50 @@ class Automation extends MetadataType {
632
534
  activity.objectTypeId
633
535
  );
634
536
  delete activity.objectTypeId;
635
- // if no activityObjectId then either serialized activity
636
- // (config in Automation ) or unconfigured so no further action to be taken
637
- if (
638
- activity.activityObjectId ===
639
- '00000000-0000-0000-0000-000000000000' ||
640
- activity.activityObjectId == null ||
641
- !this.definition.dependencies.includes(activity.r__type)
642
- ) {
643
- // empty if block
644
- }
645
- // / if managed by cache we can update references to support deployment
646
- else if (
647
- Definitions[activity.r__type] &&
648
- Definitions[activity.r__type]['idField'] &&
649
- this.cache[activity.r__type]
650
- ) {
651
- try {
652
- activity.activityObjectId = Util.getFromCache(
653
- this.cache,
654
- activity.r__type,
655
- activity.activityObjectId,
656
- Definitions[activity.r__type].idField,
657
- Definitions[activity.r__type].nameField
658
- );
659
- } catch (e) {
660
- // getFromCache throws error where the dependent metadata is not found
661
- Util.logger.error(
662
- `Missing ${activity.r__type} activity '${activity.name}'` +
663
- ` in step ${step.stepNumber || step.step}.${
664
- activity.displayOrder
665
- }` +
666
- ` of Automation '${metadata.name}' (${e.message})`
667
- );
668
- return null;
669
- }
670
- } else {
671
- Util.logger.error(
672
- `Missing ${activity.r__type} activity '${activity.name}'` +
673
- ` in step ${step.stepNumber || step.step}.${
674
- activity.displayOrder
675
- }` +
676
- ` of Automation '${metadata.name}' (Not Found in Cache)`
537
+ } catch {
538
+ Util.logger.warn(
539
+ ` - Unknown activity type '${activity.objectTypeId}'` +
540
+ ` in step ${stepNumber}.${activity.displayOrder}` +
541
+ ` of Automation '${metadata.name}'`
542
+ );
543
+ continue;
544
+ }
545
+
546
+ // if no activityObjectId then either serialized activity
547
+ // (config in Automation ) or unconfigured so no further action to be taken
548
+ if (
549
+ activity.activityObjectId === '00000000-0000-0000-0000-000000000000' ||
550
+ activity.activityObjectId == null ||
551
+ !this.definition.dependencies.includes(activity.r__type)
552
+ ) {
553
+ // empty if block
554
+ }
555
+ // / if managed by cache we can update references to support deployment
556
+ else if (
557
+ Definitions[activity.r__type]?.['idField'] &&
558
+ cache.getCache(this.buObject.mid)[activity.r__type]
559
+ ) {
560
+ try {
561
+ activity.activityObjectId = cache.searchForField(
562
+ activity.r__type,
563
+ activity.activityObjectId,
564
+ Definitions[activity.r__type].idField,
565
+ Definitions[activity.r__type].nameField
566
+ );
567
+ } catch (ex) {
568
+ // getFromCache throws error where the dependent metadata is not found
569
+ Util.logger.warn(
570
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
571
+ ` in step ${stepNumber}.${activity.displayOrder}` +
572
+ ` of Automation '${metadata.name}' (${ex.message})`
677
573
  );
678
- return null;
679
574
  }
680
- } catch (ex) {
575
+ } else {
681
576
  Util.logger.warn(
682
- `Excluding automation '${metadata.name}' from retrieve (ObjectType ${activity.objectTypeId} is unknown)`
577
+ ` - Missing ${activity.r__type} activity '${activity.name}'` +
578
+ ` in step ${stepNumber}.${activity.displayOrder}` +
579
+ ` of Automation '${metadata.name}' (Not Found in Cache)`
683
580
  );
684
- return null;
685
581
  }
686
582
  }
687
583
  }
@@ -689,7 +585,7 @@ class Automation extends MetadataType {
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;
777
680
  }
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);
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;
784
689
  }
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));
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;
699
+ }
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;