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.
- package/.eslintrc.json +67 -7
- package/.github/ISSUE_TEMPLATE/bug.yml +5 -1
- package/.github/ISSUE_TEMPLATE/task.md +1 -1
- package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
- package/.github/dependabot.yml +14 -0
- package/.github/workflows/code-analysis.yml +57 -0
- package/.husky/commit-msg +10 -0
- package/.husky/post-checkout +5 -0
- package/.husky/pre-commit +2 -1
- package/.prettierrc +8 -0
- package/.vscode/settings.json +1 -1
- package/LICENSE +2 -2
- package/README.md +134 -45
- package/boilerplate/config.json +5 -11
- package/boilerplate/files/.prettierrc +8 -0
- package/boilerplate/files/.vscode/extensions.json +0 -1
- package/boilerplate/files/.vscode/settings.json +28 -2
- package/boilerplate/files/README.md +2 -2
- package/boilerplate/forcedUpdates.json +10 -0
- package/boilerplate/npm-dependencies.json +5 -5
- package/docs/dist/documentation.md +2795 -1724
- package/jsconfig.json +1 -1
- package/lib/Builder.js +166 -75
- package/lib/Deployer.js +244 -96
- package/lib/MetadataTypeDefinitions.js +2 -0
- package/lib/MetadataTypeInfo.js +2 -0
- package/lib/Retriever.js +61 -84
- package/lib/cli.js +133 -25
- package/lib/index.js +242 -563
- package/lib/metadataTypes/AccountUser.js +101 -95
- package/lib/metadataTypes/Asset.js +677 -248
- package/lib/metadataTypes/AttributeGroup.js +23 -12
- package/lib/metadataTypes/Automation.js +456 -357
- package/lib/metadataTypes/Campaign.js +33 -93
- package/lib/metadataTypes/ContentArea.js +31 -11
- package/lib/metadataTypes/DataExtension.js +391 -376
- package/lib/metadataTypes/DataExtensionField.js +131 -54
- package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
- package/lib/metadataTypes/DataExtract.js +67 -50
- package/lib/metadataTypes/DataExtractType.js +14 -8
- package/lib/metadataTypes/Discovery.js +21 -16
- package/lib/metadataTypes/Email.js +32 -12
- package/lib/metadataTypes/EmailSendDefinition.js +85 -80
- package/lib/metadataTypes/EventDefinition.js +69 -47
- package/lib/metadataTypes/FileTransfer.js +78 -54
- package/lib/metadataTypes/Filter.js +11 -4
- package/lib/metadataTypes/Folder.js +149 -117
- package/lib/metadataTypes/FtpLocation.js +14 -8
- package/lib/metadataTypes/ImportFile.js +69 -69
- package/lib/metadataTypes/Interaction.js +19 -4
- package/lib/metadataTypes/List.js +54 -13
- package/lib/metadataTypes/MetadataType.js +687 -479
- package/lib/metadataTypes/MobileCode.js +46 -0
- package/lib/metadataTypes/MobileKeyword.js +114 -0
- package/lib/metadataTypes/Query.js +204 -103
- package/lib/metadataTypes/Role.js +76 -61
- package/lib/metadataTypes/Script.js +146 -82
- package/lib/metadataTypes/SetDefinition.js +20 -8
- package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
- package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
- package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
- package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
- package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
- package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
- package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
- package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
- package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
- package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
- package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
- package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
- package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
- package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
- package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
- package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
- package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
- package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
- package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
- package/lib/metadataTypes/definitions/Query.definition.js +4 -0
- package/lib/metadataTypes/definitions/Role.definition.js +5 -0
- package/lib/metadataTypes/definitions/Script.definition.js +4 -0
- package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
- package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
- package/lib/retrieveChangelog.js +7 -6
- package/lib/util/auth.js +117 -0
- package/lib/util/businessUnit.js +55 -66
- package/lib/util/cache.js +194 -0
- package/lib/util/cli.js +90 -116
- package/lib/util/config.js +302 -0
- package/lib/util/devops.js +240 -50
- package/lib/util/file.js +120 -191
- package/lib/util/init.config.js +195 -69
- package/lib/util/init.git.js +45 -50
- package/lib/util/init.js +72 -59
- package/lib/util/init.npm.js +48 -39
- package/lib/util/util.js +280 -564
- package/package.json +44 -33
- package/test/dataExtension.test.js +152 -0
- package/test/mockRoot/.mcdev-auth.json +8 -0
- package/test/mockRoot/.mcdevrc.json +67 -0
- package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
- package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
- package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
- package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
- package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
- package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
- package/test/query.test.js +149 -0
- package/test/resourceFactory.js +142 -0
- package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
- package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
- package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
- package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
- package/test/resources/9999999/dataExtension/build-expected.json +51 -0
- package/test/resources/9999999/dataExtension/create-expected.json +23 -0
- package/test/resources/9999999/dataExtension/create-response.xml +54 -0
- package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
- package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
- package/test/resources/9999999/dataExtension/template-expected.json +51 -0
- package/test/resources/9999999/dataExtension/update-expected.json +55 -0
- package/test/resources/9999999/dataExtension/update-response.xml +52 -0
- package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
- package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
- package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
- package/test/resources/9999999/query/build-expected.json +8 -0
- package/test/resources/9999999/query/get-expected.json +11 -0
- package/test/resources/9999999/query/patch-expected.json +11 -0
- package/test/resources/9999999/query/post-expected.json +11 -0
- package/test/resources/9999999/query/template-expected.json +8 -0
- package/test/resources/auth.json +32 -0
- package/test/resources/rest404-response.json +5 -0
- package/test/resources/retrieve-response.xml +21 -0
- package/test/utils.js +107 -0
- package/types/mcdev.d.js +301 -0
- package/CHANGELOG.md +0 -126
- package/PULL_REQUEST_TEMPLATE.md +0 -19
- 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
|
-
* @
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
|
|
120
63
|
*/
|
|
121
64
|
static async retrieveChangelog() {
|
|
122
|
-
const results = await
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
*
|
|
105
|
+
*
|
|
106
|
+
* @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
|
|
175
107
|
*/
|
|
176
108
|
static async retrieveForCache() {
|
|
177
|
-
const results = await
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 {
|
|
206
|
-
* @returns {Promise
|
|
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,
|
|
209
|
-
const results = await
|
|
210
|
-
|
|
211
|
-
'
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
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),
|
|
159
|
+
Util.replaceByObject(JSON.stringify(parsedDetails), templateVariables)
|
|
248
160
|
);
|
|
249
161
|
}
|
|
250
|
-
} catch
|
|
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
|
-
|
|
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
|
-
*
|
|
280
|
-
* @param {
|
|
281
|
-
* @
|
|
282
|
-
* @returns {AutomationItem} metadata
|
|
189
|
+
*
|
|
190
|
+
* @param {TYPE.AutomationItem} metadata a single automation
|
|
191
|
+
* @returns {TYPE.AutomationItem} metadata
|
|
283
192
|
*/
|
|
284
|
-
static postRetrieveTasks(metadata
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
321
|
-
* @param {AutomationItem}
|
|
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
|
-
*
|
|
333
|
-
* @
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
401
|
-
for (const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
activity.
|
|
405
|
-
|
|
406
|
-
activity.
|
|
407
|
-
activity.
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
*
|
|
430
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
`
|
|
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
|
-
*
|
|
467
|
-
* @param {AutomationMap}
|
|
468
|
-
* @
|
|
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]
|
|
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
|
-
|
|
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
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
437
|
+
`- Could not start scheduled automation '${originalMetadata[key].name}': ${ex.message}`
|
|
541
438
|
);
|
|
542
439
|
}
|
|
543
440
|
}
|
|
544
441
|
} else {
|
|
545
|
-
Util.logger.warn(
|
|
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
|
-
*
|
|
563
|
-
* @
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
501
|
+
} catch {
|
|
601
502
|
Util.logger.debug(
|
|
602
|
-
|
|
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
|
|
509
|
+
metadata.status = 'Scheduled';
|
|
609
510
|
}
|
|
610
|
-
} catch
|
|
611
|
-
Util.
|
|
612
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
553
|
+
} catch (ex) {
|
|
658
554
|
// getFromCache throws error where the dependent metadata is not found
|
|
659
|
-
Util.logger.
|
|
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}' (${
|
|
560
|
+
` of Automation '${metadata.name}' (${ex.message})`
|
|
665
561
|
);
|
|
666
562
|
return null;
|
|
667
563
|
}
|
|
668
564
|
} else {
|
|
669
|
-
Util.logger.
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
702
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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;
|