mcdev 3.0.3 → 3.1.3

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 (41) hide show
  1. package/.eslintrc.json +1 -1
  2. package/.github/ISSUE_TEMPLATE/bug.yml +75 -0
  3. package/.github/ISSUE_TEMPLATE/task.md +1 -1
  4. package/.issuetracker +11 -3
  5. package/.vscode/settings.json +3 -3
  6. package/CHANGELOG.md +66 -0
  7. package/README.md +245 -141
  8. package/boilerplate/config.json +3 -2
  9. package/docs/dist/documentation.md +799 -338
  10. package/lib/Deployer.js +4 -1
  11. package/lib/MetadataTypeDefinitions.js +1 -0
  12. package/lib/MetadataTypeInfo.js +1 -0
  13. package/lib/Retriever.js +30 -14
  14. package/lib/cli.js +298 -0
  15. package/lib/index.js +773 -1019
  16. package/lib/metadataTypes/AccountUser.js +389 -0
  17. package/lib/metadataTypes/Asset.js +8 -7
  18. package/lib/metadataTypes/Automation.js +121 -56
  19. package/lib/metadataTypes/DataExtension.js +133 -97
  20. package/lib/metadataTypes/DataExtensionField.js +134 -4
  21. package/lib/metadataTypes/DataExtract.js +9 -5
  22. package/lib/metadataTypes/EventDefinition.js +9 -5
  23. package/lib/metadataTypes/FileTransfer.js +9 -5
  24. package/lib/metadataTypes/ImportFile.js +13 -12
  25. package/lib/metadataTypes/MetadataType.js +41 -33
  26. package/lib/metadataTypes/Query.js +2 -3
  27. package/lib/metadataTypes/Role.js +13 -8
  28. package/lib/metadataTypes/Script.js +2 -2
  29. package/lib/metadataTypes/definitions/AccountUser.definition.js +227 -0
  30. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  31. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -1
  32. package/lib/metadataTypes/definitions/ImportFile.definition.js +2 -1
  33. package/lib/metadataTypes/definitions/Script.definition.js +5 -5
  34. package/lib/retrieveChangelog.js +96 -0
  35. package/lib/util/cli.js +4 -6
  36. package/lib/util/init.git.js +2 -1
  37. package/lib/util/util.js +17 -0
  38. package/package.json +18 -22
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  40. package/img/README.md/troubleshoot-nodejs-postinstall.jpg +0 -0
  41. package/postinstall.js +0 -41
