mcdev 3.1.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +2 -1
  3. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  4. package/.github/dependabot.yml +14 -0
  5. package/.github/workflows/code-analysis.yml +57 -0
  6. package/.husky/commit-msg +10 -0
  7. package/.husky/post-checkout +5 -0
  8. package/.husky/pre-commit +2 -1
  9. package/.prettierrc +8 -0
  10. package/.vscode/settings.json +1 -1
  11. package/LICENSE +2 -2
  12. package/README.md +134 -45
  13. package/boilerplate/config.json +5 -11
  14. package/boilerplate/files/.prettierrc +8 -0
  15. package/boilerplate/files/.vscode/extensions.json +0 -1
  16. package/boilerplate/files/.vscode/settings.json +28 -2
  17. package/boilerplate/files/README.md +2 -2
  18. package/boilerplate/forcedUpdates.json +10 -0
  19. package/boilerplate/npm-dependencies.json +5 -5
  20. package/docs/dist/documentation.md +2795 -1724
  21. package/jsconfig.json +1 -1
  22. package/lib/Builder.js +166 -75
  23. package/lib/Deployer.js +244 -96
  24. package/lib/MetadataTypeDefinitions.js +2 -0
  25. package/lib/MetadataTypeInfo.js +2 -0
  26. package/lib/Retriever.js +61 -84
  27. package/lib/cli.js +116 -11
  28. package/lib/index.js +241 -561
  29. package/lib/metadataTypes/AccountUser.js +101 -95
  30. package/lib/metadataTypes/Asset.js +677 -248
  31. package/lib/metadataTypes/AttributeGroup.js +23 -12
  32. package/lib/metadataTypes/Automation.js +451 -354
  33. package/lib/metadataTypes/Campaign.js +33 -93
  34. package/lib/metadataTypes/ContentArea.js +31 -11
  35. package/lib/metadataTypes/DataExtension.js +387 -372
  36. package/lib/metadataTypes/DataExtensionField.js +131 -54
  37. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  38. package/lib/metadataTypes/DataExtract.js +61 -48
  39. package/lib/metadataTypes/DataExtractType.js +14 -8
  40. package/lib/metadataTypes/Discovery.js +21 -16
  41. package/lib/metadataTypes/Email.js +32 -12
  42. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  43. package/lib/metadataTypes/EventDefinition.js +61 -43
  44. package/lib/metadataTypes/FileTransfer.js +72 -52
  45. package/lib/metadataTypes/Filter.js +11 -4
  46. package/lib/metadataTypes/Folder.js +149 -117
  47. package/lib/metadataTypes/FtpLocation.js +14 -8
  48. package/lib/metadataTypes/ImportFile.js +61 -64
  49. package/lib/metadataTypes/Interaction.js +19 -4
  50. package/lib/metadataTypes/List.js +54 -13
  51. package/lib/metadataTypes/MetadataType.js +668 -454
  52. package/lib/metadataTypes/MobileCode.js +46 -0
  53. package/lib/metadataTypes/MobileKeyword.js +114 -0
  54. package/lib/metadataTypes/Query.js +204 -103
  55. package/lib/metadataTypes/Role.js +76 -61
  56. package/lib/metadataTypes/Script.js +145 -81
  57. package/lib/metadataTypes/SetDefinition.js +20 -8
  58. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  59. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  60. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  61. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  62. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  63. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  64. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  65. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  66. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  67. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  68. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  69. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  70. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  72. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  73. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  74. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  75. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  76. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  77. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  78. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  79. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  80. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  81. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  82. package/lib/retrieveChangelog.js +7 -6
  83. package/lib/util/auth.js +117 -0
  84. package/lib/util/businessUnit.js +55 -66
  85. package/lib/util/cache.js +194 -0
  86. package/lib/util/cli.js +90 -116
  87. package/lib/util/config.js +302 -0
  88. package/lib/util/devops.js +240 -50
  89. package/lib/util/file.js +120 -191
  90. package/lib/util/init.config.js +195 -69
  91. package/lib/util/init.git.js +45 -50
  92. package/lib/util/init.js +72 -59
  93. package/lib/util/init.npm.js +48 -39
  94. package/lib/util/util.js +280 -564
  95. package/package.json +44 -33
  96. package/test/dataExtension.test.js +152 -0
  97. package/test/mockRoot/.mcdev-auth.json +8 -0
  98. package/test/mockRoot/.mcdevrc.json +67 -0
  99. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  105. package/test/query.test.js +149 -0
  106. package/test/resourceFactory.js +142 -0
  107. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  108. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  109. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  110. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  111. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  112. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  113. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  114. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  115. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  116. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  117. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  118. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  119. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  120. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  121. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  122. package/test/resources/9999999/query/build-expected.json +8 -0
  123. package/test/resources/9999999/query/get-expected.json +11 -0
  124. package/test/resources/9999999/query/patch-expected.json +11 -0
  125. package/test/resources/9999999/query/post-expected.json +11 -0
  126. package/test/resources/9999999/query/template-expected.json +8 -0
  127. package/test/resources/auth.json +32 -0
  128. package/test/resources/rest404-response.json +5 -0
  129. package/test/resources/retrieve-response.xml +21 -0
  130. package/test/utils.js +107 -0
  131. package/types/mcdev.d.js +301 -0
  132. package/CHANGELOG.md +0 -126
  133. package/PULL_REQUEST_TEMPLATE.md +0 -19
  134. package/test/util/file.js +0 -51
