mcdev 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.eslintrc.json +1 -1
  2. package/.github/ISSUE_TEMPLATE/bug.yml +72 -0
  3. package/.issuetracker +11 -3
  4. package/.vscode/settings.json +3 -3
  5. package/CHANGELOG.md +53 -0
  6. package/README.md +245 -141
  7. package/boilerplate/config.json +3 -2
  8. package/docs/dist/documentation.md +783 -322
  9. package/lib/Deployer.js +4 -1
  10. package/lib/MetadataTypeDefinitions.js +1 -0
  11. package/lib/MetadataTypeInfo.js +1 -0
  12. package/lib/Retriever.js +30 -14
  13. package/lib/cli.js +295 -0
  14. package/lib/index.js +774 -1019
  15. package/lib/metadataTypes/AccountUser.js +389 -0
  16. package/lib/metadataTypes/Asset.js +8 -7
  17. package/lib/metadataTypes/Automation.js +115 -52
  18. package/lib/metadataTypes/DataExtension.js +125 -89
  19. package/lib/metadataTypes/DataExtensionField.js +134 -4
  20. package/lib/metadataTypes/ImportFile.js +4 -6
  21. package/lib/metadataTypes/MetadataType.js +19 -5
  22. package/lib/metadataTypes/Query.js +2 -3
  23. package/lib/metadataTypes/Role.js +13 -8
  24. package/lib/metadataTypes/definitions/AccountUser.definition.js +227 -0
  25. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  26. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -1
  27. package/lib/metadataTypes/definitions/ImportFile.definition.js +2 -1
  28. package/lib/metadataTypes/definitions/Script.definition.js +5 -5
  29. package/lib/retrieveChangelog.js +96 -0
  30. package/lib/util/cli.js +4 -6
  31. package/lib/util/init.git.js +2 -1
  32. package/lib/util/util.js +17 -0
  33. package/package.json +17 -21
  34. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  35. package/img/README.md/troubleshoot-nodejs-postinstall.jpg +0 -0
  36. package/postinstall.js +0 -41
@@ -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,10 +857,12 @@ 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.
@@ -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
 
@@ -1,17 +1,21 @@
1
1
  'use strict';
2
2
 
3
+ const Util = require('../util/util');
4
+
3
5
  /**
4
6
  * @typedef {Object} DataExtensionFieldItem
5
7
  * @property {string} [ObjectID] id
6
- * @property {string} [CustomerKey] key
8
+ * @property {string} [CustomerKey] key in format [DEkey].[FieldName]
7
9
  * @property {Object} [DataExtension] -
8
10
  * @property {string} DataExtension.CustomerKey key of DE
9
- * @property {string} Name name
10
- * @property {string} DefaultValue -
11
+ * @property {string} Name name of field
12
+ * @property {string} [Name_new] custom attribute that is only used when trying to rename a field from Name to Name_new
13
+ * @property {string} DefaultValue empty string for not set
11
14
  * @property {'true'|'false'} IsRequired -
12
15
  * @property {'true'|'false'} IsPrimaryKey -
13
16
  * @property {string} Ordinal 1, 2, 3, ...
14
- * @property {'Text'|'Date'|'Number'|'Decimal'|'Email'} FieldType -
17
+ * @property {'Text'|'Number'|'Date'|'Boolean'|'Decimal'|'EmailAddress'|'Phone'|'Locale'} FieldType can only be set on create
18
+ * @property {string} Scale the number of places after the decimal that the field can hold; example: "0","1", ...
15
19
  *
16
20
  * @typedef {Object.<string, DataExtensionFieldItem>} DataExtensionFieldMap
17
21
  */
@@ -91,9 +95,135 @@ class DataExtensionField extends MetadataType {
91
95
  delete metadata.CustomerKey;
92
96
  delete metadata.DataExtension;
93
97
  delete metadata.Ordinal;
98
+ if (metadata.FieldType !== 'Decimal') {
99
+ // remove scale - it's only used for "Decimal" to define the digits behind the decimal
100
+ delete metadata.Scale;
101
+ }
94
102
  }
95
103
  return metadata;
96
104
  }
