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.
- package/.eslintrc.json +67 -7
- package/.github/ISSUE_TEMPLATE/bug.yml +2 -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 +30 -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 +2807 -1730
- package/jsconfig.json +1 -1
- package/lib/Builder.js +171 -74
- 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 +116 -11
- package/lib/index.js +241 -561
- package/lib/metadataTypes/AccountUser.js +117 -103
- package/lib/metadataTypes/Asset.js +705 -255
- package/lib/metadataTypes/AttributeGroup.js +23 -12
- package/lib/metadataTypes/Automation.js +489 -392
- package/lib/metadataTypes/Campaign.js +33 -93
- package/lib/metadataTypes/ContentArea.js +31 -11
- package/lib/metadataTypes/DataExtension.js +387 -372
- package/lib/metadataTypes/DataExtensionField.js +131 -54
- package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
- package/lib/metadataTypes/DataExtract.js +61 -48
- 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 +61 -43
- package/lib/metadataTypes/FileTransfer.js +72 -52
- 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 +61 -64
- package/lib/metadataTypes/Interaction.js +19 -4
- package/lib/metadataTypes/List.js +54 -13
- package/lib/metadataTypes/MetadataType.js +664 -454
- package/lib/metadataTypes/MobileCode.js +46 -0
- package/lib/metadataTypes/MobileKeyword.js +114 -0
- package/lib/metadataTypes/Query.js +206 -105
- package/lib/metadataTypes/Role.js +76 -61
- package/lib/metadataTypes/Script.js +147 -83
- 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 +250 -50
- package/lib/util/file.js +141 -201
- package/lib/util/init.config.js +208 -75
- 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 +45 -34
- 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
|
@@ -7,32 +7,13 @@
|
|
|
7
7
|
* in the generic version of the method
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
* @typedef {Object.<string, any>} MetadataTypeItem
|
|
12
|
-
*
|
|
13
|
-
* @typedef {Object} CodeExtractItem
|
|
14
|
-
* @property {MetadataTypeItem} json metadata of one item w/o code
|
|
15
|
-
* @property {CodeExtract[]} codeArr list of code snippets in this item
|
|
16
|
-
* @property {string[]} subFolder mostly set to null, otherwise list of subfolders
|
|
17
|
-
*
|
|
18
|
-
* @typedef {Object} CodeExtract
|
|
19
|
-
* @property {string[]} subFolder mostly set to null, otherwise subfolders path split into elements
|
|
20
|
-
* @property {string} fileName name of file w/o extension
|
|
21
|
-
* @property {string} fileExt file extension
|
|
22
|
-
* @property {string} content file content
|
|
23
|
-
* @property {'base64'} [encoding] optional for binary files
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
* @typedef {Object.<string, MetadataTypeItem>} MetadataTypeMap
|
|
27
|
-
*
|
|
28
|
-
* @typedef {Object.<string, MetadataTypeMap>} MultiMetadataTypeMap
|
|
29
|
-
*/
|
|
30
|
-
|
|
10
|
+
const TYPE = require('../../types/mcdev.d');
|
|
31
11
|
const Util = require('../util/util');
|
|
32
12
|
const File = require('../util/file');
|
|
13
|
+
const cache = require('../util/cache');
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
const
|
|
15
|
+
const deepEqual = require('deep-equal');
|
|
16
|
+
const pLimit = require('p-limit');
|
|
36
17
|
const Mustache = require('mustache');
|
|
37
18
|
|
|
38
19
|
/**
|
|
@@ -41,34 +22,18 @@ const Mustache = require('mustache');
|
|
|
41
22
|
*
|
|
42
23
|
*/
|
|
43
24
|
class MetadataType {
|
|
44
|
-
/**
|
|
45
|
-
* Instantiates a metadata constructor to avoid passing variables.
|
|
46
|
-
*
|
|
47
|
-
* @param {Util.ET_Client} client client for sfmc fuelsdk
|
|
48
|
-
* @param {string} businessUnit Name of business unit (corresponding to their keys in 'properties.json' file). Used to access correct directories
|
|
49
|
-
* @param {Object} cache metadata cache
|
|
50
|
-
* @param {Object} properties mcdev config
|
|
51
|
-
* @param {string} [subType] limit retrieve to specific subType
|
|
52
|
-
*/
|
|
53
|
-
constructor(client, businessUnit, cache, properties, subType) {
|
|
54
|
-
this.client = client;
|
|
55
|
-
this.businessUnit = businessUnit;
|
|
56
|
-
this.cache = cache;
|
|
57
|
-
this.properties = properties;
|
|
58
|
-
this.subType = subType;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
25
|
/**
|
|
62
26
|
* Returns file contents mapped to their filename without '.json' ending
|
|
27
|
+
*
|
|
63
28
|
* @param {string} dir directory that contains '.json' files to be read
|
|
64
29
|
* @param {boolean} [listBadKeys=false] do not print errors, used for badKeys()
|
|
65
|
-
* @returns {
|
|
30
|
+
* @returns {TYPE.MetadataTypeMap} fileName => fileContent map
|
|
66
31
|
*/
|
|
67
32
|
static getJsonFromFS(dir, listBadKeys) {
|
|
68
33
|
const fileName2FileContent = {};
|
|
69
34
|
try {
|
|
70
35
|
const files = File.readdirSync(dir);
|
|
71
|
-
|
|
36
|
+
for (const fileName of files) {
|
|
72
37
|
try {
|
|
73
38
|
if (fileName.endsWith('.json')) {
|
|
74
39
|
const fileContent = File.readJSONFile(dir, fileName, true, false);
|
|
@@ -77,10 +42,11 @@ class MetadataType {
|
|
|
77
42
|
);
|
|
78
43
|
// We always store the filename using the External Key (CustomerKey or key) to avoid duplicate names.
|
|
79
44
|
// to ensure any changes are done to both the filename and external key do a check here
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
45
|
+
// ! convert numbers to string to allow numeric keys to be checked properly
|
|
46
|
+
const key = Number.isInteger(fileContent[this.definition.keyField])
|
|
47
|
+
? fileContent[this.definition.keyField].toString()
|
|
48
|
+
: fileContent[this.definition.keyField];
|
|
49
|
+
if (key === fileNameWithoutEnding || listBadKeys) {
|
|
84
50
|
fileName2FileContent[fileNameWithoutEnding] = fileContent;
|
|
85
51
|
} else {
|
|
86
52
|
Util.metadataLogger(
|
|
@@ -90,7 +56,7 @@ class MetadataType {
|
|
|
90
56
|
'Name of the Metadata and the External Identifier must match',
|
|
91
57
|
JSON.stringify({
|
|
92
58
|
Filename: fileNameWithoutEnding,
|
|
93
|
-
ExternalIdentifier:
|
|
59
|
+
ExternalIdentifier: key,
|
|
94
60
|
})
|
|
95
61
|
);
|
|
96
62
|
}
|
|
@@ -99,7 +65,7 @@ class MetadataType {
|
|
|
99
65
|
// by catching this in the loop we gracefully handle the issue and move on to the next file
|
|
100
66
|
Util.metadataLogger('debug', this.definition.type, 'getJsonFromFS', ex);
|
|
101
67
|
}
|
|
102
|
-
}
|
|
68
|
+
}
|
|
103
69
|
} catch (ex) {
|
|
104
70
|
// this will catch issues with readdirSync
|
|
105
71
|
Util.metadataLogger('debug', this.definition.type, 'getJsonFromFS', ex);
|
|
@@ -110,6 +76,7 @@ class MetadataType {
|
|
|
110
76
|
|
|
111
77
|
/**
|
|
112
78
|
* Returns fieldnames of Metadata Type. 'this.definition.fields' variable only set in child classes.
|
|
79
|
+
*
|
|
113
80
|
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
114
81
|
* @returns {string[]} Fieldnames
|
|
115
82
|
*/
|
|
@@ -117,7 +84,7 @@ class MetadataType {
|
|
|
117
84
|
const fieldNames = [];
|
|
118
85
|
for (const fieldName in this.definition.fields) {
|
|
119
86
|
if (
|
|
120
|
-
|
|
87
|
+
additionalFields?.includes(fieldName) ||
|
|
121
88
|
this.definition.fields[fieldName].retrieving
|
|
122
89
|
) {
|
|
123
90
|
fieldNames.push(fieldName);
|
|
@@ -132,18 +99,23 @@ class MetadataType {
|
|
|
132
99
|
|
|
133
100
|
/**
|
|
134
101
|
* Deploys metadata
|
|
135
|
-
*
|
|
102
|
+
*
|
|
103
|
+
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
|
|
136
104
|
* @param {string} deployDir directory where deploy metadata are saved
|
|
137
105
|
* @param {string} retrieveDir directory where metadata after deploy should be saved
|
|
138
|
-
* @param {
|
|
139
|
-
* @returns {Promise
|
|
106
|
+
* @param {TYPE.BuObject} buObject properties for auth
|
|
107
|
+
* @returns {Promise.<object>} Promise of keyField => metadata map
|
|
140
108
|
*/
|
|
141
109
|
static async deploy(metadata, deployDir, retrieveDir, buObject) {
|
|
142
110
|
const upsertResults = await this.upsert(metadata, deployDir, buObject);
|
|
143
111
|
await this.postDeployTasks(upsertResults, metadata);
|
|
144
112
|
const savedMetadata = await this.saveResults(upsertResults, retrieveDir, null);
|
|
145
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
|
|
115
|
+
!this.definition.documentInOneFile
|
|
116
|
+
) {
|
|
146
117
|
// * do not await here as this might take a while and has no impact on the deploy
|
|
118
|
+
// * this should only be run if documentation is on a per metadata record level. Types that document an overview into a single file will need a full retrieve to work instead
|
|
147
119
|
this.document(buObject, savedMetadata, true);
|
|
148
120
|
}
|
|
149
121
|
return upsertResults;
|
|
@@ -151,86 +123,69 @@ class MetadataType {
|
|
|
151
123
|
|
|
152
124
|
/**
|
|
153
125
|
* Gets executed after deployment of metadata type
|
|
154
|
-
*
|
|
155
|
-
* @param {MetadataTypeMap}
|
|
126
|
+
*
|
|
127
|
+
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
|
|
128
|
+
* @param {TYPE.MetadataTypeMap} originalMetadata metadata to be updated (contains additioanl fields)
|
|
156
129
|
* @returns {void}
|
|
157
130
|
*/
|
|
158
131
|
static postDeployTasks(metadata, originalMetadata) {}
|
|
159
132
|
|
|
160
133
|
/**
|
|
161
134
|
* Gets executed after retreive of metadata type
|
|
162
|
-
*
|
|
135
|
+
*
|
|
136
|
+
* @param {TYPE.MetadataTypeItem} metadata a single item
|
|
163
137
|
* @param {string} targetDir folder where retrieves should be saved
|
|
164
138
|
* @param {boolean} [isTemplating] signals that we are retrieving templates
|
|
165
|
-
* @returns {MetadataTypeItem} cloned metadata
|
|
139
|
+
* @returns {TYPE.MetadataTypeItem} cloned metadata
|
|
166
140
|
*/
|
|
167
141
|
static postRetrieveTasks(metadata, targetDir, isTemplating) {
|
|
168
142
|
return JSON.parse(JSON.stringify(metadata));
|
|
169
143
|
}
|
|
170
|
-
/**
|
|
171
|
-
* used to synchronize name and external key during retrieveAsTemplate
|
|
172
|
-
* @param {MetadataTypeItem} metadata a single item
|
|
173
|
-
* @param {string} [warningMsg] optional msg to show the user
|
|
174
|
-
* @returns {void}
|
|
175
|
-
*/
|
|
176
|
-
static overrideKeyWithName(metadata, warningMsg) {
|
|
177
|
-
if (
|
|
178
|
-
this.definition.nameField &&
|
|
179
|
-
this.definition.keyField &&
|
|
180
|
-
metadata[this.definition.nameField] !== metadata[this.definition.keyField]
|
|
181
|
-
) {
|
|
182
|
-
Util.logger.warn(
|
|
183
|
-
`Reset external key of ${this.definition.type} ${
|
|
184
|
-
metadata[this.definition.nameField]
|
|
185
|
-
} to its name (${metadata[this.definition.keyField]})`
|
|
186
|
-
);
|
|
187
|
-
if (warningMsg) {
|
|
188
|
-
Util.logger.warn(warningMsg);
|
|
189
|
-
}
|
|
190
|
-
// do this after printing to cli or we lost the info
|
|
191
|
-
metadata[this.definition.keyField] = metadata[this.definition.nameField];
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
144
|
/**
|
|
195
145
|
* Gets metadata from Marketing Cloud
|
|
146
|
+
*
|
|
196
147
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
197
148
|
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
198
|
-
* @param {
|
|
149
|
+
* @param {TYPE.BuObject} buObject properties for auth
|
|
199
150
|
* @param {string} [subType] optionally limit to a single subtype
|
|
200
|
-
* @
|
|
151
|
+
* @param {string} [key] customer key of single item to retrieve
|
|
152
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} metadata
|
|
201
153
|
*/
|
|
202
|
-
static retrieve(retrieveDir, additionalFields, buObject, subType) {
|
|
154
|
+
static retrieve(retrieveDir, additionalFields, buObject, subType, key) {
|
|
203
155
|
Util.metadataLogger('error', this.definition.type, 'retrieve', `Not Supported`);
|
|
204
156
|
const metadata = {};
|
|
205
157
|
return { metadata: null, type: this.definition.type };
|
|
206
158
|
}
|
|
207
159
|
/**
|
|
208
160
|
* Gets metadata from Marketing Cloud
|
|
161
|
+
*
|
|
162
|
+
* @param {TYPE.BuObject} [buObject] properties for auth
|
|
209
163
|
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
210
|
-
* @param {Util.BuObject} buObject properties for auth
|
|
211
164
|
* @param {string} [subType] optionally limit to a single subtype
|
|
212
|
-
* @returns {Promise
|
|
165
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} metadata
|
|
213
166
|
*/
|
|
214
|
-
static retrieveChangelog(
|
|
215
|
-
return this.
|
|
167
|
+
static retrieveChangelog(buObject, additionalFields, subType) {
|
|
168
|
+
return this.retrieveForCache(buObject, subType);
|
|
216
169
|
}
|
|
217
170
|
|
|
218
171
|
/**
|
|
219
172
|
* Gets metadata cache with limited fields and does not store value to disk
|
|
220
|
-
*
|
|
173
|
+
*
|
|
174
|
+
* @param {TYPE.BuObject} buObject properties for auth
|
|
221
175
|
* @param {string} [subType] optionally limit to a single subtype
|
|
222
|
-
* @returns {Promise
|
|
176
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} metadata
|
|
223
177
|
*/
|
|
224
178
|
static async retrieveForCache(buObject, subType) {
|
|
225
179
|
return this.retrieve(null, null, buObject, subType);
|
|
226
180
|
}
|
|
227
181
|
/**
|
|
228
182
|
* Gets metadata cache with limited fields and does not store value to disk
|
|
183
|
+
*
|
|
229
184
|
* @param {string} templateDir Directory where retrieved metadata directory will be saved
|
|
230
185
|
* @param {string} name name of the metadata file
|
|
231
|
-
* @param {
|
|
186
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
232
187
|
* @param {string} [subType] optionally limit to a single subtype
|
|
233
|
-
* @returns {Promise
|
|
188
|
+
* @returns {Promise.<TYPE.MetadataTypeItemObj>} metadata
|
|
234
189
|
*/
|
|
235
190
|
static retrieveAsTemplate(templateDir, name, templateVariables, subType) {
|
|
236
191
|
Util.logger.error('retrieveAsTemplate is not supported yet for ' + this.definition.type);
|
|
@@ -242,60 +197,231 @@ class MetadataType {
|
|
|
242
197
|
);
|
|
243
198
|
return { metadata: null, type: this.definition.type };
|
|
244
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Gets metadata cache with limited fields and does not store value to disk
|
|
202
|
+
*
|
|
203
|
+
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
204
|
+
* @param {string} templateDir (List of) Directory where built definitions will be saved
|
|
205
|
+
* @param {string} key name of the metadata file
|
|
206
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
207
|
+
* @returns {Promise.<TYPE.MetadataTypeItemObj>} single metadata
|
|
208
|
+
*/
|
|
209
|
+
static async buildTemplate(retrieveDir, templateDir, key, templateVariables) {
|
|
210
|
+
// retrieve metadata template
|
|
211
|
+
let metadataStr;
|
|
212
|
+
const typeDirArr = [this.definition.type];
|
|
213
|
+
const subType = await this.findSubType(retrieveDir, key);
|
|
214
|
+
if (subType) {
|
|
215
|
+
typeDirArr.push(subType);
|
|
216
|
+
}
|
|
217
|
+
const suffix = subType ? `-${subType}-meta` : '-meta';
|
|
218
|
+
const fileName = key + '.' + this.definition.type + suffix;
|
|
219
|
+
try {
|
|
220
|
+
// ! do not load via readJSONFile to ensure we get a string, not parsed JSON
|
|
221
|
+
// templated files might contain illegal json before the conversion back to the file that shall be saved
|
|
222
|
+
metadataStr = await File.readFilteredFilename(
|
|
223
|
+
[retrieveDir, ...typeDirArr],
|
|
224
|
+
fileName,
|
|
225
|
+
'json'
|
|
226
|
+
);
|
|
227
|
+
} catch (ex) {
|
|
228
|
+
try {
|
|
229
|
+
metadataStr = await this.readSecondaryFolder(
|
|
230
|
+
retrieveDir,
|
|
231
|
+
typeDirArr,
|
|
232
|
+
key,
|
|
233
|
+
fileName,
|
|
234
|
+
ex
|
|
235
|
+
);
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`${this.definition.type}:: Could not find ./${File.normalizePath([
|
|
239
|
+
retrieveDir,
|
|
240
|
+
...typeDirArr,
|
|
241
|
+
fileName + '.json',
|
|
242
|
+
])}.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
// return;
|
|
246
|
+
}
|
|
247
|
+
const metadata = JSON.parse(Util.replaceByObject(metadataStr, templateVariables));
|
|
248
|
+
this.keepTemplateFields(metadata);
|
|
249
|
+
|
|
250
|
+
// handle extracted code
|
|
251
|
+
// templating to extracted content is applied inside of buildTemplateForNested()
|
|
252
|
+
await this.buildTemplateForNested(
|
|
253
|
+
retrieveDir,
|
|
254
|
+
templateDir,
|
|
255
|
+
metadata,
|
|
256
|
+
templateVariables,
|
|
257
|
+
key
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// write to file
|
|
262
|
+
await File.writeJSONToFile([templateDir, ...typeDirArr], fileName, metadata);
|
|
263
|
+
Util.logger.info(
|
|
264
|
+
`- templated ${this.definition.type}: ${key} (${
|
|
265
|
+
metadata[this.definition.nameField]
|
|
266
|
+
})`
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return { metadata: metadata, type: this.definition.type };
|
|
270
|
+
} catch (ex) {
|
|
271
|
+
throw new Error(`${this.definition.type}:: ${ex.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
245
274
|
|
|
246
275
|
/**
|
|
247
276
|
* Gets executed before deploying metadata
|
|
248
|
-
*
|
|
277
|
+
*
|
|
278
|
+
* @param {TYPE.MetadataTypeItem} metadata a single metadata item
|
|
249
279
|
* @param {string} deployDir folder where files for deployment are stored
|
|
250
|
-
* @
|
|
280
|
+
* @param {TYPE.BuObject} buObject buObject properties for auth
|
|
281
|
+
* @returns {Promise.<TYPE.MetadataTypeItem>} Promise of a single metadata item
|
|
251
282
|
*/
|
|
252
|
-
static async preDeployTasks(metadata, deployDir) {
|
|
283
|
+
static async preDeployTasks(metadata, deployDir, buObject) {
|
|
253
284
|
return metadata;
|
|
254
285
|
}
|
|
255
286
|
|
|
256
287
|
/**
|
|
257
288
|
* Abstract create method that needs to be implemented in child metadata type
|
|
258
|
-
*
|
|
289
|
+
*
|
|
290
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata entry
|
|
259
291
|
* @param {string} deployDir directory where deploy metadata are saved
|
|
260
292
|
* @returns {void}
|
|
261
293
|
*/
|
|
262
294
|
static create(metadata, deployDir) {
|
|
263
|
-
Util.
|
|
295
|
+
Util.logger.error(
|
|
296
|
+
` ☇ skipping ${this.definition.type} ${metadata[this.definition.keyField]} / ${
|
|
297
|
+
metadata[this.definition.nameField]
|
|
298
|
+
}: create is not supported yet for ${this.definition.type}`
|
|
299
|
+
);
|
|
264
300
|
return;
|
|
265
301
|
}
|
|
266
302
|
|
|
267
303
|
/**
|
|
268
304
|
* Abstract update method that needs to be implemented in child metadata type
|
|
269
|
-
*
|
|
270
|
-
* @param {MetadataTypeItem}
|
|
305
|
+
*
|
|
306
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata entry
|
|
307
|
+
* @param {TYPE.MetadataTypeItem} [metadataBefore] metadata mapped by their keyField
|
|
271
308
|
* @returns {void}
|
|
272
309
|
*/
|
|
273
310
|
static update(metadata, metadataBefore) {
|
|
274
|
-
Util.
|
|
311
|
+
Util.logger.error(
|
|
312
|
+
` ☇ skipping ${this.definition.type} ${metadata[this.definition.keyField]} / ${
|
|
313
|
+
metadata[this.definition.nameField]
|
|
314
|
+
}: update is not supported yet for ${this.definition.type}`
|
|
315
|
+
);
|
|
275
316
|
return;
|
|
276
317
|
}
|
|
277
318
|
|
|
319
|
+
/**
|
|
320
|
+
* test if metadata was actually changed or not to potentially skip it during deployment
|
|
321
|
+
*
|
|
322
|
+
* @param {TYPE.MetadataTypeItem} cachedVersion cached version from the server
|
|
323
|
+
* @param {TYPE.MetadataTypeItem} metadata item to upload
|
|
324
|
+
* @param {string} [fieldName] optional field name to use for identifying the record in logs
|
|
325
|
+
* @returns {boolean} true if metadata was changed
|
|
326
|
+
*/
|
|
327
|
+
static hasChanged(cachedVersion, metadata, fieldName) {
|
|
328
|
+
// should be set up type by type but the *_generic version is likely a good start for many types
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* test if metadata was actually changed or not to potentially skip it during deployment
|
|
333
|
+
*
|
|
334
|
+
* @param {TYPE.MetadataTypeItem} cachedVersion cached version from the server
|
|
335
|
+
* @param {TYPE.MetadataTypeItem} metadata item to upload
|
|
336
|
+
* @param {string} [fieldName] optional field name to use for identifying the record in logs
|
|
337
|
+
* @param {boolean} [silent] optionally suppress logging
|
|
338
|
+
* @returns {boolean} true on first identified deviation or false if none are found
|
|
339
|
+
*/
|
|
340
|
+
static hasChangedGeneric(cachedVersion, metadata, fieldName, silent) {
|
|
341
|
+
if (!cachedVersion) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
// we do need the full set in other places and hence need to work with a clone here
|
|
345
|
+
const clonedMetada = JSON.parse(JSON.stringify(metadata));
|
|
346
|
+
this.removeNotUpdateableFields(clonedMetada);
|
|
347
|
+
// iterate over what we want to upload rather than what we cached to avoid false positives
|
|
348
|
+
for (const prop in clonedMetada) {
|
|
349
|
+
if (this.definition.ignoreFieldsForUpdateCheck?.includes(prop)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
clonedMetada[prop] === null ||
|
|
354
|
+
['string', 'number', 'boolean'].includes(typeof clonedMetada[prop])
|
|
355
|
+
) {
|
|
356
|
+
// check simple variables directly
|
|
357
|
+
// check should ignore types to bypass string/number auto-conversions caused by SFMC-SDK
|
|
358
|
+
if (clonedMetada[prop] != cachedVersion[prop]) {
|
|
359
|
+
Util.logger.debug(
|
|
360
|
+
`${this.definition.type}:: ${
|
|
361
|
+
clonedMetada[fieldName || this.definition.keyField]
|
|
362
|
+
}.${prop} changed: '${cachedVersion[prop]}' to '${clonedMetada[prop]}'`
|
|
363
|
+
);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
} else if (deepEqual(clonedMetada[prop], cachedVersion[prop])) {
|
|
367
|
+
// test complex objects here
|
|
368
|
+
Util.logger.debug(
|
|
369
|
+
`${this.definition.type}:: ${
|
|
370
|
+
clonedMetada[fieldName || this.definition.keyField]
|
|
371
|
+
}.${prop} changed: '${cachedVersion[prop]}' to '${clonedMetada[prop]}'`
|
|
372
|
+
);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!silent) {
|
|
377
|
+
Util.logger.verbose(
|
|
378
|
+
` ☇ skipping ${this.definition.type} ${clonedMetada[this.definition.keyField]} / ${
|
|
379
|
+
clonedMetada[fieldName || this.definition.nameField]
|
|
380
|
+
}: no change detected`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
278
385
|
/**
|
|
279
386
|
* MetadataType upsert, after retrieving from target and comparing to check if create or update operation is needed.
|
|
280
|
-
*
|
|
387
|
+
*
|
|
388
|
+
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
|
|
281
389
|
* @param {string} deployDir directory where deploy metadata are saved
|
|
282
|
-
* @param {
|
|
283
|
-
* @returns {Promise
|
|
390
|
+
* @param {TYPE.BuObject} [buObject] properties for auth
|
|
391
|
+
* @returns {Promise.<TYPE.MetadataTypeMap>} keyField => metadata map
|
|
284
392
|
*/
|
|
285
393
|
static async upsert(metadata, deployDir, buObject) {
|
|
286
394
|
const metadataToUpdate = [];
|
|
287
395
|
const metadataToCreate = [];
|
|
396
|
+
let filteredByPreDeploy = 0;
|
|
288
397
|
for (const metadataKey in metadata) {
|
|
398
|
+
let hasError = false;
|
|
289
399
|
try {
|
|
290
400
|
// preDeployTasks parsing
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
401
|
+
let deployableMetadata;
|
|
402
|
+
try {
|
|
403
|
+
deployableMetadata = await this.preDeployTasks(
|
|
404
|
+
metadata[metadataKey],
|
|
405
|
+
deployDir,
|
|
406
|
+
buObject
|
|
407
|
+
);
|
|
408
|
+
} catch (ex) {
|
|
409
|
+
// do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
|
|
410
|
+
hasError = true;
|
|
411
|
+
deployableMetadata = metadata[metadataKey];
|
|
412
|
+
Util.logger.error(
|
|
413
|
+
` ☇ skipping ${this.definition.type} ${
|
|
414
|
+
deployableMetadata[this.definition.keyField]
|
|
415
|
+
} / ${deployableMetadata[this.definition.nameField]}: ${ex.message}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
295
418
|
// if preDeploy returns nothing then it cannot be deployed so skip deployment
|
|
296
419
|
if (deployableMetadata) {
|
|
297
420
|
metadata[metadataKey] = deployableMetadata;
|
|
298
|
-
|
|
421
|
+
// create normalizedKey off of whats in the json rather than from "metadataKey" because preDeployTasks might have altered something (type asset)
|
|
422
|
+
const normalizedKey = File.reverseFilterIllegalFilenames(
|
|
423
|
+
deployableMetadata[this.definition.keyField]
|
|
424
|
+
);
|
|
299
425
|
// Update if it already exists; Create it if not
|
|
300
426
|
if (
|
|
301
427
|
Util.logger.level === 'debug' &&
|
|
@@ -305,52 +431,79 @@ class MetadataType {
|
|
|
305
431
|
// only used if resource is excluded from cache and we still want to update it
|
|
306
432
|
// needed e.g. to rewire lost folders
|
|
307
433
|
Util.logger.warn(
|
|
308
|
-
'Hotfix for non-cachable resource found in deploy folder. Trying update:'
|
|
434
|
+
' - Hotfix for non-cachable resource found in deploy folder. Trying update:'
|
|
309
435
|
);
|
|
310
436
|
Util.logger.warn(JSON.stringify(metadata[metadataKey]));
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
437
|
+
if (hasError) {
|
|
438
|
+
metadataToUpdate.push(null);
|
|
439
|
+
} else {
|
|
440
|
+
metadataToUpdate.push({
|
|
441
|
+
before: {},
|
|
442
|
+
after: metadata[metadataKey],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
} else if (cache.getByKey(this.definition.type, normalizedKey)) {
|
|
316
446
|
// normal way of processing update files
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
447
|
+
const cachedVersion = cache.getByKey(this.definition.type, normalizedKey);
|
|
448
|
+
if (!this.hasChanged(cachedVersion, metadata[metadataKey])) {
|
|
449
|
+
hasError = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (hasError) {
|
|
453
|
+
// do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
|
|
454
|
+
metadataToUpdate.push(null);
|
|
455
|
+
} else {
|
|
456
|
+
// add ObjectId to allow actual update
|
|
457
|
+
metadata[metadataKey][this.definition.idField] =
|
|
458
|
+
cachedVersion[this.definition.idField];
|
|
459
|
+
|
|
460
|
+
metadataToUpdate.push({
|
|
461
|
+
before: cache.getByKey(this.definition.type, normalizedKey),
|
|
462
|
+
after: metadata[metadataKey],
|
|
463
|
+
});
|
|
464
|
+
}
|
|
325
465
|
} else {
|
|
326
|
-
|
|
466
|
+
if (hasError) {
|
|
467
|
+
// do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
|
|
468
|
+
metadataToCreate.push(null);
|
|
469
|
+
} else {
|
|
470
|
+
metadataToCreate.push(metadata[metadataKey]);
|
|
471
|
+
}
|
|
327
472
|
}
|
|
473
|
+
} else {
|
|
474
|
+
filteredByPreDeploy++;
|
|
328
475
|
}
|
|
329
476
|
} catch (ex) {
|
|
330
|
-
Util.
|
|
477
|
+
Util.logger.errorStack(
|
|
478
|
+
ex,
|
|
479
|
+
`Upserting ${this.definition.type} failed: ${ex.message}`
|
|
480
|
+
);
|
|
331
481
|
}
|
|
332
482
|
}
|
|
333
|
-
|
|
483
|
+
const createLimit = pLimit(10);
|
|
334
484
|
const createResults = (
|
|
335
|
-
await Promise.
|
|
336
|
-
metadataToCreate
|
|
337
|
-
|
|
338
|
-
|
|
485
|
+
await Promise.all(
|
|
486
|
+
metadataToCreate
|
|
487
|
+
.filter((r) => r !== undefined && r !== null)
|
|
488
|
+
.map((metadataEntry) =>
|
|
489
|
+
createLimit(() => this.create(metadataEntry, deployDir))
|
|
490
|
+
)
|
|
339
491
|
)
|
|
340
492
|
).filter((r) => r !== undefined && r !== null);
|
|
493
|
+
const updateLimit = pLimit(10);
|
|
341
494
|
const updateResults = (
|
|
342
|
-
await Promise.
|
|
343
|
-
metadataToUpdate
|
|
344
|
-
|
|
345
|
-
|
|
495
|
+
await Promise.all(
|
|
496
|
+
metadataToUpdate
|
|
497
|
+
.filter((r) => r !== undefined && r !== null)
|
|
498
|
+
.map((metadataEntry) =>
|
|
499
|
+
updateLimit(() => this.update(metadataEntry.after, metadataEntry.before))
|
|
500
|
+
)
|
|
346
501
|
)
|
|
347
502
|
).filter((r) => r !== undefined && r !== null);
|
|
348
503
|
// Logging
|
|
349
|
-
Util.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
'upsert',
|
|
353
|
-
`${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated`
|
|
504
|
+
Util.logger.info(
|
|
505
|
+
`${this.definition.type} upsert: ${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated` +
|
|
506
|
+
(filteredByPreDeploy > 0 ? ` / ${filteredByPreDeploy} filtered` : '')
|
|
354
507
|
);
|
|
355
508
|
|
|
356
509
|
// if Results then parse as SOAP
|
|
@@ -360,60 +513,57 @@ class MetadataType {
|
|
|
360
513
|
// @ts-ignore
|
|
361
514
|
const metadataResults = createResults
|
|
362
515
|
.concat(updateResults)
|
|
363
|
-
.
|
|
364
|
-
.
|
|
365
|
-
.
|
|
516
|
+
// TODO remove Object.keys check after create/update SOAP methods stop returning empty objects instead of null
|
|
517
|
+
.filter((r) => r !== undefined && r !== null && Object.keys(r).length !== 0)
|
|
518
|
+
.flatMap((r) => r.Results)
|
|
366
519
|
.map((r) => r.Object);
|
|
367
520
|
return this.parseResponseBody({ Results: metadataResults });
|
|
368
521
|
} else {
|
|
369
522
|
// put in Retrieve Format for parsing
|
|
370
523
|
// todo add handling when response does not contain items.
|
|
371
524
|
// @ts-ignore
|
|
372
|
-
const metadataResults = createResults
|
|
373
|
-
.concat(updateResults)
|
|
374
|
-
.filter((r) => r !== undefined && r !== null && r.res && r.res.body)
|
|
375
|
-
.map((r) => r.res.body);
|
|
525
|
+
const metadataResults = createResults.concat(updateResults).filter(Boolean);
|
|
376
526
|
return this.parseResponseBody({ items: metadataResults });
|
|
377
527
|
}
|
|
378
528
|
}
|
|
379
529
|
|
|
380
530
|
/**
|
|
381
531
|
* Creates a single metadata entry via REST
|
|
382
|
-
*
|
|
532
|
+
*
|
|
533
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry a single metadata Entry
|
|
383
534
|
* @param {string} uri rest endpoint for POST
|
|
384
535
|
* @returns {Promise} Promise
|
|
385
536
|
*/
|
|
386
537
|
static async createREST(metadataEntry, uri) {
|
|
387
538
|
this.removeNotCreateableFields(metadataEntry);
|
|
388
|
-
const options = {
|
|
389
|
-
uri: uri,
|
|
390
|
-
json: metadataEntry,
|
|
391
|
-
headers: {},
|
|
392
|
-
};
|
|
393
539
|
try {
|
|
394
|
-
|
|
395
|
-
await Util.retryOnError(
|
|
396
|
-
`Retrying ${this.definition.type}: ${metadataEntry[this.definition.nameField]}`,
|
|
397
|
-
async () => (response = await this.client.RestClient.post(options))
|
|
398
|
-
);
|
|
399
|
-
this.checkForErrors(response);
|
|
540
|
+
const response = await this.client.rest.post(uri, metadataEntry);
|
|
400
541
|
Util.logger.info(
|
|
401
|
-
|
|
542
|
+
` - created ${this.definition.type}: ${
|
|
543
|
+
metadataEntry[this.definition.keyField] ||
|
|
544
|
+
metadataEntry[this.definition.nameField]
|
|
545
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
402
546
|
);
|
|
403
547
|
return response;
|
|
404
548
|
} catch (ex) {
|
|
549
|
+
const parsedErrors = this.checkForErrors(ex);
|
|
405
550
|
Util.logger.error(
|
|
406
|
-
|
|
407
|
-
metadataEntry[this.definition.keyField]
|
|
408
|
-
|
|
551
|
+
` ☇ error creating ${this.definition.type} ${
|
|
552
|
+
metadataEntry[this.definition.keyField] ||
|
|
553
|
+
metadataEntry[this.definition.nameField]
|
|
554
|
+
} / ${metadataEntry[this.definition.nameField]}:`
|
|
409
555
|
);
|
|
556
|
+
for (const msg of parsedErrors) {
|
|
557
|
+
Util.logger.error(' • ' + msg);
|
|
558
|
+
}
|
|
410
559
|
return null;
|
|
411
560
|
}
|
|
412
561
|
}
|
|
413
562
|
|
|
414
563
|
/**
|
|
415
564
|
* Creates a single metadata entry via fuel-soap (generic lib not wrapper)
|
|
416
|
-
*
|
|
565
|
+
*
|
|
566
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry single metadata entry
|
|
417
567
|
* @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
|
|
418
568
|
* @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
|
|
419
569
|
* @returns {Promise} Promise
|
|
@@ -421,39 +571,29 @@ class MetadataType {
|
|
|
421
571
|
static async createSOAP(metadataEntry, overrideType, handleOutside) {
|
|
422
572
|
try {
|
|
423
573
|
this.removeNotCreateableFields(metadataEntry);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
async () =>
|
|
430
|
-
(response = await new Promise((resolve, reject) => {
|
|
431
|
-
this.client.SoapClient.create(
|
|
432
|
-
overrideType ||
|
|
433
|
-
this.definition.type.charAt(0).toUpperCase() +
|
|
434
|
-
this.definition.type.slice(1),
|
|
435
|
-
metadataEntry,
|
|
436
|
-
null,
|
|
437
|
-
(error, response) => (error ? reject(error) : resolve(response))
|
|
438
|
-
);
|
|
439
|
-
}))
|
|
574
|
+
const response = await this.client.soap.create(
|
|
575
|
+
overrideType ||
|
|
576
|
+
this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
|
|
577
|
+
metadataEntry,
|
|
578
|
+
null
|
|
440
579
|
);
|
|
580
|
+
|
|
441
581
|
if (!handleOutside) {
|
|
442
582
|
Util.logger.info(
|
|
443
|
-
|
|
583
|
+
` - created ${this.definition.type}: ${
|
|
584
|
+
metadataEntry[this.definition.keyField]
|
|
585
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
444
586
|
);
|
|
445
587
|
}
|
|
446
588
|
return response;
|
|
447
589
|
} catch (ex) {
|
|
448
590
|
if (!handleOutside) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
errorMsg = ex.message;
|
|
454
|
-
}
|
|
591
|
+
const errorMsg =
|
|
592
|
+
ex.results && ex.results.length
|
|
593
|
+
? `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`
|
|
594
|
+
: ex.message;
|
|
455
595
|
Util.logger.error(
|
|
456
|
-
|
|
596
|
+
` ☇ error creating ${this.definition.type} '${
|
|
457
597
|
metadataEntry[this.definition.keyField]
|
|
458
598
|
}': ${errorMsg}`
|
|
459
599
|
);
|
|
@@ -467,85 +607,71 @@ class MetadataType {
|
|
|
467
607
|
|
|
468
608
|
/**
|
|
469
609
|
* Updates a single metadata entry via REST
|
|
470
|
-
*
|
|
610
|
+
*
|
|
611
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry a single metadata Entry
|
|
471
612
|
* @param {string} uri rest endpoint for PATCH
|
|
472
613
|
* @returns {Promise} Promise
|
|
473
614
|
*/
|
|
474
615
|
static async updateREST(metadataEntry, uri) {
|
|
475
616
|
this.removeNotUpdateableFields(metadataEntry);
|
|
476
|
-
const options = {
|
|
477
|
-
uri: uri,
|
|
478
|
-
json: metadataEntry,
|
|
479
|
-
headers: {},
|
|
480
|
-
};
|
|
481
617
|
try {
|
|
482
|
-
|
|
483
|
-
await Util.retryOnError(
|
|
484
|
-
`Retrying ${this.definition.type}: ${metadataEntry[this.definition.nameField]}`,
|
|
485
|
-
async () => (response = await this.client.RestClient.patch(options))
|
|
486
|
-
);
|
|
618
|
+
const response = await this.client.rest.patch(uri, metadataEntry);
|
|
487
619
|
this.checkForErrors(response);
|
|
488
620
|
// some times, e.g. automation dont return a key in their update response and hence we need to fall back to name
|
|
489
621
|
Util.logger.info(
|
|
490
|
-
|
|
622
|
+
` - updated ${this.definition.type}: ${
|
|
491
623
|
metadataEntry[this.definition.keyField] ||
|
|
492
624
|
metadataEntry[this.definition.nameField]
|
|
493
|
-
}`
|
|
625
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
494
626
|
);
|
|
495
627
|
return response;
|
|
496
628
|
} catch (ex) {
|
|
629
|
+
const parsedErrors = this.checkForErrors(ex);
|
|
497
630
|
Util.logger.error(
|
|
498
|
-
|
|
499
|
-
metadataEntry[this.definition.keyField]
|
|
500
|
-
|
|
631
|
+
` ☇ error updating ${this.definition.type} ${
|
|
632
|
+
metadataEntry[this.definition.keyField] ||
|
|
633
|
+
metadataEntry[this.definition.nameField]
|
|
634
|
+
} / ${metadataEntry[this.definition.nameField]}:`
|
|
501
635
|
);
|
|
636
|
+
for (const msg of parsedErrors) {
|
|
637
|
+
Util.logger.error(' • ' + msg);
|
|
638
|
+
}
|
|
502
639
|
return null;
|
|
503
640
|
}
|
|
504
641
|
}
|
|
505
642
|
|
|
506
643
|
/**
|
|
507
644
|
* Updates a single metadata entry via fuel-soap (generic lib not wrapper)
|
|
508
|
-
*
|
|
645
|
+
*
|
|
646
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry single metadata entry
|
|
509
647
|
* @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
|
|
510
648
|
* @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
|
|
511
649
|
* @returns {Promise} Promise
|
|
512
650
|
*/
|
|
513
651
|
static async updateSOAP(metadataEntry, overrideType, handleOutside) {
|
|
514
|
-
let response;
|
|
515
652
|
try {
|
|
516
653
|
this.removeNotUpdateableFields(metadataEntry);
|
|
517
|
-
await
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
(response = await new Promise((resolve, reject) => {
|
|
523
|
-
this.client.SoapClient.update(
|
|
524
|
-
overrideType ||
|
|
525
|
-
this.definition.type.charAt(0).toUpperCase() +
|
|
526
|
-
this.definition.type.slice(1),
|
|
527
|
-
metadataEntry,
|
|
528
|
-
null,
|
|
529
|
-
(error, response) => (error ? reject(error) : resolve(response))
|
|
530
|
-
);
|
|
531
|
-
}))
|
|
654
|
+
const response = await this.client.soap.update(
|
|
655
|
+
overrideType ||
|
|
656
|
+
this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
|
|
657
|
+
metadataEntry,
|
|
658
|
+
null
|
|
532
659
|
);
|
|
533
660
|
if (!handleOutside) {
|
|
534
661
|
Util.logger.info(
|
|
535
|
-
|
|
662
|
+
` - updated ${this.definition.type}: ${
|
|
663
|
+
metadataEntry[this.definition.keyField]
|
|
664
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
536
665
|
);
|
|
537
666
|
}
|
|
538
667
|
return response;
|
|
539
668
|
} catch (ex) {
|
|
540
669
|
if (!handleOutside) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
} else {
|
|
545
|
-
errorMsg = ex.message;
|
|
546
|
-
}
|
|
670
|
+
const errorMsg = ex?.json?.Results.length
|
|
671
|
+
? `${ex.json.Results[0].StatusMessage} (Code ${ex.json.Results[0].ErrorCode})`
|
|
672
|
+
: ex.message;
|
|
547
673
|
Util.logger.error(
|
|
548
|
-
|
|
674
|
+
` ☇ error updating ${this.definition.type} '${
|
|
549
675
|
metadataEntry[this.definition.keyField]
|
|
550
676
|
}': ${errorMsg}`
|
|
551
677
|
);
|
|
@@ -558,23 +684,23 @@ class MetadataType {
|
|
|
558
684
|
}
|
|
559
685
|
/**
|
|
560
686
|
* Retrieves SOAP via generic fuel-soap wrapper based metadata of metadata type into local filesystem. executes callback with retrieved metadata
|
|
687
|
+
*
|
|
561
688
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
562
|
-
* @param {
|
|
563
|
-
* @param {
|
|
689
|
+
* @param {TYPE.BuObject} buObject properties for auth
|
|
690
|
+
* @param {TYPE.SoapRequestParams} [requestParams] required for the specific request (filter for example)
|
|
564
691
|
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
565
|
-
* @
|
|
566
|
-
* @returns {Promise<{metadata:MetadataTypeMap,type:string}>} Promise of item map
|
|
692
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of item map
|
|
567
693
|
*/
|
|
568
|
-
static async
|
|
569
|
-
|
|
570
|
-
buObject,
|
|
571
|
-
options,
|
|
572
|
-
additionalFields,
|
|
573
|
-
overrideType
|
|
574
|
-
) {
|
|
694
|
+
static async retrieveSOAP(retrieveDir, buObject, requestParams, additionalFields) {
|
|
695
|
+
requestParams = requestParams || {};
|
|
575
696
|
const fields = this.getFieldNamesToRetrieve(additionalFields);
|
|
697
|
+
const response = await this.client.soap.retrieveBulk(
|
|
698
|
+
this.definition.type,
|
|
699
|
+
fields,
|
|
700
|
+
requestParams
|
|
701
|
+
);
|
|
702
|
+
const metadata = this.parseResponseBody(response);
|
|
576
703
|
|
|
577
|
-
const metadata = await this.retrieveSOAPBody(fields, options, overrideType);
|
|
578
704
|
if (retrieveDir) {
|
|
579
705
|
const savedMetadata = await this.saveResults(metadata, retrieveDir, null);
|
|
580
706
|
Util.logger.info(
|
|
@@ -589,107 +715,31 @@ class MetadataType {
|
|
|
589
715
|
}
|
|
590
716
|
return { metadata: metadata, type: this.definition.type };
|
|
591
717
|
}
|
|
592
|
-
/**
|
|
593
|
-
* helper that handles batched retrieve via SOAP
|
|
594
|
-
* @param {string[]} fields list of fields that we want to see retrieved
|
|
595
|
-
* @param {Object} [options] required for the specific request (filter for example)
|
|
596
|
-
* @param {string} [type] optionally overwrite the API type of the metadata here
|
|
597
|
-
* @returns {Promise<MetadataTypeMap>} keyField => metadata map
|
|
598
|
-
*/
|
|
599
|
-
static async retrieveSOAPBody(fields, options, type) {
|
|
600
|
-
let status;
|
|
601
|
-
let batchCounter = 1;
|
|
602
|
-
const defaultBatchSize = 2500; // 2500 is the typical batch size
|
|
603
|
-
options = options || {};
|
|
604
|
-
let metadata = {};
|
|
605
|
-
do {
|
|
606
|
-
let resultsBatch;
|
|
607
|
-
await Util.retryOnError(`Retrying ${this.definition.type}`, async () => {
|
|
608
|
-
resultsBatch = await new Promise((resolve, reject) => {
|
|
609
|
-
this.client.SoapClient.retrieve(
|
|
610
|
-
type || this.definition.type,
|
|
611
|
-
fields,
|
|
612
|
-
options || {},
|
|
613
|
-
(error, response) => {
|
|
614
|
-
if (error) {
|
|
615
|
-
Util.logger.debug(`SOAP.retrieve Error: ${error.message}`);
|
|
616
|
-
reject(error);
|
|
617
|
-
}
|
|
618
|
-
if (response) {
|
|
619
|
-
resolve(response.body);
|
|
620
|
-
} else {
|
|
621
|
-
// fallback, lets make sure surrounding methods know we got an empty result back
|
|
622
|
-
resolve({});
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
);
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
status = resultsBatch.OverallStatus;
|
|
629
|
-
if (status === 'MoreDataAvailable') {
|
|
630
|
-
options.continueRequest = resultsBatch.RequestID;
|
|
631
|
-
Util.logger.info(
|
|
632
|
-
`- more than ${batchCounter * defaultBatchSize} ${
|
|
633
|
-
this.definition.typeName
|
|
634
|
-
}s found in Business Unit - loading next batch...`
|
|
635
|
-
);
|
|
636
|
-
batchCounter++;
|
|
637
|
-
}
|
|
638
|
-
const metadataBatch = this.parseResponseBody(resultsBatch);
|
|
639
|
-
|
|
640
|
-
metadata = { ...metadata, ...metadataBatch };
|
|
641
|
-
} while (status === 'MoreDataAvailable');
|
|
642
|
-
|
|
643
|
-
return metadata;
|
|
644
|
-
}
|
|
645
718
|
|
|
646
719
|
/**
|
|
647
720
|
* Retrieves Metadata for Rest Types
|
|
721
|
+
*
|
|
648
722
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
649
723
|
* @param {string} uri rest endpoint for GET
|
|
650
724
|
* @param {string} [overrideType] force a metadata type (mainly used for Folders)
|
|
651
|
-
* @param {
|
|
652
|
-
* @
|
|
725
|
+
* @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
|
|
726
|
+
* @param {string|number} [singleRetrieve] key of single item to filter by
|
|
727
|
+
* @returns {Promise.<{metadata: (TYPE.MetadataTypeMap | TYPE.MetadataTypeItem), type: string}>} Promise of item map (single item for templated result)
|
|
653
728
|
*/
|
|
654
|
-
static async retrieveREST(retrieveDir, uri, overrideType, templateVariables) {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
let lastPage = null;
|
|
661
|
-
let results = {};
|
|
662
|
-
do {
|
|
663
|
-
options.uri = this.paginate(options.uri, lastPage);
|
|
664
|
-
let response;
|
|
665
|
-
await Util.retryOnError(`Retrying ${this.definition.type}`, async () => {
|
|
666
|
-
response = await this.client.RestClient.get(options);
|
|
667
|
-
});
|
|
668
|
-
const metadata = this.parseResponseBody(response.body);
|
|
669
|
-
results = Object.assign(results, metadata);
|
|
670
|
-
if (
|
|
671
|
-
this.definition.restPagination &&
|
|
672
|
-
Object.keys(metadata).length > 0 &&
|
|
673
|
-
response.body.page * response.body.pageSize < response.body.count
|
|
674
|
-
) {
|
|
675
|
-
lastPage = Number(response.body.page);
|
|
676
|
-
moreResults = true;
|
|
677
|
-
} else {
|
|
678
|
-
moreResults = false;
|
|
679
|
-
}
|
|
680
|
-
} while (moreResults);
|
|
729
|
+
static async retrieveREST(retrieveDir, uri, overrideType, templateVariables, singleRetrieve) {
|
|
730
|
+
const response =
|
|
731
|
+
this.definition.restPagination && !singleRetrieve
|
|
732
|
+
? await this.client.rest.getBulk(uri)
|
|
733
|
+
: await this.client.rest.get(uri);
|
|
734
|
+
const results = this.parseResponseBody(response, singleRetrieve);
|
|
681
735
|
// get extended metadata if applicable
|
|
682
736
|
if (this.definition.hasExtended) {
|
|
683
|
-
const extended = await
|
|
684
|
-
Object.keys(results).map((key) =>
|
|
685
|
-
this.client.RestClient.get({
|
|
686
|
-
uri: uri + results[key][this.definition.idField],
|
|
687
|
-
})
|
|
688
|
-
)
|
|
737
|
+
const extended = await this.client.rest.getCollection(
|
|
738
|
+
Object.keys(results).map((key) => uri + results[key][this.definition.idField])
|
|
689
739
|
);
|
|
690
740
|
for (const ext of extended) {
|
|
691
|
-
const key = ext
|
|
692
|
-
results[key] = Object.assign(results[key], ext
|
|
741
|
+
const key = ext[this.definition.keyField];
|
|
742
|
+
results[key] = Object.assign(results[key], ext);
|
|
693
743
|
}
|
|
694
744
|
}
|
|
695
745
|
|
|
@@ -707,21 +757,27 @@ class MetadataType {
|
|
|
707
757
|
);
|
|
708
758
|
}
|
|
709
759
|
|
|
710
|
-
return {
|
|
760
|
+
return {
|
|
761
|
+
metadata: templateVariables ? Object.values(results)[0] : results,
|
|
762
|
+
type: overrideType || this.definition.type,
|
|
763
|
+
};
|
|
711
764
|
}
|
|
712
765
|
|
|
713
766
|
/**
|
|
714
767
|
* Builds map of metadata entries mapped to their keyfields
|
|
715
|
-
*
|
|
716
|
-
* @
|
|
768
|
+
*
|
|
769
|
+
* @param {object} body json of response body
|
|
770
|
+
* @param {string|number} [singleRetrieve] key of single item to filter by
|
|
771
|
+
* @returns {TYPE.MetadataTypeMap} keyField => metadata map
|
|
717
772
|
*/
|
|
718
|
-
static parseResponseBody(body) {
|
|
773
|
+
static parseResponseBody(body, singleRetrieve) {
|
|
719
774
|
const bodyIteratorField = this.definition.bodyIteratorField;
|
|
720
775
|
const keyField = this.definition.keyField;
|
|
721
776
|
const metadataStructure = {};
|
|
777
|
+
|
|
722
778
|
if (body !== null) {
|
|
723
|
-
|
|
724
|
-
|
|
779
|
+
if (Array.isArray(body)) {
|
|
780
|
+
// in some cases data is just an array
|
|
725
781
|
for (const item of body) {
|
|
726
782
|
const key = item[keyField];
|
|
727
783
|
metadataStructure[key] = item;
|
|
@@ -731,6 +787,20 @@ class MetadataType {
|
|
|
731
787
|
const key = item[keyField];
|
|
732
788
|
metadataStructure[key] = item;
|
|
733
789
|
}
|
|
790
|
+
} else if (singleRetrieve) {
|
|
791
|
+
// some types will return a single item intead of an array if the key is supported by their api
|
|
792
|
+
metadataStructure[singleRetrieve] = body;
|
|
793
|
+
|
|
794
|
+
return metadataStructure;
|
|
795
|
+
}
|
|
796
|
+
if (
|
|
797
|
+
metadataStructure[singleRetrieve] &&
|
|
798
|
+
(typeof singleRetrieve === 'string' || typeof singleRetrieve === 'number')
|
|
799
|
+
) {
|
|
800
|
+
// in case we really just wanted one entry but couldnt do so in the api call, filter it here
|
|
801
|
+
const single = {};
|
|
802
|
+
single[singleRetrieve] = metadataStructure[singleRetrieve];
|
|
803
|
+
return single;
|
|
734
804
|
}
|
|
735
805
|
}
|
|
736
806
|
return metadataStructure;
|
|
@@ -738,10 +808,11 @@ class MetadataType {
|
|
|
738
808
|
|
|
739
809
|
/**
|
|
740
810
|
* Deletes a field in a metadata entry if the selected definition property equals false.
|
|
811
|
+
*
|
|
741
812
|
* @example
|
|
742
813
|
* Removes field (or nested fields childs) that are not updateable
|
|
743
814
|
* deleteFieldByDefinition(metadataEntry, 'CustomerKey', 'isUpdateable');
|
|
744
|
-
* @param {MetadataTypeItem} metadataEntry One entry of a metadataType
|
|
815
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry One entry of a metadataType
|
|
745
816
|
* @param {string} fieldPath field path to be checked if it conforms to the definition (dot seperated if nested): 'fuu.bar'
|
|
746
817
|
* @param {'isCreateable'|'isUpdateable'|'retrieving'|'templating'} definitionProperty delete field if definitionProperty equals false for specified field. Options: [isCreateable | isUpdateable]
|
|
747
818
|
* @param {string} origin string of parent object, required when using arrays as these are parsed slightly differently.
|
|
@@ -752,30 +823,20 @@ class MetadataType {
|
|
|
752
823
|
let fieldContent;
|
|
753
824
|
try {
|
|
754
825
|
fieldContent = fieldPath.split('.').reduce((field, key) => field[key], metadataEntry);
|
|
755
|
-
} catch
|
|
826
|
+
} catch {
|
|
756
827
|
// when we hit fields that have dots in their name (e.g. interarction, metaData['simulation.id']) then this will fail
|
|
757
828
|
// decided to skip these cases for now entirely
|
|
758
829
|
return;
|
|
759
830
|
}
|
|
760
|
-
let originHelper;
|
|
761
|
-
|
|
762
831
|
// revert back placeholder to dots
|
|
763
|
-
|
|
764
|
-
originHelper = origin + '.' + fieldPath;
|
|
765
|
-
} else {
|
|
766
|
-
originHelper = fieldPath;
|
|
767
|
-
}
|
|
832
|
+
const originHelper = origin ? origin + '.' + fieldPath : fieldPath;
|
|
768
833
|
|
|
769
|
-
if (
|
|
770
|
-
this.definition.fields[originHelper] &&
|
|
771
|
-
this.definition.fields[originHelper].skipValidation
|
|
772
|
-
) {
|
|
834
|
+
if (this.definition.fields[originHelper]?.skipValidation) {
|
|
773
835
|
// skip if current field should not be validated
|
|
774
836
|
return;
|
|
775
837
|
} else if (
|
|
776
838
|
Array.isArray(fieldContent) &&
|
|
777
|
-
this.definition.fields[originHelper]
|
|
778
|
-
this.definition.fields[originHelper][definitionProperty] === true
|
|
839
|
+
this.definition.fields[originHelper]?.[definitionProperty] === true
|
|
779
840
|
) {
|
|
780
841
|
for (const subObject of fieldContent) {
|
|
781
842
|
// for simple arrays skip, only process object or array arrays further
|
|
@@ -823,7 +884,8 @@ class MetadataType {
|
|
|
823
884
|
}
|
|
824
885
|
/**
|
|
825
886
|
* Remove fields from metadata entry that are not createable
|
|
826
|
-
*
|
|
887
|
+
*
|
|
888
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
827
889
|
* @returns {void}
|
|
828
890
|
*/
|
|
829
891
|
static removeNotCreateableFields(metadataEntry) {
|
|
@@ -834,7 +896,8 @@ class MetadataType {
|
|
|
834
896
|
|
|
835
897
|
/**
|
|
836
898
|
* Remove fields from metadata entry that are not updateable
|
|
837
|
-
*
|
|
899
|
+
*
|
|
900
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
838
901
|
* @returns {void}
|
|
839
902
|
*/
|
|
840
903
|
static removeNotUpdateableFields(metadataEntry) {
|
|
@@ -845,7 +908,8 @@ class MetadataType {
|
|
|
845
908
|
|
|
846
909
|
/**
|
|
847
910
|
* Remove fields from metadata entry that are not needed in the template
|
|
848
|
-
*
|
|
911
|
+
*
|
|
912
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
849
913
|
* @returns {void}
|
|
850
914
|
*/
|
|
851
915
|
static keepTemplateFields(metadataEntry) {
|
|
@@ -856,7 +920,8 @@ class MetadataType {
|
|
|
856
920
|
|
|
857
921
|
/**
|
|
858
922
|
* Remove fields from metadata entry that are not needed in the stored metadata
|
|
859
|
-
*
|
|
923
|
+
*
|
|
924
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
860
925
|
* @returns {void}
|
|
861
926
|
*/
|
|
862
927
|
static keepRetrieveFields(metadataEntry) {
|
|
@@ -867,8 +932,9 @@ class MetadataType {
|
|
|
867
932
|
|
|
868
933
|
/**
|
|
869
934
|
* checks if the current metadata entry should be saved on retrieve or not
|
|
935
|
+
*
|
|
870
936
|
* @static
|
|
871
|
-
* @param {MetadataTypeItem} metadataEntry metadata entry
|
|
937
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
872
938
|
* @param {boolean} [include=false] true: use definition.include / options.include; false=exclude: use definition.filter / options.exclude
|
|
873
939
|
* @returns {boolean} true: skip saving == filtered; false: continue with saving
|
|
874
940
|
* @memberof MetadataType
|
|
@@ -911,14 +977,15 @@ class MetadataType {
|
|
|
911
977
|
}
|
|
912
978
|
/**
|
|
913
979
|
* optionally filter by what folder something is in
|
|
980
|
+
*
|
|
914
981
|
* @static
|
|
915
|
-
* @param {
|
|
982
|
+
* @param {object} metadataEntry metadata entry
|
|
916
983
|
* @param {boolean} [include=false] true: use definition.include / options.include; false=exclude: use definition.filter / options.exclude
|
|
917
984
|
* @returns {boolean} true: filtered == do NOT save; false: not filtered == do save
|
|
918
985
|
* @memberof MetadataType
|
|
919
986
|
*/
|
|
920
987
|
static isFilteredFolder(metadataEntry, include) {
|
|
921
|
-
if (metadataEntry.json
|
|
988
|
+
if (metadataEntry.json?.r__folder_Path) {
|
|
922
989
|
// r__folder_Path found in sub-object
|
|
923
990
|
metadataEntry = metadataEntry.json;
|
|
924
991
|
} else if (!metadataEntry.r__folder_Path) {
|
|
@@ -979,8 +1046,9 @@ class MetadataType {
|
|
|
979
1046
|
}
|
|
980
1047
|
/**
|
|
981
1048
|
* internal helper
|
|
1049
|
+
*
|
|
982
1050
|
* @private
|
|
983
|
-
* @param {
|
|
1051
|
+
* @param {object} myFilter include/exclude filter object
|
|
984
1052
|
* @param {string} r__folder_Path already determined folder path
|
|
985
1053
|
* @returns {?boolean} true: filter value found; false: filter value not found; null: no filter defined
|
|
986
1054
|
*/
|
|
@@ -1008,9 +1076,10 @@ class MetadataType {
|
|
|
1008
1076
|
}
|
|
1009
1077
|
/**
|
|
1010
1078
|
* internal helper
|
|
1079
|
+
*
|
|
1011
1080
|
* @private
|
|
1012
|
-
* @param {
|
|
1013
|
-
* @param {
|
|
1081
|
+
* @param {object} myFilter include/exclude filter object
|
|
1082
|
+
* @param {object} metadataEntry metadata entry
|
|
1014
1083
|
* @returns {?boolean} true: filter value found; false: filter value not found; null: no filter defined
|
|
1015
1084
|
*/
|
|
1016
1085
|
static _filterOther(myFilter, metadataEntry) {
|
|
@@ -1037,42 +1106,32 @@ class MetadataType {
|
|
|
1037
1106
|
return false;
|
|
1038
1107
|
}
|
|
1039
1108
|
|
|
1040
|
-
/**
|
|
1041
|
-
* Paginates a URL
|
|
1042
|
-
* @param {string} url url of the request
|
|
1043
|
-
* @param {number} last Number of the page of the last request
|
|
1044
|
-
* @returns {string} new url with pagination
|
|
1045
|
-
*/
|
|
1046
|
-
static paginate(url, last) {
|
|
1047
|
-
if (this.definition.restPagination) {
|
|
1048
|
-
const baseUrl = url.split('?')[0];
|
|
1049
|
-
const queryParams = new URLSearchParams(url.split('?')[1]);
|
|
1050
|
-
// if no page add page
|
|
1051
|
-
if (!queryParams.has('$page')) {
|
|
1052
|
-
queryParams.append('$page', (1).toString());
|
|
1053
|
-
}
|
|
1054
|
-
// if there is a page and a last value, then add to it.
|
|
1055
|
-
else if (queryParams.has('$page') && last) {
|
|
1056
|
-
queryParams.set('$page', (Number(last) + 1).toString());
|
|
1057
|
-
}
|
|
1058
|
-
return baseUrl + '?' + decodeURIComponent(queryParams.toString());
|
|
1059
|
-
} else {
|
|
1060
|
-
return url;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
1109
|
/**
|
|
1064
1110
|
* Helper for writing Metadata to disk, used for Retrieve and deploy
|
|
1065
|
-
*
|
|
1111
|
+
*
|
|
1112
|
+
* @param {TYPE.MetadataTypeMap} results metadata results from deploy
|
|
1066
1113
|
* @param {string} retrieveDir directory where metadata should be stored after deploy/retrieve
|
|
1067
1114
|
* @param {string} [overrideType] for use when there is a subtype (such as folder-queries)
|
|
1068
|
-
* @param {
|
|
1069
|
-
* @returns {Promise
|
|
1115
|
+
* @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
|
|
1116
|
+
* @returns {Promise.<TYPE.MetadataTypeMap>} Promise of saved metadata
|
|
1070
1117
|
*/
|
|
1071
1118
|
static async saveResults(results, retrieveDir, overrideType, templateVariables) {
|
|
1072
1119
|
const savedResults = {};
|
|
1073
|
-
const subtypeExtension = '.' + (overrideType || this.definition.type) + '-meta';
|
|
1074
1120
|
let filterCounter = 0;
|
|
1121
|
+
let subtypeExtension;
|
|
1075
1122
|
for (const originalKey in results) {
|
|
1123
|
+
if (this.definition.type === 'asset') {
|
|
1124
|
+
overrideType =
|
|
1125
|
+
this.definition.type +
|
|
1126
|
+
'-' +
|
|
1127
|
+
Object.keys(this.definition.extendedSubTypes).find((type) =>
|
|
1128
|
+
this.definition.extendedSubTypes[type].includes(
|
|
1129
|
+
results[originalKey].assetType.name
|
|
1130
|
+
)
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
subtypeExtension = '.' + (overrideType || this.definition.type) + '-meta';
|
|
1134
|
+
|
|
1076
1135
|
try {
|
|
1077
1136
|
if (
|
|
1078
1137
|
this.isFiltered(results[originalKey], true) ||
|
|
@@ -1082,9 +1141,9 @@ class MetadataType {
|
|
|
1082
1141
|
filterCounter++;
|
|
1083
1142
|
continue;
|
|
1084
1143
|
}
|
|
1144
|
+
|
|
1085
1145
|
// define directory into which the current metdata shall be saved
|
|
1086
1146
|
const baseDir = [retrieveDir, ...(overrideType || this.definition.type).split('-')];
|
|
1087
|
-
|
|
1088
1147
|
results[originalKey] = await this.postRetrieveTasks(
|
|
1089
1148
|
results[originalKey],
|
|
1090
1149
|
retrieveDir,
|
|
@@ -1167,38 +1226,70 @@ class MetadataType {
|
|
|
1167
1226
|
this.keepRetrieveFields(saveClone);
|
|
1168
1227
|
}
|
|
1169
1228
|
savedResults[originalKey] = saveClone;
|
|
1170
|
-
File.writeJSONToFile(
|
|
1229
|
+
await File.writeJSONToFile(
|
|
1171
1230
|
// manage subtypes
|
|
1172
1231
|
baseDir,
|
|
1173
1232
|
originalKey + subtypeExtension,
|
|
1174
1233
|
saveClone
|
|
1175
1234
|
);
|
|
1235
|
+
if (templateVariables) {
|
|
1236
|
+
Util.logger.info(
|
|
1237
|
+
`- templated ${this.definition.type}: ${
|
|
1238
|
+
saveClone[this.definition.nameField]
|
|
1239
|
+
}`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1176
1242
|
} catch (ex) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
if (filterCounter) {
|
|
1182
|
-
if (this.definition.type !== 'asset') {
|
|
1183
|
-
// interferes with progress bar in assets and is printed 1-by-1 otherwise
|
|
1184
|
-
Util.logger.info(
|
|
1185
|
-
`Filtered ${this.definition.type}: ${filterCounter} (downloaded but not saved to disk)`
|
|
1243
|
+
Util.logger.errorStack(
|
|
1244
|
+
ex,
|
|
1245
|
+
` - Saving ${this.definition.type} ${originalKey} failed`
|
|
1186
1246
|
);
|
|
1187
1247
|
}
|
|
1188
1248
|
}
|
|
1249
|
+
if (filterCounter && this.definition.type !== 'asset') {
|
|
1250
|
+
// interferes with progress bar in assets and is printed 1-by-1 otherwise
|
|
1251
|
+
Util.logger.info(
|
|
1252
|
+
` - Filtered ${this.definition.type}: ${filterCounter} (downloaded but not saved to disk)`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1189
1255
|
return savedResults;
|
|
1190
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* helper for buildDefinitionForNested
|
|
1259
|
+
* searches extracted file for template variable names and applies the market values
|
|
1260
|
+
*
|
|
1261
|
+
* @param {string} code code from extracted code
|
|
1262
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
1263
|
+
* @returns {string} code with markets applied
|
|
1264
|
+
*/
|
|
1265
|
+
static applyTemplateValues(code, templateVariables) {
|
|
1266
|
+
// replace template variables with their values
|
|
1267
|
+
return Mustache.render(code, templateVariables);
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* helper for buildTemplateForNested
|
|
1271
|
+
* searches extracted file for template variable values and applies the market variable names
|
|
1272
|
+
*
|
|
1273
|
+
* @param {string} code code from extracted code
|
|
1274
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
1275
|
+
* @returns {string} code with markets applied
|
|
1276
|
+
*/
|
|
1277
|
+
static applyTemplateNames(code, templateVariables) {
|
|
1278
|
+
// replace template variables with their values
|
|
1279
|
+
return Util.replaceByObject(code, templateVariables);
|
|
1280
|
+
}
|
|
1191
1281
|
/**
|
|
1192
1282
|
* helper for buildDefinition
|
|
1193
1283
|
* handles extracted code if any are found for complex types (e.g script, asset, query)
|
|
1284
|
+
*
|
|
1194
1285
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1195
1286
|
* @param {string} targetDir Directory where built definitions will be saved
|
|
1196
|
-
* @param {MetadataTypeItem} metadata main JSON file that was read from file system
|
|
1197
|
-
* @param {
|
|
1287
|
+
* @param {TYPE.MetadataTypeItem} metadata main JSON file that was read from file system
|
|
1288
|
+
* @param {TYPE.TemplateMap} variables variables to be replaced in the metadata
|
|
1198
1289
|
* @param {string} templateName name of the template to be built
|
|
1199
|
-
* @returns {Promise
|
|
1290
|
+
* @returns {Promise.<string[][]>} list of extracted files with path-parts provided as an array
|
|
1200
1291
|
*/
|
|
1201
|
-
static async
|
|
1292
|
+
static async buildDefinitionForNested(
|
|
1202
1293
|
templateDir,
|
|
1203
1294
|
targetDir,
|
|
1204
1295
|
metadata,
|
|
@@ -1208,23 +1299,46 @@ class MetadataType {
|
|
|
1208
1299
|
// generic version here does nothing. actual cases handled in type classes
|
|
1209
1300
|
return null;
|
|
1210
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* helper for buildTemplate
|
|
1304
|
+
* handles extracted code if any are found for complex types
|
|
1305
|
+
*
|
|
1306
|
+
* @param {string} templateDir Directory where metadata templates are stored
|
|
1307
|
+
* @param {string|string[]} targetDir (List of) Directory where built definitions will be saved
|
|
1308
|
+
* @param {TYPE.MetadataTypeItem} metadata main JSON file that was read from file system
|
|
1309
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
1310
|
+
* @param {string} templateName name of the template to be built
|
|
1311
|
+
* @returns {Promise.<string[][]>} list of extracted files with path-parts provided as an array
|
|
1312
|
+
*/
|
|
1313
|
+
static buildTemplateForNested(
|
|
1314
|
+
templateDir,
|
|
1315
|
+
targetDir,
|
|
1316
|
+
metadata,
|
|
1317
|
+
templateVariables,
|
|
1318
|
+
templateName
|
|
1319
|
+
) {
|
|
1320
|
+
// generic version here does nothing. actual cases handled in type classes
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1211
1323
|
/**
|
|
1212
1324
|
* check template directory for complex types that open subfolders for their subtypes
|
|
1325
|
+
*
|
|
1213
1326
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1214
1327
|
* @param {string} templateName name of the metadata file
|
|
1215
|
-
* @returns {string} subtype name
|
|
1328
|
+
* @returns {Promise.<string>} subtype name
|
|
1216
1329
|
*/
|
|
1217
|
-
static findSubType(templateDir, templateName) {
|
|
1330
|
+
static async findSubType(templateDir, templateName) {
|
|
1218
1331
|
return null;
|
|
1219
1332
|
}
|
|
1220
1333
|
/**
|
|
1221
1334
|
* optional method used for some types to try a different folder structure
|
|
1335
|
+
*
|
|
1222
1336
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1223
1337
|
* @param {string[]} typeDirArr current subdir for this type
|
|
1224
1338
|
* @param {string} templateName name of the metadata template
|
|
1225
1339
|
* @param {string} fileName name of the metadata template file w/o extension
|
|
1226
1340
|
* @param {Error} ex error from first attempt
|
|
1227
|
-
* @returns {
|
|
1341
|
+
* @returns {object} metadata
|
|
1228
1342
|
*/
|
|
1229
1343
|
static async readSecondaryFolder(templateDir, typeDirArr, templateName, fileName, ex) {
|
|
1230
1344
|
// we just want to push the method into the catch here
|
|
@@ -1234,17 +1348,18 @@ class MetadataType {
|
|
|
1234
1348
|
* Builds definition based on template
|
|
1235
1349
|
* NOTE: Most metadata files should use this generic method, unless custom
|
|
1236
1350
|
* parsing is required (for example scripts & queries)
|
|
1351
|
+
*
|
|
1237
1352
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1238
|
-
* @param {
|
|
1353
|
+
* @param {string | string[]} targetDir (List of) Directory where built definitions will be saved
|
|
1239
1354
|
* @param {string} templateName name of the metadata file
|
|
1240
|
-
* @param {
|
|
1241
|
-
* @returns {Promise
|
|
1355
|
+
* @param {TYPE.TemplateMap} variables variables to be replaced in the metadata
|
|
1356
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of item map
|
|
1242
1357
|
*/
|
|
1243
1358
|
static async buildDefinition(templateDir, targetDir, templateName, variables) {
|
|
1244
1359
|
// retrieve metadata template
|
|
1245
1360
|
let metadataStr;
|
|
1246
1361
|
let typeDirArr = [this.definition.type];
|
|
1247
|
-
const subType = this.findSubType(templateDir, templateName);
|
|
1362
|
+
const subType = await this.findSubType(templateDir, templateName);
|
|
1248
1363
|
if (subType) {
|
|
1249
1364
|
typeDirArr.push(subType);
|
|
1250
1365
|
}
|
|
@@ -1253,7 +1368,11 @@ class MetadataType {
|
|
|
1253
1368
|
try {
|
|
1254
1369
|
// ! do not load via readJSONFile to ensure we get a string, not parsed JSON
|
|
1255
1370
|
// templated files might contain illegal json before the conversion back to the file that shall be saved
|
|
1256
|
-
metadataStr = await File.
|
|
1371
|
+
metadataStr = await File.readFilteredFilename(
|
|
1372
|
+
[templateDir, ...typeDirArr],
|
|
1373
|
+
fileName,
|
|
1374
|
+
'json'
|
|
1375
|
+
);
|
|
1257
1376
|
} catch (ex) {
|
|
1258
1377
|
try {
|
|
1259
1378
|
metadataStr = await this.readSecondaryFolder(
|
|
@@ -1263,7 +1382,7 @@ class MetadataType {
|
|
|
1263
1382
|
fileName,
|
|
1264
1383
|
ex
|
|
1265
1384
|
);
|
|
1266
|
-
} catch
|
|
1385
|
+
} catch {
|
|
1267
1386
|
throw new Error(
|
|
1268
1387
|
`${this.definition.type}:: Could not find ./${File.normalizePath([
|
|
1269
1388
|
templateDir,
|
|
@@ -1280,7 +1399,7 @@ class MetadataType {
|
|
|
1280
1399
|
// update all initial variables & create metadata object
|
|
1281
1400
|
metadata = JSON.parse(Mustache.render(metadataStr, variables));
|
|
1282
1401
|
typeDirArr = typeDirArr.map((el) => Mustache.render(el, variables));
|
|
1283
|
-
} catch
|
|
1402
|
+
} catch {
|
|
1284
1403
|
throw new Error(
|
|
1285
1404
|
`${this.definition.type}:: Error applying template variables on ${
|
|
1286
1405
|
templateName + '.' + this.definition.type
|
|
@@ -1290,8 +1409,8 @@ class MetadataType {
|
|
|
1290
1409
|
|
|
1291
1410
|
// handle extracted code
|
|
1292
1411
|
// run after metadata was templated and converted into JS-object
|
|
1293
|
-
// templating to extracted content is applied inside of
|
|
1294
|
-
await this.
|
|
1412
|
+
// templating to extracted content is applied inside of buildDefinitionForNested()
|
|
1413
|
+
await this.buildDefinitionForNested(
|
|
1295
1414
|
templateDir,
|
|
1296
1415
|
targetDir,
|
|
1297
1416
|
metadata,
|
|
@@ -1301,24 +1420,19 @@ class MetadataType {
|
|
|
1301
1420
|
|
|
1302
1421
|
try {
|
|
1303
1422
|
// write to file
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
targetDirArr = [targetDir];
|
|
1307
|
-
} else {
|
|
1308
|
-
targetDirArr = targetDir;
|
|
1309
|
-
}
|
|
1423
|
+
const targetDirArr = !Array.isArray(targetDir) ? [targetDir] : targetDir;
|
|
1424
|
+
|
|
1310
1425
|
for (const targetDir of targetDirArr) {
|
|
1311
|
-
File.writeJSONToFile(
|
|
1426
|
+
await File.writeJSONToFile(
|
|
1312
1427
|
[targetDir, ...typeDirArr],
|
|
1313
1428
|
metadata[this.definition.keyField] + '.' + this.definition.type + suffix,
|
|
1314
1429
|
metadata
|
|
1315
1430
|
);
|
|
1316
1431
|
}
|
|
1317
1432
|
Util.logger.info(
|
|
1318
|
-
|
|
1319
|
-
this.definition.type +
|
|
1320
|
-
'].buildDefinition:: Complete - ' +
|
|
1433
|
+
`- prepared deployment definition of ${this.definition.type}: ${
|
|
1321
1434
|
metadata[this.definition.keyField]
|
|
1435
|
+
}`
|
|
1322
1436
|
);
|
|
1323
1437
|
|
|
1324
1438
|
return { metadata: metadata, type: this.definition.type };
|
|
@@ -1327,38 +1441,48 @@ class MetadataType {
|
|
|
1327
1441
|
}
|
|
1328
1442
|
}
|
|
1329
1443
|
/**
|
|
1444
|
+
* Standardizes a check for multiple messages
|
|
1330
1445
|
*
|
|
1331
|
-
* @param {
|
|
1332
|
-
* @returns {
|
|
1446
|
+
* @param {object} ex response payload from REST API
|
|
1447
|
+
* @returns {string[]} formatted Error Message
|
|
1333
1448
|
*/
|
|
1334
|
-
static checkForErrors(
|
|
1335
|
-
if (response
|
|
1449
|
+
static checkForErrors(ex) {
|
|
1450
|
+
if (ex?.response?.status >= 400 && ex?.response?.status < 600) {
|
|
1336
1451
|
const errors = [];
|
|
1337
|
-
if (response.
|
|
1338
|
-
for (const errMsg of response.
|
|
1339
|
-
errors.push(
|
|
1452
|
+
if (ex.response.data.errors) {
|
|
1453
|
+
for (const errMsg of ex.response.data.errors) {
|
|
1454
|
+
errors.push(
|
|
1455
|
+
...errMsg.message
|
|
1456
|
+
.split('<br />')
|
|
1457
|
+
.map((el) => el.trim())
|
|
1458
|
+
.filter(Boolean)
|
|
1459
|
+
);
|
|
1340
1460
|
}
|
|
1341
|
-
} else if (response.
|
|
1342
|
-
for (const errMsg of response.
|
|
1343
|
-
errors.push(
|
|
1461
|
+
} else if (ex.response.data.validationErrors) {
|
|
1462
|
+
for (const errMsg of ex.response.data.validationErrors) {
|
|
1463
|
+
errors.push(
|
|
1464
|
+
...errMsg.message
|
|
1465
|
+
.split('<br />')
|
|
1466
|
+
.map((el) => el.trim())
|
|
1467
|
+
.filter(Boolean)
|
|
1468
|
+
);
|
|
1344
1469
|
}
|
|
1345
|
-
} else if (response.
|
|
1346
|
-
errors.push(response.
|
|
1470
|
+
} else if (ex.response.data.message) {
|
|
1471
|
+
errors.push(ex.response.data.message);
|
|
1347
1472
|
} else {
|
|
1348
|
-
errors.push(`Undefined Errors: ${JSON.stringify(response.
|
|
1473
|
+
errors.push(`Undefined Errors: ${JSON.stringify(ex.response.data)}`);
|
|
1349
1474
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
)}`
|
|
1354
|
-
);
|
|
1475
|
+
Util.logger.debug(JSON.stringify(ex.config));
|
|
1476
|
+
Util.logger.debug(JSON.stringify(ex.response.data));
|
|
1477
|
+
return errors;
|
|
1355
1478
|
}
|
|
1356
1479
|
}
|
|
1357
1480
|
|
|
1358
1481
|
/**
|
|
1359
1482
|
* Gets metadata cache with limited fields and does not store value to disk
|
|
1360
|
-
*
|
|
1361
|
-
* @param {
|
|
1483
|
+
*
|
|
1484
|
+
* @param {TYPE.BuObject} [buObject] properties for auth
|
|
1485
|
+
* @param {TYPE.MetadataTypeMap} [metadata] a list of type definitions
|
|
1362
1486
|
* @param {boolean} [isDeploy] used to skip non-supported message during deploy
|
|
1363
1487
|
* @returns {void}
|
|
1364
1488
|
*/
|
|
@@ -1369,26 +1493,86 @@ class MetadataType {
|
|
|
1369
1493
|
}
|
|
1370
1494
|
|
|
1371
1495
|
/**
|
|
1372
|
-
* Delete a
|
|
1373
|
-
*
|
|
1496
|
+
* Delete a metadata item from the specified business unit
|
|
1497
|
+
*
|
|
1498
|
+
* @param {TYPE.BuObject} buObject references credentials
|
|
1374
1499
|
* @param {string} customerKey Identifier of data extension
|
|
1375
|
-
* @returns {
|
|
1500
|
+
* @returns {boolean} deletion success status
|
|
1376
1501
|
*/
|
|
1377
1502
|
static deleteByKey(buObject, customerKey) {
|
|
1378
|
-
Util.logger.error(`
|
|
1503
|
+
Util.logger.error(`Deletion is not yet supported for ${this.definition.typeName}!`);
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* clean up after deleting a metadata item
|
|
1508
|
+
*
|
|
1509
|
+
* @param {TYPE.BuObject} buObject references credentials
|
|
1510
|
+
* @param {string} customerKey Identifier of metadata item
|
|
1511
|
+
* @returns {void}
|
|
1512
|
+
*/
|
|
1513
|
+
static async postDeleteTasks(buObject, customerKey) {
|
|
1514
|
+
// delete local copy: retrieve/cred/bu/type/...json
|
|
1515
|
+
const jsonFile = File.normalizePath([
|
|
1516
|
+
this.properties.directories.retrieve,
|
|
1517
|
+
buObject.credential,
|
|
1518
|
+
buObject.businessUnit,
|
|
1519
|
+
this.definition.type,
|
|
1520
|
+
`${customerKey}.${this.definition.type}-meta.json`,
|
|
1521
|
+
]);
|
|
1522
|
+
await File.remove(jsonFile);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Delete a data extension from the specified business unit
|
|
1527
|
+
*
|
|
1528
|
+
* @param {TYPE.BuObject} buObject references credentials
|
|
1529
|
+
* @param {string} customerKey Identifier of metadata
|
|
1530
|
+
* @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
|
|
1531
|
+
* @returns {boolean} deletion success flag
|
|
1532
|
+
*/
|
|
1533
|
+
static async deleteByKeySOAP(buObject, customerKey, handleOutside) {
|
|
1534
|
+
const keyObj = {};
|
|
1535
|
+
keyObj[this.definition.keyField] = customerKey;
|
|
1536
|
+
try {
|
|
1537
|
+
this.client.soap.delete(
|
|
1538
|
+
this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
|
|
1539
|
+
keyObj,
|
|
1540
|
+
null
|
|
1541
|
+
);
|
|
1542
|
+
if (!handleOutside) {
|
|
1543
|
+
Util.logger.info(`- deleted ${this.definition.type}: ${customerKey}`);
|
|
1544
|
+
}
|
|
1545
|
+
this.postDeleteTasks(buObject, customerKey);
|
|
1546
|
+
|
|
1547
|
+
return true;
|
|
1548
|
+
} catch (ex) {
|
|
1549
|
+
if (!handleOutside) {
|
|
1550
|
+
const errorMsg = ex?.results?.length
|
|
1551
|
+
? `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`
|
|
1552
|
+
: ex.message;
|
|
1553
|
+
Util.logger.error(
|
|
1554
|
+
`- error deleting ${this.definition.type} '${customerKey}': ${errorMsg}`
|
|
1555
|
+
);
|
|
1556
|
+
} else {
|
|
1557
|
+
throw ex;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1379
1562
|
}
|
|
1380
1563
|
/**
|
|
1381
1564
|
* Returns metadata of a business unit that is saved locally
|
|
1565
|
+
*
|
|
1382
1566
|
* @param {string} readDir root directory of metadata.
|
|
1383
1567
|
* @param {boolean} [listBadKeys=false] do not print errors, used for badKeys()
|
|
1384
|
-
* @param {
|
|
1385
|
-
* @returns {
|
|
1568
|
+
* @param {object} [buMetadata] Metadata of BU in local directory
|
|
1569
|
+
* @returns {object} Metadata of BU in local directory
|
|
1386
1570
|
*/
|
|
1387
1571
|
static readBUMetadataForType(readDir, listBadKeys, buMetadata) {
|
|
1388
1572
|
buMetadata = buMetadata || {};
|
|
1389
1573
|
readDir = File.normalizePath([readDir, this.definition.type]);
|
|
1390
1574
|
try {
|
|
1391
|
-
if (File.
|
|
1575
|
+
if (File.pathExistsSync(readDir)) {
|
|
1392
1576
|
// check if folder name is a valid metadataType, then check if the user limited to a certain type in the command params
|
|
1393
1577
|
buMetadata[this.definition.type] = this.getJsonFromFS(readDir, listBadKeys);
|
|
1394
1578
|
return buMetadata;
|
|
@@ -1399,6 +1583,25 @@ class MetadataType {
|
|
|
1399
1583
|
throw new Error(ex.message);
|
|
1400
1584
|
}
|
|
1401
1585
|
}
|
|
1586
|
+
/**
|
|
1587
|
+
* should return only the json for all but asset, query and script that are saved as multiple files
|
|
1588
|
+
* additionally, the documentation for dataExtension and automation should be returned
|
|
1589
|
+
*
|
|
1590
|
+
* @param {string[]} keyArr customerkey of the metadata
|
|
1591
|
+
* @returns {Promise.<string[]>} list of all files that need to be committed in a flat array ['path/file1.ext', 'path/file2.ext']
|
|
1592
|
+
*/
|
|
1593
|
+
static getFilesToCommit(keyArr) {
|
|
1594
|
+
const typeExtension = '.' + this.definition.type + '-meta.json';
|
|
1595
|
+
const path = File.normalizePath([
|
|
1596
|
+
this.properties.directories.retrieve,
|
|
1597
|
+
this.buObject.credential,
|
|
1598
|
+
this.buObject.businessUnit,
|
|
1599
|
+
this.definition.type,
|
|
1600
|
+
]);
|
|
1601
|
+
|
|
1602
|
+
const fileList = keyArr.map((key) => File.normalizePath([path, key + typeExtension]));
|
|
1603
|
+
return fileList;
|
|
1604
|
+
}
|
|
1402
1605
|
}
|
|
1403
1606
|
|
|
1404
1607
|
MetadataType.definition = {
|
|
@@ -1412,13 +1615,20 @@ MetadataType.definition = {
|
|
|
1412
1615
|
type: '',
|
|
1413
1616
|
};
|
|
1414
1617
|
/**
|
|
1415
|
-
* @type {
|
|
1618
|
+
* @type {TYPE.SDK}
|
|
1416
1619
|
*/
|
|
1417
1620
|
MetadataType.client = undefined;
|
|
1418
1621
|
/**
|
|
1419
|
-
* @type {
|
|
1622
|
+
* @type {TYPE.Mcdevrc}
|
|
1420
1623
|
*/
|
|
1421
|
-
MetadataType.cache = {};
|
|
1422
1624
|
MetadataType.properties = null;
|
|
1625
|
+
/**
|
|
1626
|
+
* @type {string}
|
|
1627
|
+
*/
|
|
1628
|
+
MetadataType.subType = null;
|
|
1629
|
+
/**
|
|
1630
|
+
* @type {TYPE.BuObject}
|
|
1631
|
+
*/
|
|
1632
|
+
MetadataType.buObject = null;
|
|
1423
1633
|
|
|
1424
1634
|
module.exports = MetadataType;
|