mcdev 3.1.3 → 4.0.1

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