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
|
@@ -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,235 @@ 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(
|
|
263
|
+
[templateDir, ...typeDirArr],
|
|
264
|
+
key + '.' + this.definition.type + suffix,
|
|
265
|
+
metadata
|
|
266
|
+
);
|
|
267
|
+
Util.logger.info(
|
|
268
|
+
`- templated ${this.definition.type}: ${key} (${
|
|
269
|
+
metadata[this.definition.nameField]
|
|
270
|
+
})`
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return { metadata: metadata, type: this.definition.type };
|
|
274
|
+
} catch (ex) {
|
|
275
|
+
throw new Error(`${this.definition.type}:: ${ex.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
245
278
|
|
|
246
279
|
/**
|
|
247
280
|
* Gets executed before deploying metadata
|
|
248
|
-
*
|
|
281
|
+
*
|
|
282
|
+
* @param {TYPE.MetadataTypeItem} metadata a single metadata item
|
|
249
283
|
* @param {string} deployDir folder where files for deployment are stored
|
|
250
|
-
* @
|
|
284
|
+
* @param {TYPE.BuObject} buObject buObject properties for auth
|
|
285
|
+
* @returns {Promise.<TYPE.MetadataTypeItem>} Promise of a single metadata item
|
|
251
286
|
*/
|
|
252
|
-
static async preDeployTasks(metadata, deployDir) {
|
|
287
|
+
static async preDeployTasks(metadata, deployDir, buObject) {
|
|
253
288
|
return metadata;
|
|
254
289
|
}
|
|
255
290
|
|
|
256
291
|
/**
|
|
257
292
|
* Abstract create method that needs to be implemented in child metadata type
|
|
258
|
-
*
|
|
293
|
+
*
|
|
294
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata entry
|
|
259
295
|
* @param {string} deployDir directory where deploy metadata are saved
|
|
260
296
|
* @returns {void}
|
|
261
297
|
*/
|
|
262
298
|
static create(metadata, deployDir) {
|
|
263
|
-
Util.
|
|
299
|
+
Util.logger.error(
|
|
300
|
+
` ☇ skipping ${this.definition.type} ${metadata[this.definition.keyField]} / ${
|
|
301
|
+
metadata[this.definition.nameField]
|
|
302
|
+
}: create is not supported yet for ${this.definition.type}`
|
|
303
|
+
);
|
|
264
304
|
return;
|
|
265
305
|
}
|
|
266
306
|
|
|
267
307
|
/**
|
|
268
308
|
* Abstract update method that needs to be implemented in child metadata type
|
|
269
|
-
*
|
|
270
|
-
* @param {MetadataTypeItem}
|
|
309
|
+
*
|
|
310
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata entry
|
|
311
|
+
* @param {TYPE.MetadataTypeItem} [metadataBefore] metadata mapped by their keyField
|
|
271
312
|
* @returns {void}
|
|
272
313
|
*/
|
|
273
314
|
static update(metadata, metadataBefore) {
|
|
274
|
-
Util.
|
|
315
|
+
Util.logger.error(
|
|
316
|
+
` ☇ skipping ${this.definition.type} ${metadata[this.definition.keyField]} / ${
|
|
317
|
+
metadata[this.definition.nameField]
|
|
318
|
+
}: update is not supported yet for ${this.definition.type}`
|
|
319
|
+
);
|
|
275
320
|
return;
|
|
276
321
|
}
|
|
277
322
|
|
|
323
|
+
/**
|
|
324
|
+
* test if metadata was actually changed or not to potentially skip it during deployment
|
|
325
|
+
*
|
|
326
|
+
* @param {TYPE.MetadataTypeItem} cachedVersion cached version from the server
|
|
327
|
+
* @param {TYPE.MetadataTypeItem} metadata item to upload
|
|
328
|
+
* @param {string} [fieldName] optional field name to use for identifying the record in logs
|
|
329
|
+
* @returns {boolean} true if metadata was changed
|
|
330
|
+
*/
|
|
331
|
+
static hasChanged(cachedVersion, metadata, fieldName) {
|
|
332
|
+
// should be set up type by type but the *_generic version is likely a good start for many types
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* test if metadata was actually changed or not to potentially skip it during deployment
|
|
337
|
+
*
|
|
338
|
+
* @param {TYPE.MetadataTypeItem} cachedVersion cached version from the server
|
|
339
|
+
* @param {TYPE.MetadataTypeItem} metadata item to upload
|
|
340
|
+
* @param {string} [fieldName] optional field name to use for identifying the record in logs
|
|
341
|
+
* @param {boolean} [silent] optionally suppress logging
|
|
342
|
+
* @returns {boolean} true on first identified deviation or false if none are found
|
|
343
|
+
*/
|
|
344
|
+
static hasChangedGeneric(cachedVersion, metadata, fieldName, silent) {
|
|
345
|
+
if (!cachedVersion) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
// we do need the full set in other places and hence need to work with a clone here
|
|
349
|
+
const clonedMetada = JSON.parse(JSON.stringify(metadata));
|
|
350
|
+
this.removeNotUpdateableFields(clonedMetada);
|
|
351
|
+
// iterate over what we want to upload rather than what we cached to avoid false positives
|
|
352
|
+
for (const prop in clonedMetada) {
|
|
353
|
+
if (this.definition.ignoreFieldsForUpdateCheck?.includes(prop)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (
|
|
357
|
+
clonedMetada[prop] === null ||
|
|
358
|
+
['string', 'number', 'boolean'].includes(typeof clonedMetada[prop])
|
|
359
|
+
) {
|
|
360
|
+
// check simple variables directly
|
|
361
|
+
// check should ignore types to bypass string/number auto-conversions caused by SFMC-SDK
|
|
362
|
+
if (clonedMetada[prop] != cachedVersion[prop]) {
|
|
363
|
+
Util.logger.debug(
|
|
364
|
+
`${this.definition.type}:: ${
|
|
365
|
+
clonedMetada[fieldName || this.definition.keyField]
|
|
366
|
+
}.${prop} changed: '${cachedVersion[prop]}' to '${clonedMetada[prop]}'`
|
|
367
|
+
);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
} else if (deepEqual(clonedMetada[prop], cachedVersion[prop])) {
|
|
371
|
+
// test complex objects here
|
|
372
|
+
Util.logger.debug(
|
|
373
|
+
`${this.definition.type}:: ${
|
|
374
|
+
clonedMetada[fieldName || this.definition.keyField]
|
|
375
|
+
}.${prop} changed: '${cachedVersion[prop]}' to '${clonedMetada[prop]}'`
|
|
376
|
+
);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (!silent) {
|
|
381
|
+
Util.logger.verbose(
|
|
382
|
+
` ☇ skipping ${this.definition.type} ${clonedMetada[this.definition.keyField]} / ${
|
|
383
|
+
clonedMetada[fieldName || this.definition.nameField]
|
|
384
|
+
}: no change detected`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
278
389
|
/**
|
|
279
390
|
* MetadataType upsert, after retrieving from target and comparing to check if create or update operation is needed.
|
|
280
|
-
*
|
|
391
|
+
*
|
|
392
|
+
* @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
|
|
281
393
|
* @param {string} deployDir directory where deploy metadata are saved
|
|
282
|
-
* @param {
|
|
283
|
-
* @returns {Promise
|
|
394
|
+
* @param {TYPE.BuObject} [buObject] properties for auth
|
|
395
|
+
* @returns {Promise.<TYPE.MetadataTypeMap>} keyField => metadata map
|
|
284
396
|
*/
|
|
285
397
|
static async upsert(metadata, deployDir, buObject) {
|
|
286
398
|
const metadataToUpdate = [];
|
|
287
399
|
const metadataToCreate = [];
|
|
400
|
+
let filteredByPreDeploy = 0;
|
|
288
401
|
for (const metadataKey in metadata) {
|
|
402
|
+
let hasError = false;
|
|
289
403
|
try {
|
|
290
404
|
// preDeployTasks parsing
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
405
|
+
let deployableMetadata;
|
|
406
|
+
try {
|
|
407
|
+
deployableMetadata = await this.preDeployTasks(
|
|
408
|
+
metadata[metadataKey],
|
|
409
|
+
deployDir,
|
|
410
|
+
buObject
|
|
411
|
+
);
|
|
412
|
+
} catch (ex) {
|
|
413
|
+
// do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
|
|
414
|
+
hasError = true;
|
|
415
|
+
deployableMetadata = metadata[metadataKey];
|
|
416
|
+
Util.logger.error(
|
|
417
|
+
` ☇ skipping ${this.definition.type} ${
|
|
418
|
+
deployableMetadata[this.definition.keyField]
|
|
419
|
+
} / ${deployableMetadata[this.definition.nameField]}: ${ex.message}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
295
422
|
// if preDeploy returns nothing then it cannot be deployed so skip deployment
|
|
296
423
|
if (deployableMetadata) {
|
|
297
424
|
metadata[metadataKey] = deployableMetadata;
|
|
298
|
-
|
|
425
|
+
// create normalizedKey off of whats in the json rather than from "metadataKey" because preDeployTasks might have altered something (type asset)
|
|
426
|
+
const normalizedKey = File.reverseFilterIllegalFilenames(
|
|
427
|
+
deployableMetadata[this.definition.keyField]
|
|
428
|
+
);
|
|
299
429
|
// Update if it already exists; Create it if not
|
|
300
430
|
if (
|
|
301
431
|
Util.logger.level === 'debug' &&
|
|
@@ -305,52 +435,79 @@ class MetadataType {
|
|
|
305
435
|
// only used if resource is excluded from cache and we still want to update it
|
|
306
436
|
// needed e.g. to rewire lost folders
|
|
307
437
|
Util.logger.warn(
|
|
308
|
-
'Hotfix for non-cachable resource found in deploy folder. Trying update:'
|
|
438
|
+
' - Hotfix for non-cachable resource found in deploy folder. Trying update:'
|
|
309
439
|
);
|
|
310
440
|
Util.logger.warn(JSON.stringify(metadata[metadataKey]));
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
441
|
+
if (hasError) {
|
|
442
|
+
metadataToUpdate.push(null);
|
|
443
|
+
} else {
|
|
444
|
+
metadataToUpdate.push({
|
|
445
|
+
before: {},
|
|
446
|
+
after: metadata[metadataKey],
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
} else if (cache.getByKey(this.definition.type, normalizedKey)) {
|
|
316
450
|
// normal way of processing update files
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
451
|
+
const cachedVersion = cache.getByKey(this.definition.type, normalizedKey);
|
|
452
|
+
if (!this.hasChanged(cachedVersion, metadata[metadataKey])) {
|
|
453
|
+
hasError = true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (hasError) {
|
|
457
|
+
// do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
|
|
458
|
+
metadataToUpdate.push(null);
|
|
459
|
+
} else {
|
|
460
|
+
// add ObjectId to allow actual update
|
|
461
|
+
metadata[metadataKey][this.definition.idField] =
|
|
462
|
+
cachedVersion[this.definition.idField];
|
|
463
|
+
|
|
464
|
+
metadataToUpdate.push({
|
|
465
|
+
before: cache.getByKey(this.definition.type, normalizedKey),
|
|
466
|
+
after: metadata[metadataKey],
|
|
467
|
+
});
|
|
468
|
+
}
|
|
325
469
|
} else {
|
|
326
|
-
|
|
470
|
+
if (hasError) {
|
|
471
|
+
// do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
|
|
472
|
+
metadataToCreate.push(null);
|
|
473
|
+
} else {
|
|
474
|
+
metadataToCreate.push(metadata[metadataKey]);
|
|
475
|
+
}
|
|
327
476
|
}
|
|
477
|
+
} else {
|
|
478
|
+
filteredByPreDeploy++;
|
|
328
479
|
}
|
|
329
480
|
} catch (ex) {
|
|
330
|
-
Util.
|
|
481
|
+
Util.logger.errorStack(
|
|
482
|
+
ex,
|
|
483
|
+
`Upserting ${this.definition.type} failed: ${ex.message}`
|
|
484
|
+
);
|
|
331
485
|
}
|
|
332
486
|
}
|
|
333
|
-
|
|
487
|
+
const createLimit = pLimit(10);
|
|
334
488
|
const createResults = (
|
|
335
|
-
await Promise.
|
|
336
|
-
metadataToCreate
|
|
337
|
-
|
|
338
|
-
|
|
489
|
+
await Promise.all(
|
|
490
|
+
metadataToCreate
|
|
491
|
+
.filter((r) => r !== undefined && r !== null)
|
|
492
|
+
.map((metadataEntry) =>
|
|
493
|
+
createLimit(() => this.create(metadataEntry, deployDir))
|
|
494
|
+
)
|
|
339
495
|
)
|
|
340
496
|
).filter((r) => r !== undefined && r !== null);
|
|
497
|
+
const updateLimit = pLimit(10);
|
|
341
498
|
const updateResults = (
|
|
342
|
-
await Promise.
|
|
343
|
-
metadataToUpdate
|
|
344
|
-
|
|
345
|
-
|
|
499
|
+
await Promise.all(
|
|
500
|
+
metadataToUpdate
|
|
501
|
+
.filter((r) => r !== undefined && r !== null)
|
|
502
|
+
.map((metadataEntry) =>
|
|
503
|
+
updateLimit(() => this.update(metadataEntry.after, metadataEntry.before))
|
|
504
|
+
)
|
|
346
505
|
)
|
|
347
506
|
).filter((r) => r !== undefined && r !== null);
|
|
348
507
|
// Logging
|
|
349
|
-
Util.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
'upsert',
|
|
353
|
-
`${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated`
|
|
508
|
+
Util.logger.info(
|
|
509
|
+
`${this.definition.type} upsert: ${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated` +
|
|
510
|
+
(filteredByPreDeploy > 0 ? ` / ${filteredByPreDeploy} filtered` : '')
|
|
354
511
|
);
|
|
355
512
|
|
|
356
513
|
// if Results then parse as SOAP
|
|
@@ -360,60 +517,57 @@ class MetadataType {
|
|
|
360
517
|
// @ts-ignore
|
|
361
518
|
const metadataResults = createResults
|
|
362
519
|
.concat(updateResults)
|
|
363
|
-
.
|
|
364
|
-
.
|
|
365
|
-
.
|
|
520
|
+
// TODO remove Object.keys check after create/update SOAP methods stop returning empty objects instead of null
|
|
521
|
+
.filter((r) => r !== undefined && r !== null && Object.keys(r).length !== 0)
|
|
522
|
+
.flatMap((r) => r.Results)
|
|
366
523
|
.map((r) => r.Object);
|
|
367
524
|
return this.parseResponseBody({ Results: metadataResults });
|
|
368
525
|
} else {
|
|
369
526
|
// put in Retrieve Format for parsing
|
|
370
527
|
// todo add handling when response does not contain items.
|
|
371
528
|
// @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);
|
|
529
|
+
const metadataResults = createResults.concat(updateResults).filter(Boolean);
|
|
376
530
|
return this.parseResponseBody({ items: metadataResults });
|
|
377
531
|
}
|
|
378
532
|
}
|
|
379
533
|
|
|
380
534
|
/**
|
|
381
535
|
* Creates a single metadata entry via REST
|
|
382
|
-
*
|
|
536
|
+
*
|
|
537
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry a single metadata Entry
|
|
383
538
|
* @param {string} uri rest endpoint for POST
|
|
384
539
|
* @returns {Promise} Promise
|
|
385
540
|
*/
|
|
386
541
|
static async createREST(metadataEntry, uri) {
|
|
387
542
|
this.removeNotCreateableFields(metadataEntry);
|
|
388
|
-
const options = {
|
|
389
|
-
uri: uri,
|
|
390
|
-
json: metadataEntry,
|
|
391
|
-
headers: {},
|
|
392
|
-
};
|
|
393
543
|
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);
|
|
544
|
+
const response = await this.client.rest.post(uri, metadataEntry);
|
|
400
545
|
Util.logger.info(
|
|
401
|
-
|
|
546
|
+
` - created ${this.definition.type}: ${
|
|
547
|
+
metadataEntry[this.definition.keyField] ||
|
|
548
|
+
metadataEntry[this.definition.nameField]
|
|
549
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
402
550
|
);
|
|
403
551
|
return response;
|
|
404
552
|
} catch (ex) {
|
|
553
|
+
const parsedErrors = this.checkForErrors(ex);
|
|
405
554
|
Util.logger.error(
|
|
406
|
-
|
|
407
|
-
metadataEntry[this.definition.keyField]
|
|
408
|
-
|
|
555
|
+
` ☇ error creating ${this.definition.type} ${
|
|
556
|
+
metadataEntry[this.definition.keyField] ||
|
|
557
|
+
metadataEntry[this.definition.nameField]
|
|
558
|
+
} / ${metadataEntry[this.definition.nameField]}:`
|
|
409
559
|
);
|
|
560
|
+
for (const msg of parsedErrors) {
|
|
561
|
+
Util.logger.error(' • ' + msg);
|
|
562
|
+
}
|
|
410
563
|
return null;
|
|
411
564
|
}
|
|
412
565
|
}
|
|
413
566
|
|
|
414
567
|
/**
|
|
415
568
|
* Creates a single metadata entry via fuel-soap (generic lib not wrapper)
|
|
416
|
-
*
|
|
569
|
+
*
|
|
570
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry single metadata entry
|
|
417
571
|
* @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
|
|
418
572
|
* @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
|
|
419
573
|
* @returns {Promise} Promise
|
|
@@ -421,39 +575,29 @@ class MetadataType {
|
|
|
421
575
|
static async createSOAP(metadataEntry, overrideType, handleOutside) {
|
|
422
576
|
try {
|
|
423
577
|
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
|
-
}))
|
|
578
|
+
const response = await this.client.soap.create(
|
|
579
|
+
overrideType ||
|
|
580
|
+
this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
|
|
581
|
+
metadataEntry,
|
|
582
|
+
null
|
|
440
583
|
);
|
|
584
|
+
|
|
441
585
|
if (!handleOutside) {
|
|
442
586
|
Util.logger.info(
|
|
443
|
-
|
|
587
|
+
` - created ${this.definition.type}: ${
|
|
588
|
+
metadataEntry[this.definition.keyField]
|
|
589
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
444
590
|
);
|
|
445
591
|
}
|
|
446
592
|
return response;
|
|
447
593
|
} catch (ex) {
|
|
448
594
|
if (!handleOutside) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
errorMsg = ex.message;
|
|
454
|
-
}
|
|
595
|
+
const errorMsg =
|
|
596
|
+
ex.results && ex.results.length
|
|
597
|
+
? `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`
|
|
598
|
+
: ex.message;
|
|
455
599
|
Util.logger.error(
|
|
456
|
-
|
|
600
|
+
` ☇ error creating ${this.definition.type} '${
|
|
457
601
|
metadataEntry[this.definition.keyField]
|
|
458
602
|
}': ${errorMsg}`
|
|
459
603
|
);
|
|
@@ -467,85 +611,71 @@ class MetadataType {
|
|
|
467
611
|
|
|
468
612
|
/**
|
|
469
613
|
* Updates a single metadata entry via REST
|
|
470
|
-
*
|
|
614
|
+
*
|
|
615
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry a single metadata Entry
|
|
471
616
|
* @param {string} uri rest endpoint for PATCH
|
|
472
617
|
* @returns {Promise} Promise
|
|
473
618
|
*/
|
|
474
619
|
static async updateREST(metadataEntry, uri) {
|
|
475
620
|
this.removeNotUpdateableFields(metadataEntry);
|
|
476
|
-
const options = {
|
|
477
|
-
uri: uri,
|
|
478
|
-
json: metadataEntry,
|
|
479
|
-
headers: {},
|
|
480
|
-
};
|
|
481
621
|
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
|
-
);
|
|
622
|
+
const response = await this.client.rest.patch(uri, metadataEntry);
|
|
487
623
|
this.checkForErrors(response);
|
|
488
624
|
// some times, e.g. automation dont return a key in their update response and hence we need to fall back to name
|
|
489
625
|
Util.logger.info(
|
|
490
|
-
|
|
626
|
+
` - updated ${this.definition.type}: ${
|
|
491
627
|
metadataEntry[this.definition.keyField] ||
|
|
492
628
|
metadataEntry[this.definition.nameField]
|
|
493
|
-
}`
|
|
629
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
494
630
|
);
|
|
495
631
|
return response;
|
|
496
632
|
} catch (ex) {
|
|
633
|
+
const parsedErrors = this.checkForErrors(ex);
|
|
497
634
|
Util.logger.error(
|
|
498
|
-
|
|
499
|
-
metadataEntry[this.definition.keyField]
|
|
500
|
-
|
|
635
|
+
` ☇ error updating ${this.definition.type} ${
|
|
636
|
+
metadataEntry[this.definition.keyField] ||
|
|
637
|
+
metadataEntry[this.definition.nameField]
|
|
638
|
+
} / ${metadataEntry[this.definition.nameField]}:`
|
|
501
639
|
);
|
|
640
|
+
for (const msg of parsedErrors) {
|
|
641
|
+
Util.logger.error(' • ' + msg);
|
|
642
|
+
}
|
|
502
643
|
return null;
|
|
503
644
|
}
|
|
504
645
|
}
|
|
505
646
|
|
|
506
647
|
/**
|
|
507
648
|
* Updates a single metadata entry via fuel-soap (generic lib not wrapper)
|
|
508
|
-
*
|
|
649
|
+
*
|
|
650
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry single metadata entry
|
|
509
651
|
* @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
|
|
510
652
|
* @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
|
|
511
653
|
* @returns {Promise} Promise
|
|
512
654
|
*/
|
|
513
655
|
static async updateSOAP(metadataEntry, overrideType, handleOutside) {
|
|
514
|
-
let response;
|
|
515
656
|
try {
|
|
516
657
|
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
|
-
}))
|
|
658
|
+
const response = await this.client.soap.update(
|
|
659
|
+
overrideType ||
|
|
660
|
+
this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
|
|
661
|
+
metadataEntry,
|
|
662
|
+
null
|
|
532
663
|
);
|
|
533
664
|
if (!handleOutside) {
|
|
534
665
|
Util.logger.info(
|
|
535
|
-
|
|
666
|
+
` - updated ${this.definition.type}: ${
|
|
667
|
+
metadataEntry[this.definition.keyField]
|
|
668
|
+
} / ${metadataEntry[this.definition.nameField]}`
|
|
536
669
|
);
|
|
537
670
|
}
|
|
538
671
|
return response;
|
|
539
672
|
} catch (ex) {
|
|
540
673
|
if (!handleOutside) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
} else {
|
|
545
|
-
errorMsg = ex.message;
|
|
546
|
-
}
|
|
674
|
+
const errorMsg = ex?.json?.Results.length
|
|
675
|
+
? `${ex.json.Results[0].StatusMessage} (Code ${ex.json.Results[0].ErrorCode})`
|
|
676
|
+
: ex.message;
|
|
547
677
|
Util.logger.error(
|
|
548
|
-
|
|
678
|
+
` ☇ error updating ${this.definition.type} '${
|
|
549
679
|
metadataEntry[this.definition.keyField]
|
|
550
680
|
}': ${errorMsg}`
|
|
551
681
|
);
|
|
@@ -558,23 +688,23 @@ class MetadataType {
|
|
|
558
688
|
}
|
|
559
689
|
/**
|
|
560
690
|
* Retrieves SOAP via generic fuel-soap wrapper based metadata of metadata type into local filesystem. executes callback with retrieved metadata
|
|
691
|
+
*
|
|
561
692
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
562
|
-
* @param {
|
|
563
|
-
* @param {
|
|
693
|
+
* @param {TYPE.BuObject} buObject properties for auth
|
|
694
|
+
* @param {TYPE.SoapRequestParams} [requestParams] required for the specific request (filter for example)
|
|
564
695
|
* @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
|
|
696
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of item map
|
|
567
697
|
*/
|
|
568
|
-
static async
|
|
569
|
-
|
|
570
|
-
buObject,
|
|
571
|
-
options,
|
|
572
|
-
additionalFields,
|
|
573
|
-
overrideType
|
|
574
|
-
) {
|
|
698
|
+
static async retrieveSOAP(retrieveDir, buObject, requestParams, additionalFields) {
|
|
699
|
+
requestParams = requestParams || {};
|
|
575
700
|
const fields = this.getFieldNamesToRetrieve(additionalFields);
|
|
701
|
+
const response = await this.client.soap.retrieveBulk(
|
|
702
|
+
this.definition.type,
|
|
703
|
+
fields,
|
|
704
|
+
requestParams
|
|
705
|
+
);
|
|
706
|
+
const metadata = this.parseResponseBody(response);
|
|
576
707
|
|
|
577
|
-
const metadata = await this.retrieveSOAPBody(fields, options, overrideType);
|
|
578
708
|
if (retrieveDir) {
|
|
579
709
|
const savedMetadata = await this.saveResults(metadata, retrieveDir, null);
|
|
580
710
|
Util.logger.info(
|
|
@@ -589,107 +719,31 @@ class MetadataType {
|
|
|
589
719
|
}
|
|
590
720
|
return { metadata: metadata, type: this.definition.type };
|
|
591
721
|
}
|
|
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
722
|
|
|
646
723
|
/**
|
|
647
724
|
* Retrieves Metadata for Rest Types
|
|
725
|
+
*
|
|
648
726
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
649
727
|
* @param {string} uri rest endpoint for GET
|
|
650
728
|
* @param {string} [overrideType] force a metadata type (mainly used for Folders)
|
|
651
|
-
* @param {
|
|
652
|
-
* @
|
|
729
|
+
* @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
|
|
730
|
+
* @param {string|number} [singleRetrieve] key of single item to filter by
|
|
731
|
+
* @returns {Promise.<{metadata: (TYPE.MetadataTypeMap | TYPE.MetadataTypeItem), type: string}>} Promise of item map (single item for templated result)
|
|
653
732
|
*/
|
|
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);
|
|
733
|
+
static async retrieveREST(retrieveDir, uri, overrideType, templateVariables, singleRetrieve) {
|
|
734
|
+
const response =
|
|
735
|
+
this.definition.restPagination && !singleRetrieve
|
|
736
|
+
? await this.client.rest.getBulk(uri)
|
|
737
|
+
: await this.client.rest.get(uri);
|
|
738
|
+
const results = this.parseResponseBody(response, singleRetrieve);
|
|
681
739
|
// get extended metadata if applicable
|
|
682
740
|
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
|
-
)
|
|
741
|
+
const extended = await this.client.rest.getCollection(
|
|
742
|
+
Object.keys(results).map((key) => uri + results[key][this.definition.idField])
|
|
689
743
|
);
|
|
690
744
|
for (const ext of extended) {
|
|
691
|
-
const key = ext
|
|
692
|
-
results[key] = Object.assign(results[key], ext
|
|
745
|
+
const key = ext[this.definition.keyField];
|
|
746
|
+
results[key] = Object.assign(results[key], ext);
|
|
693
747
|
}
|
|
694
748
|
}
|
|
695
749
|
|
|
@@ -707,21 +761,27 @@ class MetadataType {
|
|
|
707
761
|
);
|
|
708
762
|
}
|
|
709
763
|
|
|
710
|
-
return {
|
|
764
|
+
return {
|
|
765
|
+
metadata: templateVariables ? Object.values(results)[0] : results,
|
|
766
|
+
type: overrideType || this.definition.type,
|
|
767
|
+
};
|
|
711
768
|
}
|
|
712
769
|
|
|
713
770
|
/**
|
|
714
771
|
* Builds map of metadata entries mapped to their keyfields
|
|
715
|
-
*
|
|
716
|
-
* @
|
|
772
|
+
*
|
|
773
|
+
* @param {object} body json of response body
|
|
774
|
+
* @param {string|number} [singleRetrieve] key of single item to filter by
|
|
775
|
+
* @returns {TYPE.MetadataTypeMap} keyField => metadata map
|
|
717
776
|
*/
|
|
718
|
-
static parseResponseBody(body) {
|
|
777
|
+
static parseResponseBody(body, singleRetrieve) {
|
|
719
778
|
const bodyIteratorField = this.definition.bodyIteratorField;
|
|
720
779
|
const keyField = this.definition.keyField;
|
|
721
780
|
const metadataStructure = {};
|
|
781
|
+
|
|
722
782
|
if (body !== null) {
|
|
723
|
-
|
|
724
|
-
|
|
783
|
+
if (Array.isArray(body)) {
|
|
784
|
+
// in some cases data is just an array
|
|
725
785
|
for (const item of body) {
|
|
726
786
|
const key = item[keyField];
|
|
727
787
|
metadataStructure[key] = item;
|
|
@@ -731,6 +791,20 @@ class MetadataType {
|
|
|
731
791
|
const key = item[keyField];
|
|
732
792
|
metadataStructure[key] = item;
|
|
733
793
|
}
|
|
794
|
+
} else if (singleRetrieve) {
|
|
795
|
+
// some types will return a single item intead of an array if the key is supported by their api
|
|
796
|
+
metadataStructure[singleRetrieve] = body;
|
|
797
|
+
|
|
798
|
+
return metadataStructure;
|
|
799
|
+
}
|
|
800
|
+
if (
|
|
801
|
+
metadataStructure[singleRetrieve] &&
|
|
802
|
+
(typeof singleRetrieve === 'string' || typeof singleRetrieve === 'number')
|
|
803
|
+
) {
|
|
804
|
+
// in case we really just wanted one entry but couldnt do so in the api call, filter it here
|
|
805
|
+
const single = {};
|
|
806
|
+
single[singleRetrieve] = metadataStructure[singleRetrieve];
|
|
807
|
+
return single;
|
|
734
808
|
}
|
|
735
809
|
}
|
|
736
810
|
return metadataStructure;
|
|
@@ -738,10 +812,11 @@ class MetadataType {
|
|
|
738
812
|
|
|
739
813
|
/**
|
|
740
814
|
* Deletes a field in a metadata entry if the selected definition property equals false.
|
|
815
|
+
*
|
|
741
816
|
* @example
|
|
742
817
|
* Removes field (or nested fields childs) that are not updateable
|
|
743
818
|
* deleteFieldByDefinition(metadataEntry, 'CustomerKey', 'isUpdateable');
|
|
744
|
-
* @param {MetadataTypeItem} metadataEntry One entry of a metadataType
|
|
819
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry One entry of a metadataType
|
|
745
820
|
* @param {string} fieldPath field path to be checked if it conforms to the definition (dot seperated if nested): 'fuu.bar'
|
|
746
821
|
* @param {'isCreateable'|'isUpdateable'|'retrieving'|'templating'} definitionProperty delete field if definitionProperty equals false for specified field. Options: [isCreateable | isUpdateable]
|
|
747
822
|
* @param {string} origin string of parent object, required when using arrays as these are parsed slightly differently.
|
|
@@ -752,30 +827,20 @@ class MetadataType {
|
|
|
752
827
|
let fieldContent;
|
|
753
828
|
try {
|
|
754
829
|
fieldContent = fieldPath.split('.').reduce((field, key) => field[key], metadataEntry);
|
|
755
|
-
} catch
|
|
830
|
+
} catch {
|
|
756
831
|
// when we hit fields that have dots in their name (e.g. interarction, metaData['simulation.id']) then this will fail
|
|
757
832
|
// decided to skip these cases for now entirely
|
|
758
833
|
return;
|
|
759
834
|
}
|
|
760
|
-
let originHelper;
|
|
761
|
-
|
|
762
835
|
// revert back placeholder to dots
|
|
763
|
-
|
|
764
|
-
originHelper = origin + '.' + fieldPath;
|
|
765
|
-
} else {
|
|
766
|
-
originHelper = fieldPath;
|
|
767
|
-
}
|
|
836
|
+
const originHelper = origin ? origin + '.' + fieldPath : fieldPath;
|
|
768
837
|
|
|
769
|
-
if (
|
|
770
|
-
this.definition.fields[originHelper] &&
|
|
771
|
-
this.definition.fields[originHelper].skipValidation
|
|
772
|
-
) {
|
|
838
|
+
if (this.definition.fields[originHelper]?.skipValidation) {
|
|
773
839
|
// skip if current field should not be validated
|
|
774
840
|
return;
|
|
775
841
|
} else if (
|
|
776
842
|
Array.isArray(fieldContent) &&
|
|
777
|
-
this.definition.fields[originHelper]
|
|
778
|
-
this.definition.fields[originHelper][definitionProperty] === true
|
|
843
|
+
this.definition.fields[originHelper]?.[definitionProperty] === true
|
|
779
844
|
) {
|
|
780
845
|
for (const subObject of fieldContent) {
|
|
781
846
|
// for simple arrays skip, only process object or array arrays further
|
|
@@ -823,7 +888,8 @@ class MetadataType {
|
|
|
823
888
|
}
|
|
824
889
|
/**
|
|
825
890
|
* Remove fields from metadata entry that are not createable
|
|
826
|
-
*
|
|
891
|
+
*
|
|
892
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
827
893
|
* @returns {void}
|
|
828
894
|
*/
|
|
829
895
|
static removeNotCreateableFields(metadataEntry) {
|
|
@@ -834,7 +900,8 @@ class MetadataType {
|
|
|
834
900
|
|
|
835
901
|
/**
|
|
836
902
|
* Remove fields from metadata entry that are not updateable
|
|
837
|
-
*
|
|
903
|
+
*
|
|
904
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
838
905
|
* @returns {void}
|
|
839
906
|
*/
|
|
840
907
|
static removeNotUpdateableFields(metadataEntry) {
|
|
@@ -845,7 +912,8 @@ class MetadataType {
|
|
|
845
912
|
|
|
846
913
|
/**
|
|
847
914
|
* Remove fields from metadata entry that are not needed in the template
|
|
848
|
-
*
|
|
915
|
+
*
|
|
916
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
849
917
|
* @returns {void}
|
|
850
918
|
*/
|
|
851
919
|
static keepTemplateFields(metadataEntry) {
|
|
@@ -856,7 +924,8 @@ class MetadataType {
|
|
|
856
924
|
|
|
857
925
|
/**
|
|
858
926
|
* Remove fields from metadata entry that are not needed in the stored metadata
|
|
859
|
-
*
|
|
927
|
+
*
|
|
928
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
860
929
|
* @returns {void}
|
|
861
930
|
*/
|
|
862
931
|
static keepRetrieveFields(metadataEntry) {
|
|
@@ -867,8 +936,9 @@ class MetadataType {
|
|
|
867
936
|
|
|
868
937
|
/**
|
|
869
938
|
* checks if the current metadata entry should be saved on retrieve or not
|
|
939
|
+
*
|
|
870
940
|
* @static
|
|
871
|
-
* @param {MetadataTypeItem} metadataEntry metadata entry
|
|
941
|
+
* @param {TYPE.MetadataTypeItem} metadataEntry metadata entry
|
|
872
942
|
* @param {boolean} [include=false] true: use definition.include / options.include; false=exclude: use definition.filter / options.exclude
|
|
873
943
|
* @returns {boolean} true: skip saving == filtered; false: continue with saving
|
|
874
944
|
* @memberof MetadataType
|
|
@@ -911,14 +981,15 @@ class MetadataType {
|
|
|
911
981
|
}
|
|
912
982
|
/**
|
|
913
983
|
* optionally filter by what folder something is in
|
|
984
|
+
*
|
|
914
985
|
* @static
|
|
915
|
-
* @param {
|
|
986
|
+
* @param {object} metadataEntry metadata entry
|
|
916
987
|
* @param {boolean} [include=false] true: use definition.include / options.include; false=exclude: use definition.filter / options.exclude
|
|
917
988
|
* @returns {boolean} true: filtered == do NOT save; false: not filtered == do save
|
|
918
989
|
* @memberof MetadataType
|
|
919
990
|
*/
|
|
920
991
|
static isFilteredFolder(metadataEntry, include) {
|
|
921
|
-
if (metadataEntry.json
|
|
992
|
+
if (metadataEntry.json?.r__folder_Path) {
|
|
922
993
|
// r__folder_Path found in sub-object
|
|
923
994
|
metadataEntry = metadataEntry.json;
|
|
924
995
|
} else if (!metadataEntry.r__folder_Path) {
|
|
@@ -979,8 +1050,9 @@ class MetadataType {
|
|
|
979
1050
|
}
|
|
980
1051
|
/**
|
|
981
1052
|
* internal helper
|
|
1053
|
+
*
|
|
982
1054
|
* @private
|
|
983
|
-
* @param {
|
|
1055
|
+
* @param {object} myFilter include/exclude filter object
|
|
984
1056
|
* @param {string} r__folder_Path already determined folder path
|
|
985
1057
|
* @returns {?boolean} true: filter value found; false: filter value not found; null: no filter defined
|
|
986
1058
|
*/
|
|
@@ -1008,9 +1080,10 @@ class MetadataType {
|
|
|
1008
1080
|
}
|
|
1009
1081
|
/**
|
|
1010
1082
|
* internal helper
|
|
1083
|
+
*
|
|
1011
1084
|
* @private
|
|
1012
|
-
* @param {
|
|
1013
|
-
* @param {
|
|
1085
|
+
* @param {object} myFilter include/exclude filter object
|
|
1086
|
+
* @param {object} metadataEntry metadata entry
|
|
1014
1087
|
* @returns {?boolean} true: filter value found; false: filter value not found; null: no filter defined
|
|
1015
1088
|
*/
|
|
1016
1089
|
static _filterOther(myFilter, metadataEntry) {
|
|
@@ -1037,69 +1110,59 @@ class MetadataType {
|
|
|
1037
1110
|
return false;
|
|
1038
1111
|
}
|
|
1039
1112
|
|
|
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
1113
|
/**
|
|
1064
1114
|
* Helper for writing Metadata to disk, used for Retrieve and deploy
|
|
1065
|
-
*
|
|
1115
|
+
*
|
|
1116
|
+
* @param {TYPE.MetadataTypeMap} results metadata results from deploy
|
|
1066
1117
|
* @param {string} retrieveDir directory where metadata should be stored after deploy/retrieve
|
|
1067
1118
|
* @param {string} [overrideType] for use when there is a subtype (such as folder-queries)
|
|
1068
|
-
* @param {
|
|
1069
|
-
* @returns {Promise
|
|
1119
|
+
* @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
|
|
1120
|
+
* @returns {Promise.<TYPE.MetadataTypeMap>} Promise of saved metadata
|
|
1070
1121
|
*/
|
|
1071
1122
|
static async saveResults(results, retrieveDir, overrideType, templateVariables) {
|
|
1072
1123
|
const savedResults = {};
|
|
1073
|
-
const subtypeExtension = '.' + (overrideType || this.definition.type) + '-meta';
|
|
1074
1124
|
let filterCounter = 0;
|
|
1075
|
-
|
|
1125
|
+
let subtypeExtension;
|
|
1126
|
+
for (const originalKey in results) {
|
|
1127
|
+
if (this.definition.type === 'asset') {
|
|
1128
|
+
overrideType =
|
|
1129
|
+
this.definition.type +
|
|
1130
|
+
'-' +
|
|
1131
|
+
Object.keys(this.definition.extendedSubTypes).find((type) =>
|
|
1132
|
+
this.definition.extendedSubTypes[type].includes(
|
|
1133
|
+
results[originalKey].assetType.name
|
|
1134
|
+
)
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
subtypeExtension = '.' + (overrideType || this.definition.type) + '-meta';
|
|
1138
|
+
|
|
1076
1139
|
try {
|
|
1077
1140
|
if (
|
|
1078
|
-
this.isFiltered(results[
|
|
1079
|
-
this.isFiltered(results[
|
|
1141
|
+
this.isFiltered(results[originalKey], true) ||
|
|
1142
|
+
this.isFiltered(results[originalKey], false)
|
|
1080
1143
|
) {
|
|
1081
1144
|
// if current metadata entry is filtered don't save it
|
|
1082
1145
|
filterCounter++;
|
|
1083
1146
|
continue;
|
|
1084
1147
|
}
|
|
1148
|
+
|
|
1085
1149
|
// define directory into which the current metdata shall be saved
|
|
1086
1150
|
const baseDir = [retrieveDir, ...(overrideType || this.definition.type).split('-')];
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
results[metadataEntry],
|
|
1151
|
+
results[originalKey] = await this.postRetrieveTasks(
|
|
1152
|
+
results[originalKey],
|
|
1090
1153
|
retrieveDir,
|
|
1091
1154
|
templateVariables ? true : false
|
|
1092
1155
|
);
|
|
1093
|
-
if (!results[
|
|
1156
|
+
if (!results[originalKey] || results[originalKey] === null) {
|
|
1094
1157
|
// we encountered a situation in our postRetrieveTasks that made us want to filter this record
|
|
1095
|
-
delete results[
|
|
1158
|
+
delete results[originalKey];
|
|
1096
1159
|
filterCounter++;
|
|
1097
1160
|
continue;
|
|
1098
1161
|
}
|
|
1099
1162
|
|
|
1100
1163
|
if (
|
|
1101
|
-
this.isFilteredFolder(results[
|
|
1102
|
-
this.isFilteredFolder(results[
|
|
1164
|
+
this.isFilteredFolder(results[originalKey], true) ||
|
|
1165
|
+
this.isFilteredFolder(results[originalKey], false)
|
|
1103
1166
|
) {
|
|
1104
1167
|
// if current metadata entry is filtered don't save it
|
|
1105
1168
|
filterCounter++;
|
|
@@ -1107,20 +1170,20 @@ class MetadataType {
|
|
|
1107
1170
|
}
|
|
1108
1171
|
|
|
1109
1172
|
// for complex types like asset, script, query we need to save the scripts that were extracted from the JSON
|
|
1110
|
-
if (results[
|
|
1173
|
+
if (results[originalKey].json && results[originalKey].codeArr) {
|
|
1111
1174
|
// replace market values with template variable placeholders (do not do it on .codeArr)
|
|
1112
1175
|
if (templateVariables) {
|
|
1113
|
-
results[
|
|
1114
|
-
results[
|
|
1176
|
+
results[originalKey].json = Util.replaceByObject(
|
|
1177
|
+
results[originalKey].json,
|
|
1115
1178
|
templateVariables
|
|
1116
1179
|
);
|
|
1117
|
-
results[
|
|
1118
|
-
results[
|
|
1180
|
+
results[originalKey].subFolder = Util.replaceByObject(
|
|
1181
|
+
results[originalKey].subFolder,
|
|
1119
1182
|
templateVariables
|
|
1120
1183
|
);
|
|
1121
1184
|
}
|
|
1122
1185
|
|
|
1123
|
-
const postRetrieveData = results[
|
|
1186
|
+
const postRetrieveData = results[originalKey];
|
|
1124
1187
|
if (postRetrieveData.subFolder) {
|
|
1125
1188
|
// very complex types have their own subfolder
|
|
1126
1189
|
baseDir.push(...postRetrieveData.subFolder);
|
|
@@ -1141,13 +1204,13 @@ class MetadataType {
|
|
|
1141
1204
|
);
|
|
1142
1205
|
}
|
|
1143
1206
|
// normalize results[metadataEntry]
|
|
1144
|
-
results[
|
|
1207
|
+
results[originalKey] = postRetrieveData.json;
|
|
1145
1208
|
} else {
|
|
1146
1209
|
// not a complex type, run the the entire JSON through templating
|
|
1147
1210
|
// replace market values with template variable placeholders
|
|
1148
1211
|
if (templateVariables) {
|
|
1149
|
-
results[
|
|
1150
|
-
results[
|
|
1212
|
+
results[originalKey] = Util.replaceByObject(
|
|
1213
|
+
results[originalKey],
|
|
1151
1214
|
templateVariables
|
|
1152
1215
|
);
|
|
1153
1216
|
}
|
|
@@ -1156,7 +1219,7 @@ class MetadataType {
|
|
|
1156
1219
|
// we dont store Id on local disk, but we need it for caching logic,
|
|
1157
1220
|
// so its in retrieve but not in save. Here we put into the clone so that the original
|
|
1158
1221
|
// object used for caching doesnt have the Id removed.
|
|
1159
|
-
const saveClone = JSON.parse(JSON.stringify(results[
|
|
1222
|
+
const saveClone = JSON.parse(JSON.stringify(results[originalKey]));
|
|
1160
1223
|
if (!this.definition.keepId) {
|
|
1161
1224
|
delete saveClone[this.definition.idField];
|
|
1162
1225
|
}
|
|
@@ -1166,45 +1229,71 @@ class MetadataType {
|
|
|
1166
1229
|
} else {
|
|
1167
1230
|
this.keepRetrieveFields(saveClone);
|
|
1168
1231
|
}
|
|
1169
|
-
savedResults[
|
|
1170
|
-
File.writeJSONToFile(
|
|
1232
|
+
savedResults[originalKey] = saveClone;
|
|
1233
|
+
await File.writeJSONToFile(
|
|
1171
1234
|
// manage subtypes
|
|
1172
1235
|
baseDir,
|
|
1173
|
-
|
|
1236
|
+
originalKey + subtypeExtension,
|
|
1174
1237
|
saveClone
|
|
1175
1238
|
);
|
|
1239
|
+
if (templateVariables) {
|
|
1240
|
+
Util.logger.info(
|
|
1241
|
+
`- templated ${this.definition.type}: ${
|
|
1242
|
+
saveClone[this.definition.nameField]
|
|
1243
|
+
}`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1176
1246
|
} catch (ex) {
|
|
1177
|
-
|
|
1178
|
-
Util.metadataLogger(
|
|
1179
|
-
'error',
|
|
1180
|
-
this.definition.type,
|
|
1181
|
-
'saveResults',
|
|
1247
|
+
Util.logger.errorStack(
|
|
1182
1248
|
ex,
|
|
1183
|
-
|
|
1249
|
+
` - Saving ${this.definition.type} ${originalKey} failed`
|
|
1184
1250
|
);
|
|
1185
1251
|
}
|
|
1186
1252
|
}
|
|
1187
|
-
if (filterCounter) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1253
|
+
if (filterCounter && this.definition.type !== 'asset') {
|
|
1254
|
+
// interferes with progress bar in assets and is printed 1-by-1 otherwise
|
|
1255
|
+
Util.logger.info(
|
|
1256
|
+
` - Filtered ${this.definition.type}: ${filterCounter} (downloaded but not saved to disk)`
|
|
1257
|
+
);
|
|
1194
1258
|
}
|
|
1195
1259
|
return savedResults;
|
|
1196
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* helper for buildDefinitionForNested
|
|
1263
|
+
* searches extracted file for template variable names and applies the market values
|
|
1264
|
+
*
|
|
1265
|
+
* @param {string} code code from extracted code
|
|
1266
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
1267
|
+
* @returns {string} code with markets applied
|
|
1268
|
+
*/
|
|
1269
|
+
static applyTemplateValues(code, templateVariables) {
|
|
1270
|
+
// replace template variables with their values
|
|
1271
|
+
return Mustache.render(code, templateVariables);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* helper for buildTemplateForNested
|
|
1275
|
+
* searches extracted file for template variable values and applies the market variable names
|
|
1276
|
+
*
|
|
1277
|
+
* @param {string} code code from extracted code
|
|
1278
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
1279
|
+
* @returns {string} code with markets applied
|
|
1280
|
+
*/
|
|
1281
|
+
static applyTemplateNames(code, templateVariables) {
|
|
1282
|
+
// replace template variables with their values
|
|
1283
|
+
return Util.replaceByObject(code, templateVariables);
|
|
1284
|
+
}
|
|
1197
1285
|
/**
|
|
1198
1286
|
* helper for buildDefinition
|
|
1199
1287
|
* handles extracted code if any are found for complex types (e.g script, asset, query)
|
|
1288
|
+
*
|
|
1200
1289
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1201
1290
|
* @param {string} targetDir Directory where built definitions will be saved
|
|
1202
|
-
* @param {MetadataTypeItem} metadata main JSON file that was read from file system
|
|
1203
|
-
* @param {
|
|
1291
|
+
* @param {TYPE.MetadataTypeItem} metadata main JSON file that was read from file system
|
|
1292
|
+
* @param {TYPE.TemplateMap} variables variables to be replaced in the metadata
|
|
1204
1293
|
* @param {string} templateName name of the template to be built
|
|
1205
|
-
* @returns {Promise
|
|
1294
|
+
* @returns {Promise.<string[][]>} list of extracted files with path-parts provided as an array
|
|
1206
1295
|
*/
|
|
1207
|
-
static async
|
|
1296
|
+
static async buildDefinitionForNested(
|
|
1208
1297
|
templateDir,
|
|
1209
1298
|
targetDir,
|
|
1210
1299
|
metadata,
|
|
@@ -1214,23 +1303,46 @@ class MetadataType {
|
|
|
1214
1303
|
// generic version here does nothing. actual cases handled in type classes
|
|
1215
1304
|
return null;
|
|
1216
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* helper for buildTemplate
|
|
1308
|
+
* handles extracted code if any are found for complex types
|
|
1309
|
+
*
|
|
1310
|
+
* @param {string} templateDir Directory where metadata templates are stored
|
|
1311
|
+
* @param {string|string[]} targetDir (List of) Directory where built definitions will be saved
|
|
1312
|
+
* @param {TYPE.MetadataTypeItem} metadata main JSON file that was read from file system
|
|
1313
|
+
* @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
1314
|
+
* @param {string} templateName name of the template to be built
|
|
1315
|
+
* @returns {Promise.<string[][]>} list of extracted files with path-parts provided as an array
|
|
1316
|
+
*/
|
|
1317
|
+
static buildTemplateForNested(
|
|
1318
|
+
templateDir,
|
|
1319
|
+
targetDir,
|
|
1320
|
+
metadata,
|
|
1321
|
+
templateVariables,
|
|
1322
|
+
templateName
|
|
1323
|
+
) {
|
|
1324
|
+
// generic version here does nothing. actual cases handled in type classes
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1217
1327
|
/**
|
|
1218
1328
|
* check template directory for complex types that open subfolders for their subtypes
|
|
1329
|
+
*
|
|
1219
1330
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1220
1331
|
* @param {string} templateName name of the metadata file
|
|
1221
|
-
* @returns {string} subtype name
|
|
1332
|
+
* @returns {Promise.<string>} subtype name
|
|
1222
1333
|
*/
|
|
1223
|
-
static findSubType(templateDir, templateName) {
|
|
1334
|
+
static async findSubType(templateDir, templateName) {
|
|
1224
1335
|
return null;
|
|
1225
1336
|
}
|
|
1226
1337
|
/**
|
|
1227
1338
|
* optional method used for some types to try a different folder structure
|
|
1339
|
+
*
|
|
1228
1340
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1229
1341
|
* @param {string[]} typeDirArr current subdir for this type
|
|
1230
1342
|
* @param {string} templateName name of the metadata template
|
|
1231
1343
|
* @param {string} fileName name of the metadata template file w/o extension
|
|
1232
1344
|
* @param {Error} ex error from first attempt
|
|
1233
|
-
* @returns {
|
|
1345
|
+
* @returns {object} metadata
|
|
1234
1346
|
*/
|
|
1235
1347
|
static async readSecondaryFolder(templateDir, typeDirArr, templateName, fileName, ex) {
|
|
1236
1348
|
// we just want to push the method into the catch here
|
|
@@ -1240,17 +1352,18 @@ class MetadataType {
|
|
|
1240
1352
|
* Builds definition based on template
|
|
1241
1353
|
* NOTE: Most metadata files should use this generic method, unless custom
|
|
1242
1354
|
* parsing is required (for example scripts & queries)
|
|
1355
|
+
*
|
|
1243
1356
|
* @param {string} templateDir Directory where metadata templates are stored
|
|
1244
|
-
* @param {
|
|
1357
|
+
* @param {string | string[]} targetDir (List of) Directory where built definitions will be saved
|
|
1245
1358
|
* @param {string} templateName name of the metadata file
|
|
1246
|
-
* @param {
|
|
1247
|
-
* @returns {Promise
|
|
1359
|
+
* @param {TYPE.TemplateMap} variables variables to be replaced in the metadata
|
|
1360
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of item map
|
|
1248
1361
|
*/
|
|
1249
1362
|
static async buildDefinition(templateDir, targetDir, templateName, variables) {
|
|
1250
1363
|
// retrieve metadata template
|
|
1251
1364
|
let metadataStr;
|
|
1252
1365
|
let typeDirArr = [this.definition.type];
|
|
1253
|
-
const subType = this.findSubType(templateDir, templateName);
|
|
1366
|
+
const subType = await this.findSubType(templateDir, templateName);
|
|
1254
1367
|
if (subType) {
|
|
1255
1368
|
typeDirArr.push(subType);
|
|
1256
1369
|
}
|
|
@@ -1259,7 +1372,11 @@ class MetadataType {
|
|
|
1259
1372
|
try {
|
|
1260
1373
|
// ! do not load via readJSONFile to ensure we get a string, not parsed JSON
|
|
1261
1374
|
// templated files might contain illegal json before the conversion back to the file that shall be saved
|
|
1262
|
-
metadataStr = await File.
|
|
1375
|
+
metadataStr = await File.readFilteredFilename(
|
|
1376
|
+
[templateDir, ...typeDirArr],
|
|
1377
|
+
fileName,
|
|
1378
|
+
'json'
|
|
1379
|
+
);
|
|
1263
1380
|
} catch (ex) {
|
|
1264
1381
|
try {
|
|
1265
1382
|
metadataStr = await this.readSecondaryFolder(
|
|
@@ -1269,7 +1386,7 @@ class MetadataType {
|
|
|
1269
1386
|
fileName,
|
|
1270
1387
|
ex
|
|
1271
1388
|
);
|
|
1272
|
-
} catch
|
|
1389
|
+
} catch {
|
|
1273
1390
|
throw new Error(
|
|
1274
1391
|
`${this.definition.type}:: Could not find ./${File.normalizePath([
|
|
1275
1392
|
templateDir,
|
|
@@ -1286,7 +1403,7 @@ class MetadataType {
|
|
|
1286
1403
|
// update all initial variables & create metadata object
|
|
1287
1404
|
metadata = JSON.parse(Mustache.render(metadataStr, variables));
|
|
1288
1405
|
typeDirArr = typeDirArr.map((el) => Mustache.render(el, variables));
|
|
1289
|
-
} catch
|
|
1406
|
+
} catch {
|
|
1290
1407
|
throw new Error(
|
|
1291
1408
|
`${this.definition.type}:: Error applying template variables on ${
|
|
1292
1409
|
templateName + '.' + this.definition.type
|
|
@@ -1296,8 +1413,8 @@ class MetadataType {
|
|
|
1296
1413
|
|
|
1297
1414
|
// handle extracted code
|
|
1298
1415
|
// run after metadata was templated and converted into JS-object
|
|
1299
|
-
// templating to extracted content is applied inside of
|
|
1300
|
-
await this.
|
|
1416
|
+
// templating to extracted content is applied inside of buildDefinitionForNested()
|
|
1417
|
+
await this.buildDefinitionForNested(
|
|
1301
1418
|
templateDir,
|
|
1302
1419
|
targetDir,
|
|
1303
1420
|
metadata,
|
|
@@ -1307,24 +1424,19 @@ class MetadataType {
|
|
|
1307
1424
|
|
|
1308
1425
|
try {
|
|
1309
1426
|
// write to file
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
targetDirArr = [targetDir];
|
|
1313
|
-
} else {
|
|
1314
|
-
targetDirArr = targetDir;
|
|
1315
|
-
}
|
|
1427
|
+
const targetDirArr = !Array.isArray(targetDir) ? [targetDir] : targetDir;
|
|
1428
|
+
|
|
1316
1429
|
for (const targetDir of targetDirArr) {
|
|
1317
|
-
File.writeJSONToFile(
|
|
1430
|
+
await File.writeJSONToFile(
|
|
1318
1431
|
[targetDir, ...typeDirArr],
|
|
1319
1432
|
metadata[this.definition.keyField] + '.' + this.definition.type + suffix,
|
|
1320
1433
|
metadata
|
|
1321
1434
|
);
|
|
1322
1435
|
}
|
|
1323
1436
|
Util.logger.info(
|
|
1324
|
-
|
|
1325
|
-
this.definition.type +
|
|
1326
|
-
'].buildDefinition:: Complete - ' +
|
|
1437
|
+
`- prepared deployment definition of ${this.definition.type}: ${
|
|
1327
1438
|
metadata[this.definition.keyField]
|
|
1439
|
+
}`
|
|
1328
1440
|
);
|
|
1329
1441
|
|
|
1330
1442
|
return { metadata: metadata, type: this.definition.type };
|
|
@@ -1333,38 +1445,48 @@ class MetadataType {
|
|
|
1333
1445
|
}
|
|
1334
1446
|
}
|
|
1335
1447
|
/**
|
|
1448
|
+
* Standardizes a check for multiple messages
|
|
1336
1449
|
*
|
|
1337
|
-
* @param {
|
|
1338
|
-
* @returns {
|
|
1450
|
+
* @param {object} ex response payload from REST API
|
|
1451
|
+
* @returns {string[]} formatted Error Message
|
|
1339
1452
|
*/
|
|
1340
|
-
static checkForErrors(
|
|
1341
|
-
if (response
|
|
1453
|
+
static checkForErrors(ex) {
|
|
1454
|
+
if (ex?.response?.status >= 400 && ex?.response?.status < 600) {
|
|
1342
1455
|
const errors = [];
|
|
1343
|
-
if (response.
|
|
1344
|
-
for (const errMsg of response.
|
|
1345
|
-
errors.push(
|
|
1456
|
+
if (ex.response.data.errors) {
|
|
1457
|
+
for (const errMsg of ex.response.data.errors) {
|
|
1458
|
+
errors.push(
|
|
1459
|
+
...errMsg.message
|
|
1460
|
+
.split('<br />')
|
|
1461
|
+
.map((el) => el.trim())
|
|
1462
|
+
.filter(Boolean)
|
|
1463
|
+
);
|
|
1346
1464
|
}
|
|
1347
|
-
} else if (response.
|
|
1348
|
-
for (const errMsg of response.
|
|
1349
|
-
errors.push(
|
|
1465
|
+
} else if (ex.response.data.validationErrors) {
|
|
1466
|
+
for (const errMsg of ex.response.data.validationErrors) {
|
|
1467
|
+
errors.push(
|
|
1468
|
+
...errMsg.message
|
|
1469
|
+
.split('<br />')
|
|
1470
|
+
.map((el) => el.trim())
|
|
1471
|
+
.filter(Boolean)
|
|
1472
|
+
);
|
|
1350
1473
|
}
|
|
1351
|
-
} else if (response.
|
|
1352
|
-
errors.push(response.
|
|
1474
|
+
} else if (ex.response.data.message) {
|
|
1475
|
+
errors.push(ex.response.data.message);
|
|
1353
1476
|
} else {
|
|
1354
|
-
errors.push(`Undefined Errors: ${JSON.stringify(response.
|
|
1477
|
+
errors.push(`Undefined Errors: ${JSON.stringify(ex.response.data)}`);
|
|
1355
1478
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
)}`
|
|
1360
|
-
);
|
|
1479
|
+
Util.logger.debug(JSON.stringify(ex.config));
|
|
1480
|
+
Util.logger.debug(JSON.stringify(ex.response.data));
|
|
1481
|
+
return errors;
|
|
1361
1482
|
}
|
|
1362
1483
|
}
|
|
1363
1484
|
|
|
1364
1485
|
/**
|
|
1365
1486
|
* Gets metadata cache with limited fields and does not store value to disk
|
|
1366
|
-
*
|
|
1367
|
-
* @param {
|
|
1487
|
+
*
|
|
1488
|
+
* @param {TYPE.BuObject} [buObject] properties for auth
|
|
1489
|
+
* @param {TYPE.MetadataTypeMap} [metadata] a list of type definitions
|
|
1368
1490
|
* @param {boolean} [isDeploy] used to skip non-supported message during deploy
|
|
1369
1491
|
* @returns {void}
|
|
1370
1492
|
*/
|
|
@@ -1375,26 +1497,86 @@ class MetadataType {
|
|
|
1375
1497
|
}
|
|
1376
1498
|
|
|
1377
1499
|
/**
|
|
1378
|
-
* Delete a
|
|
1379
|
-
*
|
|
1500
|
+
* Delete a metadata item from the specified business unit
|
|
1501
|
+
*
|
|
1502
|
+
* @param {TYPE.BuObject} buObject references credentials
|
|
1380
1503
|
* @param {string} customerKey Identifier of data extension
|
|
1381
|
-
* @returns {
|
|
1504
|
+
* @returns {boolean} deletion success status
|
|
1382
1505
|
*/
|
|
1383
1506
|
static deleteByKey(buObject, customerKey) {
|
|
1384
|
-
Util.logger.error(`
|
|
1507
|
+
Util.logger.error(`Deletion is not yet supported for ${this.definition.typeName}!`);
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* clean up after deleting a metadata item
|
|
1512
|
+
*
|
|
1513
|
+
* @param {TYPE.BuObject} buObject references credentials
|
|
1514
|
+
* @param {string} customerKey Identifier of metadata item
|
|
1515
|
+
* @returns {void}
|
|
1516
|
+
*/
|
|
1517
|
+
static async postDeleteTasks(buObject, customerKey) {
|
|
1518
|
+
// delete local copy: retrieve/cred/bu/type/...json
|
|
1519
|
+
const jsonFile = File.normalizePath([
|
|
1520
|
+
this.properties.directories.retrieve,
|
|
1521
|
+
buObject.credential,
|
|
1522
|
+
buObject.businessUnit,
|
|
1523
|
+
this.definition.type,
|
|
1524
|
+
`${customerKey}.${this.definition.type}-meta.json`,
|
|
1525
|
+
]);
|
|
1526
|
+
await File.remove(jsonFile);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Delete a data extension from the specified business unit
|
|
1531
|
+
*
|
|
1532
|
+
* @param {TYPE.BuObject} buObject references credentials
|
|
1533
|
+
* @param {string} customerKey Identifier of metadata
|
|
1534
|
+
* @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
|
|
1535
|
+
* @returns {boolean} deletion success flag
|
|
1536
|
+
*/
|
|
1537
|
+
static async deleteByKeySOAP(buObject, customerKey, handleOutside) {
|
|
1538
|
+
const keyObj = {};
|
|
1539
|
+
keyObj[this.definition.keyField] = customerKey;
|
|
1540
|
+
try {
|
|
1541
|
+
this.client.soap.delete(
|
|
1542
|
+
this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
|
|
1543
|
+
keyObj,
|
|
1544
|
+
null
|
|
1545
|
+
);
|
|
1546
|
+
if (!handleOutside) {
|
|
1547
|
+
Util.logger.info(`- deleted ${this.definition.type}: ${customerKey}`);
|
|
1548
|
+
}
|
|
1549
|
+
this.postDeleteTasks(buObject, customerKey);
|
|
1550
|
+
|
|
1551
|
+
return true;
|
|
1552
|
+
} catch (ex) {
|
|
1553
|
+
if (!handleOutside) {
|
|
1554
|
+
const errorMsg = ex?.results?.length
|
|
1555
|
+
? `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`
|
|
1556
|
+
: ex.message;
|
|
1557
|
+
Util.logger.error(
|
|
1558
|
+
`- error deleting ${this.definition.type} '${customerKey}': ${errorMsg}`
|
|
1559
|
+
);
|
|
1560
|
+
} else {
|
|
1561
|
+
throw ex;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1385
1566
|
}
|
|
1386
1567
|
/**
|
|
1387
1568
|
* Returns metadata of a business unit that is saved locally
|
|
1569
|
+
*
|
|
1388
1570
|
* @param {string} readDir root directory of metadata.
|
|
1389
1571
|
* @param {boolean} [listBadKeys=false] do not print errors, used for badKeys()
|
|
1390
|
-
* @param {
|
|
1391
|
-
* @returns {
|
|
1572
|
+
* @param {object} [buMetadata] Metadata of BU in local directory
|
|
1573
|
+
* @returns {object} Metadata of BU in local directory
|
|
1392
1574
|
*/
|
|
1393
1575
|
static readBUMetadataForType(readDir, listBadKeys, buMetadata) {
|
|
1394
1576
|
buMetadata = buMetadata || {};
|
|
1395
1577
|
readDir = File.normalizePath([readDir, this.definition.type]);
|
|
1396
1578
|
try {
|
|
1397
|
-
if (File.
|
|
1579
|
+
if (File.pathExistsSync(readDir)) {
|
|
1398
1580
|
// check if folder name is a valid metadataType, then check if the user limited to a certain type in the command params
|
|
1399
1581
|
buMetadata[this.definition.type] = this.getJsonFromFS(readDir, listBadKeys);
|
|
1400
1582
|
return buMetadata;
|
|
@@ -1405,6 +1587,25 @@ class MetadataType {
|
|
|
1405
1587
|
throw new Error(ex.message);
|
|
1406
1588
|
}
|
|
1407
1589
|
}
|
|
1590
|
+
/**
|
|
1591
|
+
* should return only the json for all but asset, query and script that are saved as multiple files
|
|
1592
|
+
* additionally, the documentation for dataExtension and automation should be returned
|
|
1593
|
+
*
|
|
1594
|
+
* @param {string[]} keyArr customerkey of the metadata
|
|
1595
|
+
* @returns {Promise.<string[]>} list of all files that need to be committed in a flat array ['path/file1.ext', 'path/file2.ext']
|
|
1596
|
+
*/
|
|
1597
|
+
static getFilesToCommit(keyArr) {
|
|
1598
|
+
const typeExtension = '.' + this.definition.type + '-meta.json';
|
|
1599
|
+
const path = File.normalizePath([
|
|
1600
|
+
this.properties.directories.retrieve,
|
|
1601
|
+
this.buObject.credential,
|
|
1602
|
+
this.buObject.businessUnit,
|
|
1603
|
+
this.definition.type,
|
|
1604
|
+
]);
|
|
1605
|
+
|
|
1606
|
+
const fileList = keyArr.map((key) => File.normalizePath([path, key + typeExtension]));
|
|
1607
|
+
return fileList;
|
|
1608
|
+
}
|
|
1408
1609
|
}
|
|
1409
1610
|
|
|
1410
1611
|
MetadataType.definition = {
|
|
@@ -1418,13 +1619,20 @@ MetadataType.definition = {
|
|
|
1418
1619
|
type: '',
|
|
1419
1620
|
};
|
|
1420
1621
|
/**
|
|
1421
|
-
* @type {
|
|
1622
|
+
* @type {TYPE.SDK}
|
|
1422
1623
|
*/
|
|
1423
1624
|
MetadataType.client = undefined;
|
|
1424
1625
|
/**
|
|
1425
|
-
* @type {
|
|
1626
|
+
* @type {TYPE.Mcdevrc}
|
|
1426
1627
|
*/
|
|
1427
|
-
MetadataType.cache = {};
|
|
1428
1628
|
MetadataType.properties = null;
|
|
1629
|
+
/**
|
|
1630
|
+
* @type {string}
|
|
1631
|
+
*/
|
|
1632
|
+
MetadataType.subType = null;
|
|
1633
|
+
/**
|
|
1634
|
+
* @type {TYPE.BuObject}
|
|
1635
|
+
*/
|
|
1636
|
+
MetadataType.buObject = null;
|
|
1429
1637
|
|
|
1430
1638
|
module.exports = MetadataType;
|