105
+ /**
106
+ * Mofifies passed deployColumns for update by mapping ObjectID to their target column's values.
107
+ * 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
108
+ *
109
+ * @param {DataExtensionFieldItem[]} deployColumns Columns of data extension that will be deployed
110
+ * @param {string} deKey external/customer key of Data Extension
111
+ * @returns {Object<string,DataExtensionFieldItem>} existing fields by their original name to allow re-adding FieldType after update
112
+ */
113
+ static async prepareDeployColumnsOnUpdate(deployColumns, deKey) {
114
+ // retrieve existing fields to enable updating them
115
+ const response = await this.retrieveForCache(
116
+ {
117
+ filter: {
118
+ leftOperand: 'DataExtension.CustomerKey',
119
+ operator: 'equals',
120
+ rightOperand: deKey,
121
+ },
122
+ },
123
+ ['Name', 'ObjectID']
124
+ );
125
+
126
+ const fieldsObj = response.metadata;
127
+
128
+ // ensure fields can be updated properly by their adding ObjectId based on Name-matching
129
+ /** @type {Object<string,DataExtensionFieldItem>} */
130
+ const existingFieldByName = {};
131
+
132
+ Object.keys(fieldsObj).forEach((key) => {
133
+ existingFieldByName[fieldsObj[key].Name] = fieldsObj[key];
134
+ });
135
+ for (let i = deployColumns.length - 1; i >= 0; i--) {
136
+ const item = deployColumns[i];
137
+ const itemOld = existingFieldByName[item.Name];
138
+ if (itemOld) {
139
+ // field is getting updated ---
140
+
141
+ // Updating to a new FieldType will result in an error; warn & afterwards remove it
142
+ if (itemOld.FieldType !== item.FieldType) {
143
+ Util.logger.warn(
144
+ `- The Field Type of an existing field cannot be changed. Keeping the original value for [${deKey}].[${item.Name}]: '${itemOld.FieldType}'`
145
+ );
146
+ item.FieldType = itemOld.FieldType;
147
+ }
148
+ if (item.FieldType !== 'Decimal') {
149
+ // remove scale - it's only used for "Decimal" to define the digits behind the decimal
150
+ delete item.Scale;
151
+ }
152
+ delete item.FieldType;
153
+
154
+ if (itemOld.MaxLength > item.MaxLength) {
155
+ Util.logger.warn(
156
+ `- The length of an existing field cannot be decreased. Keeping the original value for [${deKey}].[${item.Name}]: '${itemOld.MaxLength}'`
157
+ );
158
+ item.MaxLength = itemOld.MaxLength;
159
+ }
160
+ if (Util.isFalse(itemOld.IsRequired) && Util.isTrue(item.IsRequired)) {
161
+ Util.logger.warn(
162
+ `- A field cannot be changed to be required on update after it was created to allow nulls: [${deKey}].[${item.Name}]`
163
+ );
164
+ item.IsRequired = itemOld.IsRequired;
165
+ }
166
+
167
+ // enable renaming
168
+ if (item.Name_new) {
169
+ item.Name = item.Name_new;
170
+ delete item.Name_new;
171
+ Util.logger.warn(
172
+ `Found 'Name_new' value '${item.Name_new}' for ${deKey}.${item.Name} - trying to rename.`
173
+ );
174
+ }
175
+
176
+ // check if any changes were found
177
+ let changeFound = false;
178
+ Object.keys(item).forEach((key) => {
179
+ if (item[key] !== itemOld[key]) {
180
+ changeFound = true;
181
+ }
182
+ });
183
+ if (!changeFound) {
184
+ deployColumns.splice(i, 1);
185
+ Util.logger.verbose(`no change - removed field [${deKey}].[${item.Name}]`);
186
+ continue;
187
+ }
188
+
189
+ // set the ObjectId for clear identification during update
190
+ item.ObjectID = itemOld.ObjectID;
191
+ } else {
192
+ // field is getting added ---
193
+ if (Util.isTrue(item.IsRequired) && item.DefaultValue === '') {
194
+ Util.logger.warn(
195
+ `- Adding new fields to an existing table requires that these fields are either not-required (nullable) or have a default value set. Changing [${deKey}].[${item.Name}] to be not-required`
196
+ );
197
+ item.IsRequired = 'false';
198
+ }
199
+ if (item.Name_new) {
200
+ Util.logger.warn(
201
+ `Found 'Name_new' value '${item.Name_new}' for ${deKey}.${item.Name} but could not find a corresponding DE field on the server - adding new field instead of updating.`
202
+ );
203
+ delete item.Name_new;
204
+ }
205
+ // Field doesn't exist in target, therefore Remove ObjectID if present
206
+ delete item.ObjectID;
207
+ }
208
+ if (!Util.isTrue(item.IsRequired) && !Util.isFalse(item.IsRequired)) {
209
+ Util.logger.error(
210
+ `- Invalid value for 'IsRequired' of [${deKey}].[${item.Name}]. Found '${item.IsRequired}' instead of 'true'/'false'. Removing field from deploy!`
211
+ );
212
+ deployColumns.splice(i, 1);
213
+ }
214
+ if (!Util.isTrue(item.IsPrimaryKey) && !Util.isFalse(item.IsPrimaryKey)) {
215
+ Util.logger.error(
216
+ `- Invalid value for 'IsPrimaryKey' of [${deKey}].[${item.Name}]. Found '${item.IsPrimaryKey}' instead of 'true'/'false'. Removing field from deploy!`
217
+ );
218
+ deployColumns.splice(i, 1);
219
+ }
220
+ }
221
+ Util.logger.debug(
222
+ `${deployColumns.length} Fields added/updated for [${deKey}]: ` +
223
+ deployColumns.map((item) => item.Name).join(', ')
224
+ );
225
+ return deployColumns.length ? existingFieldByName : null;
226
+ }
97
227
  }