@@ -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
- // @ts-ignore
35
- const Promise = require('bluebird');
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 {Object} fileName => fileContent map
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
- files.forEach((fileName) => {
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
- if (
81
- fileContent[this.definition.keyField] === fileNameWithoutEnding ||
82
- listBadKeys
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: fileContent[this.definition.keyField],
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
- (additionalFields && additionalFields.includes(fieldName)) ||
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
- * @param {MetadataTypeMap} metadata metadata mapped by their keyField
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 {Util.BuObject} buObject properties for auth
139
- * @returns {Promise<Object>} Promise of keyField => metadata map
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 (this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
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
- * @param {MetadataTypeMap} metadata metadata mapped by their keyField
155
- * @param {MetadataTypeMap} originalMetadata metadata to be updated (contains additioanl fields)
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
- * @param {MetadataTypeItem} metadata a single item
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 {Util.BuObject} buObject properties for auth
149
+ * @param {TYPE.BuObject} buObject properties for auth
199
150
  * @param {string} [subType] optionally limit to a single subtype
200
- * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} metadata
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<{metadata:MetadataTypeMap,type:string}>} metadata
165
+ * @returns {Promise.<TYPE.MetadataTypeMapObj>} metadata
213
166
  */
214
- static retrieveChangelog(additionalFields, buObject, subType) {
215
- return this.retrieve(null, additionalFields, buObject, subType);
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
- * @param {Util.BuObject} buObject properties for auth
173
+ *
174
+ * @param {TYPE.BuObject} buObject properties for auth
221
175
  * @param {string} [subType] optionally limit to a single subtype
222
- * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} metadata
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 {Util.TemplateMap} templateVariables variables to be replaced in the metadata
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<{metadata:MetadataTypeMap,type:string}>} metadata
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
- * @param {MetadataTypeItem} metadata a single metadata item
281
+ *
282
+ * @param {TYPE.MetadataTypeItem} metadata a single metadata item
249
283
  * @param {string} deployDir folder where files for deployment are stored
250
- * @returns {Promise<MetadataTypeItem>} Promise of a single metadata item
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
- * @param {MetadataTypeItem} metadata single metadata entry
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.metadataLogger('error', this.definition.type, 'create', 'create not supported');
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
- * @param {MetadataTypeItem} metadata single metadata entry
270
- * @param {MetadataTypeItem} [metadataBefore] metadata mapped by their keyField
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.metadataLogger('error', this.definition.type, 'update', 'update not supported');
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
- * @param {MetadataTypeMap} metadata metadata mapped by their keyField
391
+ *
392
+ * @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
281
393
  * @param {string} deployDir directory where deploy metadata are saved
282
- * @param {Util.BuObject} [buObject] properties for auth
283
- * @returns {Promise<MetadataTypeMap>} keyField => metadata map
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
- const deployableMetadata = await this.preDeployTasks(
292
- metadata[metadataKey],
293
- deployDir
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
- const normalizedKey = File.reverseFilterIllegalFilenames(metadataKey);
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
- metadataToUpdate.push({
312
- before: {},
313
- after: metadata[metadataKey],
314
- });
315
- } else if (this.cache[this.definition.type][normalizedKey]) {
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
- metadata[metadataKey][this.definition.idField] =
318
- this.cache[this.definition.type][normalizedKey][
319
- this.definition.idField
320
- ];
321
- metadataToUpdate.push({
322
- before: this.cache[this.definition.type][normalizedKey],
323
- after: metadata[metadataKey],
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
- metadataToCreate.push(metadata[metadataKey]);
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.metadataLogger('error', this.definition.type, 'upsert', ex, metadataKey);
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.map(
336
- metadataToCreate,
337
- (metadataEntry) => this.create(metadataEntry, deployDir),
338
- { concurrency: 10 }
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.map(
343
- metadataToUpdate,
344
- (metadataEntry) => this.update(metadataEntry.after, metadataEntry.before),
345
- { concurrency: 10 }
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.metadataLogger(
350
- 'info',
351
- this.definition.type,
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
- .filter((r) => r !== undefined && r !== null)
364
- .map((r) => r.body.Results)
365
- .flat()
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
- * @param {MetadataTypeItem} metadataEntry a single metadata Entry
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
- let response;
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
- `- created ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
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
- `- error creating ${this.definition.type}: ${
407
- metadataEntry[this.definition.keyField]
408
- } (${ex.message})`
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
- * @param {MetadataTypeItem} metadataEntry single metadata entry
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
- let response;
425
- await Util.retryOnError(
426
- `Retrying to create ${this.definition.type}: ${
427
- metadataEntry[this.definition.nameField]
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
- `- created ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
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
- let errorMsg;
450
- if (ex.results && ex.results.length) {
451
- errorMsg = `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`;
452
- } else {
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
- `- error creating ${this.definition.type} '${
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
- * @param {MetadataTypeItem} metadataEntry a single metadata Entry
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
- let response;
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
- `- updated ${this.definition.type}: ${
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
- `- error updating ${this.definition.type}: ${
499
- metadataEntry[this.definition.keyField]
500
- } (${ex.message})`
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
- * @param {MetadataTypeItem} metadataEntry single metadata entry
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 Util.retryOnError(
518
- `Retrying to update ${this.definition.type}: ${
519
- metadataEntry[this.definition.nameField]
520
- }`,
521
- async () =>
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
- `- updated ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
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
- let errorMsg;
542
- if (ex.results && ex.results.length) {
543
- errorMsg = `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`;
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
- `- error updating ${this.definition.type} '${
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 {Util.BuObject} buObject properties for auth
563
- * @param {Object} [options] required for the specific request (filter for example)
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
- * @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
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 retrieveSOAPgeneric(
569
- retrieveDir,
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 {Util.TemplateMap} [templateVariables] variables to be replaced in the metadata
652
- * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} Promise of item map
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 options = {
656
- uri: uri,
657
- headers: {},
658
- };
659
- let moreResults;
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 Promise.all(
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.body[this.definition.keyField];
692
- results[key] = Object.assign(results[key], ext.body);
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 { metadata: results, type: overrideType || this.definition.type };
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
- * @param {Object} body json of response body
716
- * @returns {Promise<MetadataTypeMap>} keyField => metadata map
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
- // in some cases data is just an array
724
- if (Array.isArray(bodyIteratorField)) {
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 (e) {
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
- if (origin) {
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
- * @param {MetadataTypeItem} metadataEntry metadata entry
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
- * @param {MetadataTypeItem} metadataEntry metadata entry
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
- * @param {MetadataTypeItem} metadataEntry metadata entry
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
- * @param {MetadataTypeItem} metadataEntry metadata entry
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 {Object} metadataEntry metadata entry
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 && metadataEntry.json.r__folder_Path) {
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 {Object} myFilter include/exclude filter object
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 {Object} myFilter include/exclude filter object
1013
- * @param {Object} metadataEntry metadata entry
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,42 +1110,32 @@ 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
- * @param {MetadataTypeMap} results metadata results from deploy
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 {Util.TemplateMap} [templateVariables] variables to be replaced in the metadata
1069
- * @returns {Promise<MetadataTypeMap>} Promise of saved metadata
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;
1125
+ let subtypeExtension;
1075
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
1141
  this.isFiltered(results[originalKey], true) ||
@@ -1082,9 +1145,9 @@ class MetadataType {
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
1151
  results[originalKey] = await this.postRetrieveTasks(
1089
1152
  results[originalKey],
1090
1153
  retrieveDir,
@@ -1167,38 +1230,70 @@ class MetadataType {
1167
1230
  this.keepRetrieveFields(saveClone);
1168
1231
  }
1169
1232
  savedResults[originalKey] = saveClone;
1170
- File.writeJSONToFile(
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
- console.log(ex.stack);
1178
- Util.metadataLogger('error', this.definition.type, 'saveResults', ex, originalKey);
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)`
1247
+ Util.logger.errorStack(
1248
+ ex,
1249
+ ` - Saving ${this.definition.type} ${originalKey} failed`
1186
1250
  );
1187
1251
  }
1188
1252
  }
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
+ );
1258
+ }
1189
1259
  return savedResults;
1190
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
+ }
1191
1285
  /**
1192
1286
  * helper for buildDefinition
1193
1287
  * handles extracted code if any are found for complex types (e.g script, asset, query)
1288
+ *
1194
1289
  * @param {string} templateDir Directory where metadata templates are stored
1195
1290
  * @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 {Util.TemplateMap} variables variables to be replaced in the metadata
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
1198
1293
  * @param {string} templateName name of the template to be built
1199
- * @returns {Promise<void>} Promise
1294
+ * @returns {Promise.<string[][]>} list of extracted files with path-parts provided as an array
1200
1295
  */
1201
- static async buildDefinitionForExtracts(
1296
+ static async buildDefinitionForNested(
1202
1297
  templateDir,
1203
1298
  targetDir,
1204
1299
  metadata,
@@ -1208,23 +1303,46 @@ class MetadataType {
1208
1303
  // generic version here does nothing. actual cases handled in type classes
1209
1304
  return null;
1210
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
+ }
1211
1327
  /**
1212
1328
  * check template directory for complex types that open subfolders for their subtypes
1329
+ *
1213
1330
  * @param {string} templateDir Directory where metadata templates are stored
1214
1331
  * @param {string} templateName name of the metadata file
1215
- * @returns {string} subtype name
1332
+ * @returns {Promise.<string>} subtype name
1216
1333
  */
1217
- static findSubType(templateDir, templateName) {
1334
+ static async findSubType(templateDir, templateName) {
1218
1335
  return null;
1219
1336
  }
1220
1337
  /**
1221
1338
  * optional method used for some types to try a different folder structure
1339
+ *
1222
1340
  * @param {string} templateDir Directory where metadata templates are stored
1223
1341
  * @param {string[]} typeDirArr current subdir for this type
1224
1342
  * @param {string} templateName name of the metadata template
1225
1343
  * @param {string} fileName name of the metadata template file w/o extension
1226
1344
  * @param {Error} ex error from first attempt
1227
- * @returns {Object} metadata
1345
+ * @returns {object} metadata
1228
1346
  */
1229
1347
  static async readSecondaryFolder(templateDir, typeDirArr, templateName, fileName, ex) {
1230
1348
  // we just want to push the method into the catch here
@@ -1234,17 +1352,18 @@ class MetadataType {
1234
1352
  * Builds definition based on template
1235
1353
  * NOTE: Most metadata files should use this generic method, unless custom
1236
1354
  * parsing is required (for example scripts & queries)
1355
+ *
1237
1356
  * @param {string} templateDir Directory where metadata templates are stored
1238
- * @param {String|String[]} targetDir (List of) Directory where built definitions will be saved
1357
+ * @param {string | string[]} targetDir (List of) Directory where built definitions will be saved
1239
1358
  * @param {string} templateName name of the metadata file
1240
- * @param {Util.TemplateMap} variables variables to be replaced in the metadata
1241
- * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} Promise of item map
1359
+ * @param {TYPE.TemplateMap} variables variables to be replaced in the metadata
1360
+ * @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of item map
1242
1361
  */
1243
1362
  static async buildDefinition(templateDir, targetDir, templateName, variables) {
1244
1363
  // retrieve metadata template
1245
1364
  let metadataStr;
1246
1365
  let typeDirArr = [this.definition.type];
1247
- const subType = this.findSubType(templateDir, templateName);
1366
+ const subType = await this.findSubType(templateDir, templateName);
1248
1367
  if (subType) {
1249
1368
  typeDirArr.push(subType);
1250
1369
  }
@@ -1253,7 +1372,11 @@ class MetadataType {
1253
1372
  try {
1254
1373
  // ! do not load via readJSONFile to ensure we get a string, not parsed JSON
1255
1374
  // templated files might contain illegal json before the conversion back to the file that shall be saved
1256
- metadataStr = await File.readFile([templateDir, ...typeDirArr], fileName, 'json');
1375
+ metadataStr = await File.readFilteredFilename(
1376
+ [templateDir, ...typeDirArr],
1377
+ fileName,
1378
+ 'json'
1379
+ );
1257
1380
  } catch (ex) {
1258
1381
  try {
1259
1382
  metadataStr = await this.readSecondaryFolder(
@@ -1263,7 +1386,7 @@ class MetadataType {
1263
1386
  fileName,
1264
1387
  ex
1265
1388
  );
1266
- } catch (ex) {
1389
+ } catch {
1267
1390
  throw new Error(
1268
1391
  `${this.definition.type}:: Could not find ./${File.normalizePath([
1269
1392
  templateDir,
@@ -1280,7 +1403,7 @@ class MetadataType {
1280
1403
  // update all initial variables & create metadata object
1281
1404
  metadata = JSON.parse(Mustache.render(metadataStr, variables));
1282
1405
  typeDirArr = typeDirArr.map((el) => Mustache.render(el, variables));
1283
- } catch (ex) {
1406
+ } catch {
1284
1407
  throw new Error(
1285
1408
  `${this.definition.type}:: Error applying template variables on ${
1286
1409
  templateName + '.' + this.definition.type
@@ -1290,8 +1413,8 @@ class MetadataType {
1290
1413
 
1291
1414
  // handle extracted code
1292
1415
  // run after metadata was templated and converted into JS-object
1293
- // templating to extracted content is applied inside of buildDefinitionForExtracts()
1294
- await this.buildDefinitionForExtracts(
1416
+ // templating to extracted content is applied inside of buildDefinitionForNested()
1417
+ await this.buildDefinitionForNested(
1295
1418
  templateDir,
1296
1419
  targetDir,
1297
1420
  metadata,
@@ -1301,24 +1424,19 @@ class MetadataType {
1301
1424
 
1302
1425
  try {
1303
1426
  // write to file
1304
- let targetDirArr;
1305
- if (!Array.isArray(targetDir)) {
1306
- targetDirArr = [targetDir];
1307
- } else {
1308
- targetDirArr = targetDir;
1309
- }
1427
+ const targetDirArr = !Array.isArray(targetDir) ? [targetDir] : targetDir;
1428
+
1310
1429
  for (const targetDir of targetDirArr) {
1311
- File.writeJSONToFile(
1430
+ await File.writeJSONToFile(
1312
1431
  [targetDir, ...typeDirArr],
1313
1432
  metadata[this.definition.keyField] + '.' + this.definition.type + suffix,
1314
1433
  metadata
1315
1434
  );
1316
1435
  }
1317
1436
  Util.logger.info(
1318
- 'MetadataType[' +
1319
- this.definition.type +
1320
- '].buildDefinition:: Complete - ' +
1437
+ `- prepared deployment definition of ${this.definition.type}: ${
1321
1438
  metadata[this.definition.keyField]
1439
+ }`
1322
1440
  );
1323
1441
 
1324
1442
  return { metadata: metadata, type: this.definition.type };
@@ -1327,38 +1445,48 @@ class MetadataType {
1327
1445
  }
1328
1446
  }
1329
1447
  /**
1448
+ * Standardizes a check for multiple messages
1330
1449
  *
1331
- * @param {Object} response response payload from REST API
1332
- * @returns {void}
1450
+ * @param {object} ex response payload from REST API
1451
+ * @returns {string[]} formatted Error Message
1333
1452
  */
1334
- static checkForErrors(response) {
1335
- if (response && response.res.statusCode >= 400 && response.res.statusCode < 600) {
1453
+ static checkForErrors(ex) {
1454
+ if (ex?.response?.status >= 400 && ex?.response?.status < 600) {
1336
1455
  const errors = [];
1337
- if (response.body.errors) {
1338
- for (const errMsg of response.body.errors) {
1339
- errors.push(errMsg.message.split('<br />').join(''));
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
+ );
1340
1464
  }
1341
- } else if (response.body.validationErrors) {
1342
- for (const errMsg of response.body.validationErrors) {
1343
- errors.push(errMsg.message.split('<br />').join(''));
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
+ );
1344
1473
  }
1345
- } else if (response.body.message) {
1346
- errors.push(response.body.message);
1474
+ } else if (ex.response.data.message) {
1475
+ errors.push(ex.response.data.message);
1347
1476
  } else {
1348
- errors.push(`Undefined Errors: ${JSON.stringify(response.body)}`);
1477
+ errors.push(`Undefined Errors: ${JSON.stringify(ex.response.data)}`);
1349
1478
  }
1350
- throw new Error(
1351
- `Errors on upserting metadata at ${response.res.request.path}: ${errors.join(
1352
- '<br />'
1353
- )}`
1354
- );
1479
+ Util.logger.debug(JSON.stringify(ex.config));
1480
+ Util.logger.debug(JSON.stringify(ex.response.data));
1481
+ return errors;
1355
1482
  }
1356
1483
  }
1357
1484
 
1358
1485
  /**
1359
1486
  * Gets metadata cache with limited fields and does not store value to disk
1360
- * @param {Util.BuObject} [buObject] properties for auth
1361
- * @param {MetadataTypeMap} [metadata] a list of type definitions
1487
+ *
1488
+ * @param {TYPE.BuObject} [buObject] properties for auth
1489
+ * @param {TYPE.MetadataTypeMap} [metadata] a list of type definitions
1362
1490
  * @param {boolean} [isDeploy] used to skip non-supported message during deploy
1363
1491
  * @returns {void}
1364
1492
  */
@@ -1369,26 +1497,86 @@ class MetadataType {
1369
1497
  }
1370
1498
 
1371
1499
  /**
1372
- * Delete a data extension from the specified business unit
1373
- * @param {Util.BuObject} buObject references credentials
1500
+ * Delete a metadata item from the specified business unit
1501
+ *
1502
+ * @param {TYPE.BuObject} buObject references credentials
1374
1503
  * @param {string} customerKey Identifier of data extension
1375
- * @returns {void} -
1504
+ * @returns {boolean} deletion success status
1376
1505
  */
1377
1506
  static deleteByKey(buObject, customerKey) {
1378
- Util.logger.error(`Deleting type ${this.definition.type} is not supported.`);
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
+ }
1379
1566
  }
1380
1567
  /**
1381
1568
  * Returns metadata of a business unit that is saved locally
1569
+ *
1382
1570
  * @param {string} readDir root directory of metadata.
1383
1571
  * @param {boolean} [listBadKeys=false] do not print errors, used for badKeys()
1384
- * @param {Object} [buMetadata] Metadata of BU in local directory
1385
- * @returns {Object} Metadata of BU in local directory
1572
+ * @param {object} [buMetadata] Metadata of BU in local directory
1573
+ * @returns {object} Metadata of BU in local directory
1386
1574
  */
1387
1575
  static readBUMetadataForType(readDir, listBadKeys, buMetadata) {
1388
1576
  buMetadata = buMetadata || {};
1389
1577
  readDir = File.normalizePath([readDir, this.definition.type]);
1390
1578
  try {
1391
- if (File.existsSync(readDir)) {
1579
+ if (File.pathExistsSync(readDir)) {
1392
1580
  // check if folder name is a valid metadataType, then check if the user limited to a certain type in the command params
1393
1581
  buMetadata[this.definition.type] = this.getJsonFromFS(readDir, listBadKeys);
1394
1582
  return buMetadata;
@@ -1399,6 +1587,25 @@ class MetadataType {
1399
1587
  throw new Error(ex.message);
1400
1588
  }
1401
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
+ }
1402
1609
  }
1403
1610
 
1404
1611
  MetadataType.definition = {
@@ -1412,13 +1619,20 @@ MetadataType.definition = {
1412
1619
  type: '',
1413
1620
  };
1414
1621
  /**
1415
- * @type {Util.ET_Client}
1622
+ * @type {TYPE.SDK}
1416
1623
  */
1417
1624
  MetadataType.client = undefined;
1418
1625
  /**
1419
- * @type {MultiMetadataTypeMap}
1626
+ * @type {TYPE.Mcdevrc}
1420
1627
  */
1421
- MetadataType.cache = {};
1422
1628
  MetadataType.properties = null;
1629
+ /**
1630
+ * @type {string}
1631
+ */
1632
+ MetadataType.subType = null;
1633
+ /**
1634
+ * @type {TYPE.BuObject}
1635
+ */
1636
+ MetadataType.buObject = null;
1423
1637
 
1424
1638
  module.exports = MetadataType;