@@ -92,14 +92,10 @@ class Automation extends MetadataType {
92
92
  * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
93
93
  */
94
94
  static async retrieve(retrieveDir) {
95
- const results = await new Promise((resolve) => {
96
- this.client.SoapClient.retrieve('Program', ['ObjectID'], (error, response) => {
97
- if (error) {
98
- throw new Error(error);
99
- } else {
100
- resolve(response.body.Results);
101
- }
102
- });
95
+ const results = await new Promise((resolve, reject) => {
96
+ this.client.SoapClient.retrieve('Program', ['ObjectID'], (ex, response) =>
97
+ ex ? reject(ex) : resolve(response.body.Results)
98
+ );
103
99
  });
104
100
  const details = (
105
101
  await Promise.all(
@@ -116,6 +112,60 @@ class Automation extends MetadataType {
116
112
  Util.logger.info(
117
113
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
118
114
  );
115
+ return { metadata: savedMetadata, type: this.definition.type };
116
+ }
117
+ /**
118
+ * Retrieves Metadata of Automation
119
+ * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
120
+ */
121
+ static async retrieveChangelog() {
122
+ const results = await new Promise((resolve, reject) => {
123
+ this.client.SoapClient.retrieve(
124
+ 'Program',
125
+ ['ObjectID'],
126
+
127
+ (ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
128
+ );
129
+ });
130
+ const details = [];
131
+ (
132
+ await Promise.all(
133
+ results.map(async (a) => {
134
+ const options = {
135
+ filter: {
136
+ leftOperand: 'ProgramID',
137
+ operator: 'equals',
138
+ rightOperand: a.ObjectID,
139
+ },
140
+ };
141
+
142
+ return new Promise((resolve, reject) => {
143
+ this.client.SoapClient.retrieve(
144
+ 'Automation',
145
+ [
146
+ 'ProgramID',
147
+ 'Name',
148
+ 'CustomerKey',
149
+ 'LastSaveDate',
150
+ 'LastSavedBy',
151
+ 'CreatedBy',
152
+ 'CreatedDate',
153
+ ],
154
+ options,
155
+ (ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
156
+ );
157
+ });
158
+ })
159
+ )
160
+ ).forEach((item) => {
161
+ details.push(...item);
162
+ });
163
+ details.map((item) => {
164
+ item.key = item.CustomerKey;
165
+ });
166
+
167
+ const parsed = this.parseResponseBody({ items: details });
168
+
119
169
  return { metadata: parsed, type: this.definition.type };
120
170
  }
121
171
 
@@ -152,10 +202,10 @@ class Automation extends MetadataType {
152
202
  * Retrieve a specific Automation Definition by Name
153
203
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
154
204
  * @param {string} name name of the metadata file
155
- * @param {Util.TemplateMap} variables variables to be replaced in the metadata
205
+ * @param {Util.TemplateMap} templateVariables variables to be replaced in the metadata
156
206
  * @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
157
207
  */
158
- static async retrieveAsTemplate(templateDir, name, variables) {
208
+ static async retrieveAsTemplate(templateDir, name, templateVariables) {
159
209
  const results = await new Promise((resolve) => {
160
210
  this.client.SoapClient.retrieve(
161
211
  'Program',
@@ -189,12 +239,14 @@ class Automation extends MetadataType {
189
239
  })
190
240
  ).body;
191
241
  let val = null;
242
+ let originalKey;
192
243
  // if parsing fails, we should just save what we get
193
244
  try {
194
245
  const parsedDetails = this.parseMetadata(details);
246
+ originalKey = parsedDetails[this.definition.keyField];
195
247
  if (parsedDetails !== null) {
196
248
  val = JSON.parse(
197
- Util.replaceByObject(JSON.stringify(parsedDetails), variables)
249
+ Util.replaceByObject(JSON.stringify(parsedDetails), templateVariables)
198
250
  );
199
251
  }
200
252
  } catch (ex) {
@@ -209,7 +261,7 @@ class Automation extends MetadataType {
209
261
  this.keepTemplateFields(val);
210
262
  File.writeJSONToFile(
211
263
  [templateDir, this.definition.type].join('/'),
212
- val[this.definition.keyField] + '.' + this.definition.type + '-meta',
264
+ originalKey + '.' + this.definition.type + '-meta',
213
265
  val
214
266
  );
215
267
  Util.logger.info(
@@ -315,9 +367,8 @@ class Automation extends MetadataType {
315
367
  delete metadata.schedule.scheduledTime;
316
368
  delete metadata.schedule.scheduledStatus;
317
369
  if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
318
- metadata.schedule.timezoneId = this.definition.timeZoneMapping[
319
- metadata.schedule.timezoneName
320
- ];
370
+ metadata.schedule.timezoneId =
371
+ this.definition.timeZoneMapping[metadata.schedule.timezoneName];
321
372
  } else {
322
373
  Util.logger.error(
323
374
  `Could not find timezone ${metadata.schedule.timezoneName} in definition.timeZoneMapping`
@@ -435,44 +486,56 @@ class Automation extends MetadataType {
435
486
  }
436
487
  if (schedule !== null) {
437
488
  try {
438
- await new Promise((resolve, reject) => {
439
- this.client.SoapClient.schedule(
440
- 'Automation',
441
- schedule,
442
- {
443
- Interaction: {
444
- ObjectID: metadata[key].id,
445
- },
446
- },
447
- 'start',
448
- null,
449
- (error, response) => {
450
- if (
451
- error ||
452
- (response.body.Results &&
453
- response.body.Results[0] &&
454
- response.body.Results[0].StatusCode &&
455
- response.body.Results[0].StatusCode === 'Error')
456
- ) {
457
- reject(error || response.body.Results[0].StatusMessage);
458
- } else {
459
- resolve(response.body.Results);
460
- }
461
- }
462
- );
463
- });
464
- const intervalString =
465
- (schedule._interval > 1 ? `${schedule._interval} ` : '') +
466
- (schedule.RecurrenceType === 'Daily'
467
- ? 'Day'
468
- : schedule.RecurrenceType.slice(0, -2) +
469
- (schedule._interval > 1 ? 's' : ''));
470
- Util.logger.warn(
471
- `Automation '${
472
- originalMetadata[key].name
473
- }' deployed Active: runs every ${intervalString} starting ${
474
- schedule._StartDateTime.split('T').join(' ').split('.')[0]
475
- } ${schedule._timezoneString}`
489
+ await Util.retryOnError(
490
+ `Retrying ${this.definition.type}`,
491
+ async () => {
492
+ await new Promise((resolve, reject) => {
493
+ this.client.SoapClient.schedule(
494
+ 'Automation',
495
+ schedule,
496
+ {
497
+ Interaction: {
498
+ ObjectID: metadata[key].id,
499
+ },
500
+ },
501
+ 'start',
502
+ null,
503
+ (error, response) => {
504
+ if (
505
+ error ||
506
+ (response.body.Results &&
507
+ response.body.Results[0] &&
508
+ response.body.Results[0].StatusCode &&
509
+ response.body.Results[0].StatusCode ===
510
+ 'Error')
511
+ ) {
512
+ reject(
513
+ error ||
514
+ response.body.Results[0].StatusMessage
515
+ );
516
+ } else {
517
+ resolve(response.body.Results);
518
+ }
519
+ }
520
+ );
521
+ });
522
+ const intervalString =
523
+ (schedule._interval > 1 ? `${schedule._interval} ` : '') +
524
+ (schedule.RecurrenceType === 'Daily'
525
+ ? 'Day'
526
+ : schedule.RecurrenceType.slice(0, -2) +
527
+ (schedule._interval > 1 ? 's' : ''));
528
+ Util.logger.warn(
529
+ `Automation '${
530
+ originalMetadata[key].name
531
+ }' deployed Active: runs every ${intervalString} starting ${
532
+ schedule._StartDateTime
533
+ .split('T')
534
+ .join(' ')
535
+ .split('.')[0]
536
+ } ${schedule._timezoneString}`
537
+ );
538
+ }
476
539
  );
477
540
  } catch (ex) {
478
541
  Util.logger.error(
@@ -684,9 +747,8 @@ class Automation extends MetadataType {
684
747
  }
685
748
 
686
749
  if (this.definition.timeZoneMapping[scheduleObject.timezoneName]) {
687
- scheduleObject.timezoneId = this.definition.timeZoneMapping[
688
- scheduleObject.timezoneName
689
- ];
750
+ scheduleObject.timezoneId =
751
+ this.definition.timeZoneMapping[scheduleObject.timezoneName];
690
752
  } else {
691
753
  Util.logger.error(
692
754
  `Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
@@ -780,6 +842,9 @@ class Automation extends MetadataType {
780
842
  // Assign definition to static attributes
781
843
  Automation.definition = Definitions.automation;
782
844
  Automation.cache = {};
845
+ /**
846
+ * @type {Util.ET_Client}
847
+ */
783
848
  Automation.client = undefined;
784
849
 
785
850
  module.exports = Automation;
@@ -3,6 +3,7 @@
3
3
  const jsonToTable = require('json-to-table');
4
4
  const MetadataType = require('./MetadataType');
5
5
  const DataExtensionField = require('./DataExtensionField');
6
+ const Folder = require('./Folder');
6
7
  const Util = require('../util/util');
7
8
  const File = require('../util/file');
8
9
 
@@ -43,7 +44,7 @@ class DataExtension extends MetadataType {
43
44
  */
44
45
  static async upsert(desToDeploy, _, buObject) {
45
46
  Util.logger.info('- Retrieve target metadata for comparison with deploy metadata');
46
- const results = await this.retrieveForCache(buObject);
47
+ const results = await this.retrieveForCache(buObject, null, true);
47
48
  const targetMetadata = results.metadata;
48
49
  Util.logger.info('- Retrieved target metadata');
49
50
  /** @type {Promise[]} */
@@ -89,21 +90,7 @@ class DataExtension extends MetadataType {
89
90
  // skip rest of handling for this DE
90
91
  continue;
91
92
  }
92
- // Error if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
93
- // Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
94
- if (
95
- desToDeploy[dataExtension].SendableSubscriberField &&
96
- desToDeploy[dataExtension].SendableSubscriberField.Name === '_SubscriberKey'
97
- ) {
98
- desToDeploy[dataExtension].SendableSubscriberField.Name = 'Subscriber Key';
99
- }
100
93
  if (targetMetadata[dataExtension]) {
101
- // Update dataExtension + Columns if they already exist; Create them if not
102
- // Modify columns for update call
103
- DataExtension.prepareDeployColumnsOnUpdate(
104
- desToDeploy[dataExtension].Fields,
105
- targetMetadata[dataExtension].Fields
106
- );
107
94
  // data extension already exists in target and needs to be updated
108
95
  deUpdatePromises.push(DataExtension.update(desToDeploy[dataExtension]));
109
96
  } else {
@@ -113,7 +100,7 @@ class DataExtension extends MetadataType {
113
100
  }
114
101
  if (deUpdatePromises.length) {
115
102
  Util.logger.info(
116
- 'Please note that Data Retention Policies can only be set during creation, not during update.'
103
+ '- Please note that Data Retention Policies can only be set during creation, not during update.'
117
104
  );
118
105
  }
119
106
 
@@ -123,6 +110,7 @@ class DataExtension extends MetadataType {
123
110
  const updateResults = (await Promise.allSettled(deUpdatePromises)).filter(
124
111
  this._filterUpsertResults
125
112
  );
113
+
126
114
  const successfulResults = [...createResults, ...updateResults];
127
115
 
128
116
  Util.metadataLogger(
@@ -160,6 +148,10 @@ class DataExtension extends MetadataType {
160
148
  // promise rejects, whole request failed
161
149
  Util.logger.error('- error upserting dataExtension: ' + res.reason);
162
150
  return false;
151
+ } else if (res.value == undefined || Object.keys(res.value).length === 0) {
152
+ // in case of returning empty result handle gracefully
153
+ // TODO: consider if SOAP handler for this should really return empty object
154
+ return false;
163
155
  } else if (res.value.results) {
164
156
  Util.logger.error(
165
157
  '- error upserting dataExtension: ' +
@@ -177,39 +169,6 @@ class DataExtension extends MetadataType {
177
169
  }
178
170
  }
179
171
 
180
- /**
181
- * Mofifies passed deployColumns for update by mapping ObjectID to their target column's values.
182
- * Removes FieldType field if its the same in deploy and target column, because it results in an error even if its of the same type
183
- *
184
- * @param {DataExtensionField.DataExtensionFieldItem[]} deployColumns Columns of data extension that will be deployed
185
- * @param {DataExtensionField.DataExtensionFieldItem[]} targetColumns Columns of data extension that currently exists in target
186
- * @returns {void}
187
- */
188
- static prepareDeployColumnsOnUpdate(deployColumns, targetColumns) {
189
- // Map data extension column ObjectIDs to their target, because they are environment specific
190
- for (const column in deployColumns) {
191
- const deployColumn = deployColumns[column];
192
- // Check if column exists in target
193
- if (targetColumns[column]) {
194
- // Map ObjectID to value of target, because DataExtensionField updates are based on ObjectID
195
- deployColumn.ObjectID = targetColumns[column].ObjectID;
196
- // Remove FieldType if it is the same in target, because an error occurs if a FieldTypes gets passed on a field update (even if its the same type)
197
- if (targetColumns[column].FieldType === deployColumn.FieldType) {
198
- delete deployColumn.FieldType;
199
- } else {
200
- // Updating to a new FieldType will result in an error
201
- Util.logger.warn(
202
- 'DataExtension.prepareDeployColumnsOnUpdate:: Cannot update FieldType of field: ' +
203
- deployColumn.CustomerKey
204
- );
205
- }
206
- } else {
207
- // Field doesn't exist in target, therefore Remove ObjectID if present
208
- delete deployColumn.ObjectID;
209
- }
210
- }
211
- }
212
-
213
172
  /**
214
173
  * Create a single dataExtension. Also creates their columns in 'dataExtension.columns'
215
174
  * @param {DataExtensionItem} metadata single metadata entry
@@ -256,33 +215,17 @@ class DataExtension extends MetadataType {
256
215
  * @returns {Promise} Promise
257
216
  */
258
217
  static async update(metadata) {
259
- // retrieve existing fields to enable updating them
260
- const fieldsObj = await this._retrieveFields(['Name', 'ObjectID'], {
261
- filter: {
262
- leftOperand: 'DataExtension.CustomerKey',
263
- operator: 'equals',
264
- rightOperand: metadata.CustomerKey,
265
- },
266
- });
267
-
268
- // ensure fields can be updated properly by their adding ObjectId based on Name-matching
269
- const existingFieldObj = {};
270
- Object.keys(fieldsObj).forEach((key) => {
271
- existingFieldObj[fieldsObj[key].Name] = fieldsObj[key].ObjectID;
272
- });
273
- metadata.Fields.map((item) => {
274
- if (existingFieldObj[item.Name]) {
275
- // field is getting updated ---
276
- // remove FieldType as it cannot be updated mostly and will lead to an API error
277
- delete item.FieldType;
278
-
279
- // set the ObjectId for clear identification
280
- item.ObjectID = existingFieldObj[item.Name];
281
- } else {
282
- // field is getting added ---
283
- delete item.ObjectID; // make sure this is gone or it will cause issues on new fields!
284
- }
285
- });
218
+ // Update dataExtension + Columns if they already exist; Create them if not
219
+ // Modify columns for update call
220
+ DataExtensionField.cache = this.metadata;
221
+ DataExtensionField.client = this.client;
222
+ DataExtensionField.properties = this.properties;
223
+ DataExtension.oldFields = DataExtension.oldFields || {};
224
+ DataExtension.oldFields[metadata.CustomerKey] =
225
+ await DataExtensionField.prepareDeployColumnsOnUpdate(
226
+ metadata.Fields,
227
+ metadata.CustomerKey
228
+ );
286
229
 
287
230
  // convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
288
231
  // <Fields>
@@ -295,25 +238,70 @@ class DataExtension extends MetadataType {
295
238
  metadata.Fields = { Field: metadata.Fields };
296
239
  return super.updateSOAP(metadata);
297
240
  }
241
+ /**
242
+ * Gets executed after deployment of metadata type
243
+ * @param {DataExtensionMap} upsertedMetadata metadata mapped by their keyField
244
+ * @returns {void}
245
+ */
246
+ static postDeployTasks(upsertedMetadata) {
247
+ if (!DataExtension.oldFields) {
248
+ // only run postDeploy if we are in update mode
249
+ return;
250
+ }
251
+ // somewhat of a workardoun but it ensures we get the field list from the server rather than whatever we might have in cache got returned during update/add. This ensures a complete and correctly ordered field list
252
+ for (const key in upsertedMetadata) {
253
+ const item = upsertedMetadata[key];
254
+ const isUpdate =
255
+ this.cache &&
256
+ this.cache.dataExtension &&
257
+ this.cache.dataExtension[item.CustomerKey];
258
+ if (isUpdate) {
259
+ const cachedVersion = this.cache.dataExtension[item.CustomerKey];
260
+ // restore retention values that are typically not returned by the update call
261
+ item.RowBasedRetention = cachedVersion.RowBasedRetention;
262
+ item.ResetRetentionPeriodOnImport = cachedVersion.ResetRetentionPeriodOnImport;
263
+ item.DeleteAtEndOfRetentionPeriod = cachedVersion.DeleteAtEndOfRetentionPeriod;
264
+ item.RetainUntil = cachedVersion.RetainUntil;
265
+
266
+ // ensure we have th
267
+ const existingFields = DataExtension.oldFields[item[this.definition.nameField]];
268
+ if (item.Fields !== '' && existingFields) {
269
+ // TODO should be replaced by a manual sort using existingFields
270
+ // ! this is inefficient because it triggers a new download of the fields during the saveResults() step
271
+ item.Fields.length = 0;
272
+ }
273
+ // sort Fields entry to the end of the object for saving in .json
274
+ const fieldsBackup = item.Fields;
275
+ delete item.Fields;
276
+ item.Fields = fieldsBackup;
277
+ }
278
+ }
279
+ }
298
280
 
299
281
  /**
300
282
  * Retrieves dataExtension metadata. Afterwards starts retrieval of dataExtensionColumn metadata retrieval
301
283
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
302
284
  * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
303
285
  * @param {Util.BuObject} buObject properties for auth
286
+ * @param {void} [_] -
287
+ * @param {boolean} [isDeploy] used to signal that fields shall be retrieve in caching mode
304
288
  * @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of item map
305
289
  */
306
- static async retrieve(retrieveDir, additionalFields, buObject) {
290
+ static async retrieve(retrieveDir, additionalFields, buObject, _, isDeploy) {
307
291
  let metadata = await this._retrieveAll(additionalFields);
308
292
  // in case of cache dont get fields
309
- if (metadata && retrieveDir) {
293
+ if (isDeploy || (metadata && retrieveDir)) {
310
294
  // get fields from API
311
- const fieldsObj = await this._retrieveFields(additionalFields);
295
+ const fieldsObj = await this._retrieveFields(null, additionalFields);
312
296
  const fieldKeys = Object.keys(fieldsObj);
313
297
  // add fields to corresponding DE
314
298
  fieldKeys.forEach((key) => {
315
299
  const field = fieldsObj[key];
316
- metadata[field.DataExtension.CustomerKey].Fields.push(field);
300
+ if (metadata[field?.DataExtension?.CustomerKey]) {
301
+ metadata[field.DataExtension.CustomerKey].Fields.push(field);
302
+ } else {
303
+ Util.logger.warn(`Issue retrieving data extension fields. key='${key}'`);
304
+ }
317
305
  });
318
306
 
319
307
  // sort fields by Ordinal value (API returns field unsorted)
@@ -336,9 +324,7 @@ class DataExtension extends MetadataType {
336
324
  clientSecret: this.properties.credentials[buObject.credential].clientSecret,
337
325
  tenant: this.properties.credentials[buObject.credential].tenant,
338
326
  eid: this.properties.credentials[buObject.credential].eid,
339
- mid: this.properties.credentials[buObject.credential].businessUnits[
340
- Util.parentBuName
341
- ],
327
+ mid: this.properties.credentials[buObject.credential].eid,
342
328
  businessUnit: Util.parentBuName,
343
329
  credential: buObject.credential,
344
330
  };
@@ -351,6 +337,16 @@ class DataExtension extends MetadataType {
351
337
  }
352
338
  const metadataParentBu = await this._retrieveAll(additionalFields);
353
339
 
340
+ // get shared folders to match our shared / synched Data Extensions
341
+ Util.logger.info('- Caching dependent Metadata: folder (shared via _ParentBU_)');
342
+ Folder.cache = {};
343
+ Folder.client = this.client;
344
+ Folder.properties = this.properties;
345
+ const result = await Folder.retrieveForCache(buObjectParentBu);
346
+ const parentCache = {
347
+ folder: result.metadata,
348
+ };
349
+
354
350
  // get the types and clean out non-shared ones
355
351
  const folderTypesFromParent = require('../MetadataTypeDefinitions').folder
356
352
  .folderTypesFromParent;
@@ -358,22 +354,30 @@ class DataExtension extends MetadataType {
358
354
  try {
359
355
  // get the data extension type from the folder
360
356
  const folderContentType = Util.getFromCache(
361
- this.cache,
357
+ parentCache,
362
358
  'folder',
363
359
  metadataParentBu[metadataEntry].CategoryID,
364
360
  'ID',
365
361
  'ContentType'
366
362
  );
367
363
  if (!folderTypesFromParent.includes(folderContentType)) {
364
+ Util.logger.verbose(
365
+ `removing ${metadataEntry} because r__folder_ContentType '${folderContentType}' identifies this DE as not being shared`
366
+ );
368
367
  delete metadataParentBu[metadataEntry];
369
368
  }
370
369
  } catch (ex) {
370
+ Util.logger.debug(
371
+ `removing ${metadataEntry} because of error while retrieving r__folder_ContentType: ${ex.message}`
372
+ );
371
373
  delete metadataParentBu[metadataEntry];
372
374
  }
373
375
  }
374
376
 
375
377
  // revert client to current default
376
378
  this.client = clientBackup;
379
+ Folder.client = clientBackup;
380
+ Folder.cache = this.cache;
377
381
 
378
382
  // make sure to overwrite parent bu DEs with local ones
379
383
  metadata = { ...metadataParentBu, ...metadata };
@@ -390,6 +394,15 @@ class DataExtension extends MetadataType {
390
394
  }
391
395
  return { metadata: metadata, type: 'dataExtension' };
392
396
  }
397
+ /**
398
+ * Retrieves dataExtension metadata. Afterwards starts retrieval of dataExtensionColumn metadata retrieval
399
+ * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
400
+ * @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of item map
401
+ */
402
+ static async retrieveChangelog(additionalFields) {
403
+ const metadata = await this._retrieveAll(additionalFields);
404
+ return { metadata: metadata, type: 'dataExtension' };
405
+ }
393
406
  /**
394
407
  * manages post retrieve steps
395
408
  * @param {DataExtensionItem} metadata a single dataExtension
@@ -410,17 +423,25 @@ class DataExtension extends MetadataType {
410
423
  'Ensure that Queries that write into this DE are updated with the new key before deployment.';
411
424
  this.overrideKeyWithName(metadata, warningMsg);
412
425
  }
426
+ // Error during deploy if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
427
+ // Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
428
+ if (
429
+ metadata.SendableSubscriberField &&
430
+ metadata.SendableSubscriberField.Name === '_SubscriberKey'
431
+ ) {
432
+ metadata.SendableSubscriberField.Name = 'Subscriber Key';
433
+ }
413
434
  return this._parseMetadata(JSON.parse(JSON.stringify(metadata)));
414
435
  }
415
436
 
416
437
  /**
417
438
  * Helper to retrieve Data Extension Fields
418
439
  * @private
419
- * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
420
440
  * @param {Object} [options] options (e.g. continueRequest)
441
+ * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
421
442
  * @returns {Promise<DataExtensionField.DataExtensionFieldMap>} Promise of items
422
443
  */
423
- static async _retrieveFields(additionalFields, options) {
444
+ static async _retrieveFields(options, additionalFields) {
424
445
  if (!options) {
425
446
  // dont print this during updates or templating which retrieves fields DE-by-DE
426
447
  Util.logger.info('- Caching dependent Metadata: dataExtensionField');
@@ -447,7 +468,7 @@ class DataExtension extends MetadataType {
447
468
  rightOperand: customerKey,
448
469
  },
449
470
  };
450
- const fieldsObj = await this._retrieveFields(null, fieldOptions);
471
+ const fieldsObj = await this._retrieveFields(fieldOptions);
451
472
 
452
473
  DataExtensionField.cache = this.metadata;
453
474
  DataExtensionField.client = this.client;
@@ -524,6 +545,16 @@ class DataExtension extends MetadataType {
524
545
  // contenttype
525
546
  delete metadata.r__folder_ContentType;
526
547
 
548
+ // Error if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
549
+ // Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
550
+ // TODO remove from preDeploy with release of version 4, keep until then to help with migration of old metadata
551
+ if (
552
+ metadata.SendableSubscriberField &&
553
+ metadata.SendableSubscriberField.Name === '_SubscriberKey'
554
+ ) {
555
+ metadata.SendableSubscriberField.Name = 'Subscriber Key';
556
+ }
557
+
527
558
  return metadata;
528
559
  }
529
560
 
@@ -826,19 +857,21 @@ class DataExtension extends MetadataType {
826
857
  /**
827
858
  * Retrieves folder metadata into local filesystem. Also creates a uniquePath attribute for each folder.
828
859
  * @param {Object} buObject properties for auth
860
+ * @param {void} [_] -
861
+ * @param {boolean} [isDeploy] used to signal that fields shall be retrieve in caching mode
829
862
  * @returns {Promise} Promise
830
863
  */
831
- static async retrieveForCache(buObject) {
832
- return this.retrieve(null, ['ObjectID', 'CustomerKey', 'Name'], buObject);
864
+ static async retrieveForCache(buObject, _, isDeploy) {
865
+ return this.retrieve(null, ['ObjectID', 'CustomerKey', 'Name'], buObject, null, isDeploy);
833
866
  }
834
867
  /**
835
868
  * Retrieves dataExtension metadata in template format.
836
869
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
837
870
  * @param {string} name name of the metadata item
838
- * @param {Util.TemplateMap} variables variables to be replaced in the metadata
871
+ * @param {Util.TemplateMap} templateVariables variables to be replaced in the metadata
839
872
  * @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of items
840
873
  */
841
- static async retrieveAsTemplate(templateDir, name, variables) {
874
+ static async retrieveAsTemplate(templateDir, name, templateVariables) {
842
875
  const options = {
843
876
  filter: {
844
877
  leftOperand: 'Name',
@@ -863,20 +896,20 @@ class DataExtension extends MetadataType {
863
896
  // API returns field unsorted
864
897
  metadata[key].Fields.sort((a, b) => a.Ordinal - b.Ordinal);
865
898
 
899
+ const originalKey = key;
866
900
  const metadataCleaned = JSON.parse(
867
- JSON.stringify(await this.postRetrieveTasks(metadata[key], null, !!variables))
901
+ JSON.stringify(
902
+ await this.postRetrieveTasks(metadata[key], null, !!templateVariables)
903
+ )
868
904
  );
869
905
 
870
906
  this.keepTemplateFields(metadataCleaned);
871
907
  const metadataTemplated = JSON.parse(
872
- Util.replaceByObject(JSON.stringify(metadataCleaned), variables)
908
+ Util.replaceByObject(JSON.stringify(metadataCleaned), templateVariables)
873
909
  );
874
910
  File.writeJSONToFile(
875
911
  [templateDir, this.definition.type].join('/'),
876
- metadataTemplated[this.definition.keyField] +
877
- '.' +
878
- this.definition.type +
879
- '-meta',
912
+ originalKey + '.' + this.definition.type + '-meta',
880
913
  metadataTemplated
881
914
  );
882
915
  } catch (ex) {
@@ -993,6 +1026,9 @@ class DataExtension extends MetadataType {
993
1026
 
994
1027
  // Assign definition to static attributes
995
1028
  DataExtension.definition = require('../MetadataTypeDefinitions').dataExtension;
1029
+ /**
1030
+ * @type {Util.ET_Client}
1031
+ */
996
1032
  DataExtension.client = undefined;
997
1033
  DataExtension.cache = {};
998
1034