98
228
 
99
229
  // Assign definition to static attributes
@@ -147,12 +147,10 @@ class ImportFile extends MetadataType {
147
147
  }
148
148
  }
149
149
  // When the destinationObjectTypeId is 584 is refers to Mobile Connect which is not supported as an Import Type
150
- metadata.destinationObjectTypeId = this.definition.destinationObjectTypeMapping[
151
- metadata.c__destinationType
152
- ];
153
- metadata.subscriberImportTypeId = this.definition.subscriberImportTypeMapping[
154
- metadata.c__subscriberImportType
155
- ];
150
+ metadata.destinationObjectTypeId =
151
+ this.definition.destinationObjectTypeMapping[metadata.c__destinationType];
152
+ metadata.subscriberImportTypeId =
153
+ this.definition.subscriberImportTypeMapping[metadata.c__subscriberImportType];
156
154
  metadata.updateTypeId = this.definition.updateTypeMapping[metadata.c__dataAction];
157
155
  return metadata;
158
156
  }
@@ -204,6 +204,16 @@ class MetadataType {
204
204
  const metadata = {};
205
205
  return { metadata: null, type: this.definition.type };
206
206
  }
207
+ /**
208
+ * Gets metadata from Marketing Cloud
209
+ * @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
+ * @param {string} [subType] optionally limit to a single subtype
212
+ * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} metadata
213
+ */
214
+ static retrieveChangelog(additionalFields, buObject, subType) {
215
+ return this.retrieve(null, additionalFields, buObject, subType);
216
+ }
207
217
 
208
218
  /**
209
219
  * Gets metadata cache with limited fields and does not store value to disk
@@ -304,9 +314,10 @@ class MetadataType {
304
314
  });
305
315
  } else if (this.cache[this.definition.type][normalizedKey]) {
306
316
  // normal way of processing update files
307
- metadata[metadataKey][this.definition.idField] = this.cache[
308
- this.definition.type
309
- ][normalizedKey][this.definition.idField];
317
+ metadata[metadataKey][this.definition.idField] =
318
+ this.cache[this.definition.type][normalizedKey][
319
+ this.definition.idField
320
+ ];
310
321
  metadataToUpdate.push({
311
322
  before: this.cache[this.definition.type][normalizedKey],
312
323
  after: metadata[metadataKey],
@@ -474,8 +485,12 @@ class MetadataType {
474
485
  async () => (response = await this.client.RestClient.patch(options))
475
486
  );
476
487
  this.checkForErrors(response);
488
+ // some times, e.g. automation dont return a key in their update response and hence we need to fall back to name
477
489
  Util.logger.info(
478
- `- updated ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
490
+ `- updated ${this.definition.type}: ${
491
+ metadataEntry[this.definition.keyField] ||
492
+ metadataEntry[this.definition.nameField]
493
+ }`
479
494
  );
480
495
  return response;
481
496
  } catch (ex) {
@@ -565,7 +580,6 @@ class MetadataType {
565
580
  Util.logger.info(
566
581
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
567
582
  );
568
-
569
583
  if (
570
584
  buObject &&
571
585
  this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)
@@ -144,9 +144,8 @@ class Query extends MetadataType {
144
144
  } catch (ex) {
145
145
  throw new Error(`Query '${metadata.key}': ${ex.message}`);
146
146
  }
147
- metadata.targetUpdateTypeId = this.definition.targetUpdateTypeMapping[
148
- metadata.targetUpdateTypeName
149
- ];
147
+ metadata.targetUpdateTypeId =
148
+ this.definition.targetUpdateTypeMapping[metadata.targetUpdateTypeName];
150
149
  return metadata;
151
150
  }
152
151
 
@@ -107,14 +107,19 @@ class Role extends MetadataType {
107
107
  return;
108
108
  }
109
109
  if (!metadata) {
110
- metadata = this.readBUMetadataForType(
111
- File.normalizePath([
112
- this.properties.directories.retrieve,
113
- buObject.credential,
114
- Util.parentBuName,
115
- ]),
116
- true
117
- ).role;
110
+ try {
111
+ metadata = this.readBUMetadataForType(
112
+ File.normalizePath([
113
+ this.properties.directories.retrieve,
114
+ buObject.credential,
115
+ Util.parentBuName,
116
+ ]),
117
+ true
118
+ ).role;
119
+ } catch (ex) {
120
+ Util.logger.error(ex.message);
121
+ return;
122
+ }
118
123
  }
119
124
  const directory = this.properties.directories.roles;
120
125