mcdev 3.1.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +5 -1
  3. package/.github/ISSUE_TEMPLATE/task.md +1 -1
  4. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  5. package/.github/dependabot.yml +14 -0
  6. package/.github/workflows/code-analysis.yml +57 -0
  7. package/.husky/commit-msg +10 -0
  8. package/.husky/post-checkout +5 -0
  9. package/.husky/pre-commit +2 -1
  10. package/.prettierrc +8 -0
  11. package/.vscode/settings.json +1 -1
  12. package/LICENSE +2 -2
  13. package/README.md +134 -45
  14. package/boilerplate/config.json +5 -11
  15. package/boilerplate/files/.prettierrc +8 -0
  16. package/boilerplate/files/.vscode/extensions.json +0 -1
  17. package/boilerplate/files/.vscode/settings.json +28 -2
  18. package/boilerplate/files/README.md +2 -2
  19. package/boilerplate/forcedUpdates.json +10 -0
  20. package/boilerplate/npm-dependencies.json +5 -5
  21. package/docs/dist/documentation.md +2795 -1724
  22. package/jsconfig.json +1 -1
  23. package/lib/Builder.js +166 -75
  24. package/lib/Deployer.js +244 -96
  25. package/lib/MetadataTypeDefinitions.js +2 -0
  26. package/lib/MetadataTypeInfo.js +2 -0
  27. package/lib/Retriever.js +61 -84
  28. package/lib/cli.js +133 -25
  29. package/lib/index.js +242 -563
  30. package/lib/metadataTypes/AccountUser.js +101 -95
  31. package/lib/metadataTypes/Asset.js +677 -248
  32. package/lib/metadataTypes/AttributeGroup.js +23 -12
  33. package/lib/metadataTypes/Automation.js +456 -357
  34. package/lib/metadataTypes/Campaign.js +33 -93
  35. package/lib/metadataTypes/ContentArea.js +31 -11
  36. package/lib/metadataTypes/DataExtension.js +391 -376
  37. package/lib/metadataTypes/DataExtensionField.js +131 -54
  38. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  39. package/lib/metadataTypes/DataExtract.js +67 -50
  40. package/lib/metadataTypes/DataExtractType.js +14 -8
  41. package/lib/metadataTypes/Discovery.js +21 -16
  42. package/lib/metadataTypes/Email.js +32 -12
  43. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  44. package/lib/metadataTypes/EventDefinition.js +69 -47
  45. package/lib/metadataTypes/FileTransfer.js +78 -54
  46. package/lib/metadataTypes/Filter.js +11 -4
  47. package/lib/metadataTypes/Folder.js +149 -117
  48. package/lib/metadataTypes/FtpLocation.js +14 -8
  49. package/lib/metadataTypes/ImportFile.js +69 -69
  50. package/lib/metadataTypes/Interaction.js +19 -4
  51. package/lib/metadataTypes/List.js +54 -13
  52. package/lib/metadataTypes/MetadataType.js +687 -479
  53. package/lib/metadataTypes/MobileCode.js +46 -0
  54. package/lib/metadataTypes/MobileKeyword.js +114 -0
  55. package/lib/metadataTypes/Query.js +204 -103
  56. package/lib/metadataTypes/Role.js +76 -61
  57. package/lib/metadataTypes/Script.js +146 -82
  58. package/lib/metadataTypes/SetDefinition.js +20 -8
  59. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  60. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  61. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  62. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  63. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  64. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  65. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  66. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  67. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  68. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  69. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  70. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  72. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  73. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  74. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  75. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  76. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  77. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  78. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  79. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  80. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  81. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  82. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  83. package/lib/retrieveChangelog.js +7 -6
  84. package/lib/util/auth.js +117 -0
  85. package/lib/util/businessUnit.js +55 -66
  86. package/lib/util/cache.js +194 -0
  87. package/lib/util/cli.js +90 -116
  88. package/lib/util/config.js +302 -0
  89. package/lib/util/devops.js +240 -50
  90. package/lib/util/file.js +120 -191
  91. package/lib/util/init.config.js +195 -69
  92. package/lib/util/init.git.js +45 -50
  93. package/lib/util/init.js +72 -59
  94. package/lib/util/init.npm.js +48 -39
  95. package/lib/util/util.js +280 -564
  96. package/package.json +44 -33
  97. package/test/dataExtension.test.js +152 -0
  98. package/test/mockRoot/.mcdev-auth.json +8 -0
  99. package/test/mockRoot/.mcdevrc.json +67 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  105. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  106. package/test/query.test.js +149 -0
  107. package/test/resourceFactory.js +142 -0
  108. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  109. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  110. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  111. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  112. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  113. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  114. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  115. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  116. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  117. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  118. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  119. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  120. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  121. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  122. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  123. package/test/resources/9999999/query/build-expected.json +8 -0
  124. package/test/resources/9999999/query/get-expected.json +11 -0
  125. package/test/resources/9999999/query/patch-expected.json +11 -0
  126. package/test/resources/9999999/query/post-expected.json +11 -0
  127. package/test/resources/9999999/query/template-expected.json +8 -0
  128. package/test/resources/auth.json +32 -0
  129. package/test/resources/rest404-response.json +5 -0
  130. package/test/resources/retrieve-response.xml +21 -0
  131. package/test/utils.js +107 -0
  132. package/types/mcdev.d.js +301 -0
  133. package/CHANGELOG.md +0 -126
  134. package/PULL_REQUEST_TEMPLATE.md +0 -19
  135. package/test/util/file.js +0 -51
@@ -1,62 +1,48 @@
1
1
  'use strict';
2
2
 
3
3
  const jsonToTable = require('json-to-table');
4
+ const TYPE = require('../../types/mcdev.d');
4
5
  const MetadataType = require('./MetadataType');
5
6
  const DataExtensionField = require('./DataExtensionField');
6
7
  const Folder = require('./Folder');
7
8
  const Util = require('../util/util');
8
9
  const File = require('../util/file');
9
-
10
- /**
11
- * @typedef {Object} DataExtensionItem
12
- * @property {string} CustomerKey key
13
- * @property {string} Name name
14
- * @property {string} Description -
15
- * @property {'true'|'false'} IsSendable -
16
- * @property {'true'|'false'} IsTestable -
17
- * @property {Object} SendableDataExtensionField -
18
- * @property {string} SendableDataExtensionField.Name -
19
- * @property {Object} SendableSubscriberField -
20
- * @property {string} SendableSubscriberField.Name -
21
- * @property {DataExtensionField.DataExtensionFieldItem[]} Fields list of DE fields
22
- * @property {'dataextension'|'salesforcedataextension'|'synchronizeddataextension'|'shared_dataextension'|'shared_salesforcedataextension'} r__folder_ContentType retrieved from associated folder
23
- * @property {string} r__folder_Path folder path in which this DE is saved
24
- * @property {string} [CategoryID] holds folder ID, replaced with r__folder_Path during retrieve
25
- * @property {string} [r__dataExtensionTemplate_Name] name of optionally associated DE template
26
- * @property {Object} [Template] -
27
- * @property {string} [Template.CustomerKey] key of optionally associated DE teplate
28
- *
29
- * @typedef {Object.<string, DataExtensionItem>} DataExtensionMap
30
- */
10
+ const auth = require('../util/auth');
11
+ const cache = require('../util/cache');
31
12
 
32
13
  /**
33
14
  * DataExtension MetadataType
15
+ *
34
16
  * @augments MetadataType
35
17
  */
36
18
  class DataExtension extends MetadataType {
37
19
  /**
38
20
  * Upserts dataExtensions after retrieving them from source and target to compare
39
21
  * if create or update operation is needed.
40
- * @param {DataExtensionMap} desToDeploy dataExtensions mapped by their customerKey
41
- * @param {Object} _ -
42
- * @param {Util.BuObject} buObject properties for auth
22
+ *
23
+ * @param {TYPE.DataExtensionMap} desToDeploy dataExtensions mapped by their customerKey
24
+ * @param {void} _ unused parameter
25
+ * @param {TYPE.BuObject} buObject properties for auth
43
26
  * @returns {Promise} Promise
44
27
  */
45
28
  static async upsert(desToDeploy, _, buObject) {
46
- Util.logger.info('- Retrieve target metadata for comparison with deploy metadata');
47
- const results = await this.retrieveForCache(buObject, null, true);
48
- const targetMetadata = results.metadata;
49
- Util.logger.info('- Retrieved target metadata');
29
+ // get dataExtensions from target BU for add/update decision
30
+ /** @type {TYPE.DataExtensionMap} */
31
+ const targetMetadata = cache.getCache().dataExtension || {};
32
+ // get existing de-fields to properly handle add/update/delete of fields
33
+ await this._attachFields(targetMetadata);
34
+
50
35
  /** @type {Promise[]} */
51
36
  const deCreatePromises = [];
52
37
  /** @type {Promise[]} */
53
38
  const deUpdatePromises = [];
39
+ let filteredByPreDeploy = 0;
54
40
  for (const dataExtension in desToDeploy) {
55
41
  if (desToDeploy[dataExtension].Name.startsWith('_')) {
56
42
  Util.logger.warn(
57
- '- Cannot Upsert Strongly Typed Data Extensions - skipping ' +
58
- desToDeploy[dataExtension].Name
43
+ ` ☇ skipping dataExtension ${desToDeploy[dataExtension].Name}: Cannot Upsert Strongly Typed Data Extensions`
59
44
  );
45
+ filteredByPreDeploy++;
60
46
  continue;
61
47
  }
62
48
  if (
@@ -65,8 +51,9 @@ class DataExtension extends MetadataType {
65
51
  ) {
66
52
  // this needs to be run before executing preDeployTasks()
67
53
  Util.logger.warn(
68
- `- Cannot Create/Update a Shared Data Extension from the Child BU - skipping ${desToDeploy[dataExtension].Name}`
54
+ ` ☇ skipping dataExtension ${desToDeploy[dataExtension].Name}: Cannot Create/Update a Shared Data Extension from the Child BU`
69
55
  );
56
+ filteredByPreDeploy++;
70
57
  continue;
71
58
  }
72
59
  if (
@@ -74,8 +61,9 @@ class DataExtension extends MetadataType {
74
61
  ) {
75
62
  // this needs to be run before executing preDeployTasks()
76
63
  Util.logger.warn(
77
- `- Cannot Create/Update a Synchronized Data Extension. Please use Contact Builder to maintain these - skipping ${desToDeploy[dataExtension].Name}`
64
+ ` ☇ skipping dataExtension ${desToDeploy[dataExtension].Name}:Cannot Create/Update a Synchronized Data Extension. Please use Contact Builder to maintain these`
78
65
  );
66
+ filteredByPreDeploy++;
79
67
  continue;
80
68
  }
81
69
  try {
@@ -100,7 +88,7 @@ class DataExtension extends MetadataType {
100
88
  }
101
89
  if (deUpdatePromises.length) {
102
90
  Util.logger.info(
103
- '- Please note that Data Retention Policies can only be set during creation, not during update.'
91
+ ' - Please note that Data Retention Policies can only be set during creation, not during update.'
104
92
  );
105
93
  }
106
94
 
@@ -112,27 +100,23 @@ class DataExtension extends MetadataType {
112
100
  );
113
101
 
114
102
  const successfulResults = [...createResults, ...updateResults];
115
-
116
- Util.metadataLogger(
117
- 'info',
118
- this.definition.type,
119
- 'upsert',
120
- `${createResults.length} of ${deCreatePromises.length} created / ${updateResults.length} of ${deUpdatePromises.length} updated`
103
+ Util.logger.info(
104
+ `${this.definition.type} upsert: ${createResults.length} of ${deCreatePromises.length} created / ${updateResults.length} of ${deUpdatePromises.length} updated` +
105
+ (filteredByPreDeploy > 0 ? ` / ${filteredByPreDeploy} filtered` : '')
121
106
  );
122
107
  if (successfulResults.length > 0) {
123
108
  const metadataResults = successfulResults
124
- .map((r) => r.value.body.Results[0].Object)
109
+ .map((r) => r.value.Results[0].Object)
125
110
  .map((r) => {
126
111
  // if only one fields added will return object otherwise array
127
- if (r.Fields && r.Fields.Field && Array.isArray(r.Fields.Field)) {
112
+ if (Array.isArray(r?.Fields?.Field)) {
128
113
  r.Fields = r.Fields.Field;
129
- } else if (r.Fields && r.Fields.Field) {
114
+ } else if (r?.Fields?.Field) {
130
115
  r.Fields = [r.Fields.Field];
131
116
  }
132
117
  return r;
133
118
  });
134
- const formattedResults = super.parseResponseBody({ Results: metadataResults });
135
- return formattedResults;
119
+ return super.parseResponseBody({ Results: metadataResults });
136
120
  } else {
137
121
  return {};
138
122
  }
@@ -140,8 +124,9 @@ class DataExtension extends MetadataType {
140
124
 
141
125
  /**
142
126
  * helper for upsert()
143
- * @param {Object} res -
144
- * @returns {Boolean} true: keep, false: discard
127
+ *
128
+ * @param {object} res -
129
+ * @returns {boolean} true: keep, false: discard
145
130
  */
146
131
  static _filterUpsertResults(res) {
147
132
  if (res.status === 'rejected') {
@@ -155,12 +140,12 @@ class DataExtension extends MetadataType {
155
140
  } else if (res.value.results) {
156
141
  Util.logger.error(
157
142
  '- error upserting dataExtension: ' +
158
- (res.value.results[0].Object ? res.value.results[0].Object.Name : '') +
143
+ (res.value.Results[0].Object ? res.value.Results[0].Object.Name : '') +
159
144
  '. ' +
160
- res.value.results[0].StatusMessage
145
+ res.value.Results[0].StatusMessage
161
146
  );
162
147
  return false;
163
- } else if (res.status === 'fulfilled' && res.value && res.value.faultstring) {
148
+ } else if (res.status === 'fulfilled' && res?.value?.faultstring) {
164
149
  // can happen that the promise does not reject, but that it resolves an error
165
150
  Util.logger.error('- error upserting dataExtension: ' + res.value.faultstring);
166
151
  return false;
@@ -171,7 +156,8 @@ class DataExtension extends MetadataType {
171
156
 
172
157
  /**
173
158
  * Create a single dataExtension. Also creates their columns in 'dataExtension.columns'
174
- * @param {DataExtensionItem} metadata single metadata entry
159
+ *
160
+ * @param {TYPE.DataExtensionItem} metadata single metadata entry
175
161
  * @returns {Promise} Promise
176
162
  */
177
163
  static async create(metadata) {
@@ -193,8 +179,9 @@ class DataExtension extends MetadataType {
193
179
  * SFMC saves a date in "RetainUntil" under certain circumstances even
194
180
  * if that field duplicates whats in the period fields
195
181
  * during deployment, that extra value is not accepted by the APIs which is why it needs to be removed
182
+ *
196
183
  * @private
197
- * @param {DataExtensionItem} metadata single metadata entry
184
+ * @param {TYPE.DataExtensionItem} metadata single metadata entry
198
185
  * @returns {void}
199
186
  */
200
187
  static _cleanupRetentionPolicyFields(metadata) {
@@ -205,23 +192,23 @@ class DataExtension extends MetadataType {
205
192
  ) {
206
193
  metadata.RetainUntil = '';
207
194
  Util.logger.warn(
208
- `RetainUntil date was reset automatically because RetentionPeriod info was found in: ${metadata.CustomerKey}`
195
+ ` - RetainUntil date was reset automatically because RetentionPeriod info was found in: ${metadata.CustomerKey}`
209
196
  );
210
197
  }
211
198
  }
212
199
  /**
213
200
  * Updates a single dataExtension. Also updates their columns in 'dataExtension.columns'
214
- * @param {DataExtensionItem} metadata single metadata entry
201
+ *
202
+ * @param {TYPE.DataExtensionItem} metadata single metadata entry
215
203
  * @returns {Promise} Promise
216
204
  */
217
205
  static async update(metadata) {
218
206
  // Update dataExtension + Columns if they already exist; Create them if not
219
207
  // Modify columns for update call
220
- DataExtensionField.cache = this.metadata;
221
208
  DataExtensionField.client = this.client;
222
209
  DataExtensionField.properties = this.properties;
223
210
  DataExtension.oldFields = DataExtension.oldFields || {};
224
- DataExtension.oldFields[metadata.CustomerKey] =
211
+ DataExtension.oldFields[metadata[this.definition.keyField]] =
225
212
  await DataExtensionField.prepareDeployColumnsOnUpdate(
226
213
  metadata.Fields,
227
214
  metadata.CustomerKey
@@ -240,97 +227,129 @@ class DataExtension extends MetadataType {
240
227
  }
241
228
  /**
242
229
  * Gets executed after deployment of metadata type
243
- * @param {DataExtensionMap} upsertedMetadata metadata mapped by their keyField
230
+ *
231
+ * @param {TYPE.DataExtensionMap} upsertedMetadata metadata mapped by their keyField
232
+ * @param {TYPE.DataExtensionMap} originalMetadata metadata to be updated (contains additioanl fields)
244
233
  * @returns {void}
245
234
  */
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
235
+ static postDeployTasks(upsertedMetadata, originalMetadata) {
252
236
  for (const key in upsertedMetadata) {
253
237
  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];
238
+ const cachedVersion = cache.getByKey('dataExtension', item.CustomerKey);
239
+ if (cachedVersion) {
240
+ // UPDATE
260
241
  // restore retention values that are typically not returned by the update call
261
242
  item.RowBasedRetention = cachedVersion.RowBasedRetention;
262
243
  item.ResetRetentionPeriodOnImport = cachedVersion.ResetRetentionPeriodOnImport;
263
244
  item.DeleteAtEndOfRetentionPeriod = cachedVersion.DeleteAtEndOfRetentionPeriod;
264
245
  item.RetainUntil = cachedVersion.RetainUntil;
265
246
 
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;
247
+ const existingFields = DataExtension.oldFields[item[this.definition.keyField]];
248
+ if (item.Fields === '') {
249
+ // if no fields were updated, we need to set Fields to "empty string" for the API to work
250
+ // reset here to get the correct field list
251
+ item.Fields = Object.keys(existingFields)
252
+ .map((key) => existingFields[key])
253
+ .sort((a, b) => a.Ordinal - b.Ordinal);
254
+ }
255
+ if (existingFields) {
256
+ // get list of updated fields
257
+ /** @type {TYPE.DataExtensionFieldItem[]} */
258
+ const updatedFieldsArr = originalMetadata[key].Fields.Field.filter(
259
+ (field) => field.ObjectID && field.ObjectID !== ''
260
+ );
261
+ // convert existing fields obj into array and sort
262
+ /** @type {TYPE.DataExtensionFieldItem[]} */
263
+ const finalFieldsArr = Object.keys(existingFields)
264
+ .map((key) => {
265
+ /** @type {TYPE.DataExtensionFieldItem} */
266
+ const existingField = existingFields[key];
267
+ // check if the current field was updated and then override with it. otherwise use existing value
268
+ const field =
269
+ updatedFieldsArr.find(
270
+ (field) => field.ObjectID === existingField.ObjectID
271
+ ) || existingField;
272
+ // field does not have a ordinal value because we rely on array order
273
+ field.Ordinal = existingField.Ordinal;
274
+ // updating FieldType is not supported by API and hence removed
275
+ field.FieldType = existingField.FieldType;
276
+ return field;
277
+ })
278
+ .sort((a, b) => a.Ordinal - b.Ordinal);
279
+
280
+ // get list of new fields
281
+ /** @type {TYPE.DataExtensionFieldItem[]} */
282
+ const newFieldsArr = originalMetadata[key].Fields.Field.filter(
283
+ (field) => !field.ObjectID
284
+ );
285
+ // push new fields to end of list
286
+ if (newFieldsArr.length) {
287
+ finalFieldsArr.push(...newFieldsArr);
288
+ }
289
+
290
+ // sort Fields entry to the end of the object for saving in .json
291
+ delete item.Fields;
292
+ item.Fields = finalFieldsArr;
272
293
  }
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;
294
+ }
295
+ // UPDATE + CREATE
296
+ for (const field of item.Fields) {
297
+ DataExtensionField.postRetrieveTasks(field, true);
277
298
  }
278
299
  }
279
300
  }
280
301
 
281
302
  /**
282
303
  * Retrieves dataExtension metadata. Afterwards starts retrieval of dataExtensionColumn metadata retrieval
304
+ *
283
305
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
284
306
  * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
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
288
- * @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of item map
307
+ * @param {TYPE.BuObject} buObject properties for auth
308
+ * @param {void} [_] unused parameter
309
+ * @param {string} [key] customer key of single item to retrieve
310
+ * @returns {Promise.<{metadata: TYPE.DataExtensionMap, type: string}>} Promise of item map
289
311
  */
290
- static async retrieve(retrieveDir, additionalFields, buObject, _, isDeploy) {
291
- let metadata = await this._retrieveAll(additionalFields);
312
+ static async retrieve(retrieveDir, additionalFields, buObject, _, key) {
313
+ /** @type {TYPE.SoapRequestParams} */
314
+ let requestParams = null;
315
+ /** @type {TYPE.SoapRequestParams} */
316
+ let fieldOptions = null;
317
+ if (key) {
318
+ requestParams = {
319
+ filter: {
320
+ leftOperand: 'CustomerKey',
321
+ operator: 'equals',
322
+ rightOperand: key,
323
+ },
324
+ };
325
+ fieldOptions = {
326
+ filter: {
327
+ leftOperand: 'DataExtension.CustomerKey',
328
+ operator: 'equals',
329
+ rightOperand: key,
330
+ },
331
+ };
332
+ }
333
+ let metadata = await this._retrieveAll(additionalFields, requestParams);
292
334
  // in case of cache dont get fields
293
- if (isDeploy || (metadata && retrieveDir)) {
335
+ if (metadata && retrieveDir) {
294
336
  // get fields from API
295
- const fieldsObj = await this._retrieveFields(null, additionalFields);
296
- const fieldKeys = Object.keys(fieldsObj);
297
- // add fields to corresponding DE
298
- fieldKeys.forEach((key) => {
299
- const field = fieldsObj[key];
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
- }
305
- });
306
-
307
- // sort fields by Ordinal value (API returns field unsorted)
308
- for (const metadataEntry in metadata) {
309
- metadata[metadataEntry].Fields.sort(DataExtensionField.sortDeFields);
310
- }
311
-
312
- // remove attributes that we do not want to retrieve
313
- // * do this after sorting on the DE's field list
314
- fieldKeys.forEach((key) => {
315
- DataExtensionField.postRetrieveTasks(fieldsObj[key], true);
316
- });
337
+ await this._attachFields(metadata, fieldOptions, additionalFields);
317
338
  }
318
339
  if (!retrieveDir && buObject.eid !== buObject.mid) {
319
340
  // for caching, we want to retrieve shared DEs as well from the instance parent BU
320
- Util.logger.info('- Caching dependent Metadata: dataExtension (shared via _ParentBU_)');
321
- /** @type {Util.BuObject} */
341
+ Util.logger.info(
342
+ ' - Caching dependent Metadata: dataExtension (shared via _ParentBU_)'
343
+ );
344
+ /** @type {TYPE.BuObject} */
322
345
  const buObjectParentBu = {
323
- clientId: this.properties.credentials[buObject.credential].clientId,
324
- clientSecret: this.properties.credentials[buObject.credential].clientSecret,
325
- tenant: this.properties.credentials[buObject.credential].tenant,
326
346
  eid: this.properties.credentials[buObject.credential].eid,
327
347
  mid: this.properties.credentials[buObject.credential].eid,
328
348
  businessUnit: Util.parentBuName,
329
349
  credential: buObject.credential,
330
350
  };
331
- const clientBackup = this.client;
332
351
  try {
333
- this.client = await Util.getETClient(buObjectParentBu);
352
+ this.client = auth.getSDK(buObjectParentBu);
334
353
  } catch (ex) {
335
354
  Util.logger.error(ex.message);
336
355
  return;
@@ -338,14 +357,11 @@ class DataExtension extends MetadataType {
338
357
  const metadataParentBu = await this._retrieveAll(additionalFields);
339
358
 
340
359
  // get shared folders to match our shared / synched Data Extensions
341
- Util.logger.info('- Caching dependent Metadata: folder (shared via _ParentBU_)');
342
- Folder.cache = {};
360
+ Util.logger.info(' - Caching dependent Metadata: folder (shared via _ParentBU_)');
343
361
  Folder.client = this.client;
344
362
  Folder.properties = this.properties;
345
363
  const result = await Folder.retrieveForCache(buObjectParentBu);
346
- const parentCache = {
347
- folder: result.metadata,
348
- };
364
+ cache.mergeMetadata('folder', result.metadata, buObject.eid);
349
365
 
350
366
  // get the types and clean out non-shared ones
351
367
  const folderTypesFromParent = require('../MetadataTypeDefinitions').folder
@@ -353,12 +369,12 @@ class DataExtension extends MetadataType {
353
369
  for (const metadataEntry in metadataParentBu) {
354
370
  try {
355
371
  // get the data extension type from the folder
356
- const folderContentType = Util.getFromCache(
357
- parentCache,
372
+ const folderContentType = cache.searchForField(
358
373
  'folder',
359
374
  metadataParentBu[metadataEntry].CategoryID,
360
375
  'ID',
361
- 'ContentType'
376
+ 'ContentType',
377
+ buObject.eid
362
378
  );
363
379
  if (!folderTypesFromParent.includes(folderContentType)) {
364
380
  Util.logger.verbose(
@@ -375,16 +391,14 @@ class DataExtension extends MetadataType {
375
391
  }
376
392
 
377
393
  // revert client to current default
378
- this.client = clientBackup;
379
- Folder.client = clientBackup;
380
- Folder.cache = this.cache;
394
+ this.client = auth.getSDK(this.buObject);
395
+ Folder.client = auth.getSDK(this.buObject);
381
396
 
382
397
  // make sure to overwrite parent bu DEs with local ones
383
398
  metadata = { ...metadataParentBu, ...metadata };
384
399
  }
385
-
386
400
  if (retrieveDir) {
387
- const savedMetadata = await this.saveResults(metadata, retrieveDir, null);
401
+ const savedMetadata = await super.saveResults(metadata, retrieveDir, null);
388
402
  Util.logger.info(
389
403
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
390
404
  );
@@ -394,41 +408,62 @@ class DataExtension extends MetadataType {
394
408
  }
395
409
  return { metadata: metadata, type: 'dataExtension' };
396
410
  }
411
+
412
+ /**
413
+ * helper to retrieve all dataExtension fields and attach them to the dataExtension metadata
414
+ *
415
+ * @private
416
+ * @param {TYPE.DataExtensionMap} metadata already cached dataExtension metadata
417
+ * @param {TYPE.SoapRequestParams} [fieldOptions] optionally filter results
418
+ * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
419
+ * @returns {Promise.<void>} -
420
+ */
421
+ static async _attachFields(metadata, fieldOptions, additionalFields) {
422
+ const fieldsObj = await this._retrieveFields(fieldOptions, additionalFields);
423
+ const fieldKeys = Object.keys(fieldsObj);
424
+ // add fields to corresponding DE
425
+ for (const key of fieldKeys) {
426
+ const field = fieldsObj[key];
427
+ if (metadata[field?.DataExtension?.CustomerKey]) {
428
+ metadata[field.DataExtension.CustomerKey].Fields.push(field);
429
+ } else {
430
+ Util.logger.warn(` - Issue retrieving data extension fields. key='${key}'`);
431
+ }
432
+ }
433
+
434
+ // sort fields by Ordinal value (API returns field unsorted)
435
+ for (const metadataEntry in metadata) {
436
+ metadata[metadataEntry].Fields.sort(DataExtensionField.sortDeFields);
437
+ }
438
+
439
+ // remove attributes that we do not want to retrieve
440
+ // * do this after sorting on the DE's field list
441
+ for (const key of fieldKeys) {
442
+ DataExtensionField.postRetrieveTasks(fieldsObj[key], true);
443
+ }
444
+ }
445
+
397
446
  /**
398
447
  * Retrieves dataExtension metadata. Afterwards starts retrieval of dataExtensionColumn metadata retrieval
448
+ *
449
+ * @param {TYPE.BuObject} [buObject] properties for auth
399
450
  * @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
451
+ * @returns {Promise.<{metadata: TYPE.DataExtensionMap, type: string}>} Promise of item map
401
452
  */
402
- static async retrieveChangelog(additionalFields) {
453
+ static async retrieveChangelog(buObject, additionalFields) {
403
454
  const metadata = await this._retrieveAll(additionalFields);
404
455
  return { metadata: metadata, type: 'dataExtension' };
405
456
  }
406
457
  /**
407
458
  * manages post retrieve steps
408
- * @param {DataExtensionItem} metadata a single dataExtension
409
- * @param {string} [_] unused
410
- * @param {boolean} [isTemplating] signals that we are retrieving templates
411
- * @returns {DataExtensionItem} metadata
459
+ *
460
+ * @param {TYPE.DataExtensionItem} metadata a single dataExtension
461
+ * @returns {TYPE.DataExtensionItem} metadata
412
462
  */
413
- static async postRetrieveTasks(metadata, _, isTemplating) {
414
- if (!metadata.Fields || !metadata.Fields.length) {
415
- // assume we were in deploy mode. retrieve fields.
416
- const tempList = {};
417
- tempList[metadata[this.definition.keyField]] = metadata;
418
- await this._retrieveFieldsForSingleDe(tempList, metadata[this.definition.keyField]);
419
- }
420
- // if retrieving template, replace the name with customer key if that wasn't already the case
421
- if (isTemplating) {
422
- const warningMsg =
423
- 'Ensure that Queries that write into this DE are updated with the new key before deployment.';
424
- this.overrideKeyWithName(metadata, warningMsg);
425
- }
463
+ static async postRetrieveTasks(metadata) {
426
464
  // Error during deploy if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
427
465
  // Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
428
- if (
429
- metadata.SendableSubscriberField &&
430
- metadata.SendableSubscriberField.Name === '_SubscriberKey'
431
- ) {
466
+ if (metadata.SendableSubscriberField?.Name === '_SubscriberKey') {
432
467
  metadata.SendableSubscriberField.Name = 'Subscriber Key';
433
468
  }
434
469
  return this._parseMetadata(JSON.parse(JSON.stringify(metadata)));
@@ -436,17 +471,17 @@ class DataExtension extends MetadataType {
436
471
 
437
472
  /**
438
473
  * Helper to retrieve Data Extension Fields
474
+ *
439
475
  * @private
440
- * @param {Object} [options] options (e.g. continueRequest)
476
+ * @param {TYPE.SoapRequestParams} [options] options (e.g. continueRequest)
441
477
  * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
442
- * @returns {Promise<DataExtensionField.DataExtensionFieldMap>} Promise of items
478
+ * @returns {Promise.<TYPE.DataExtensionFieldMap>} Promise of items
443
479
  */
444
480
  static async _retrieveFields(options, additionalFields) {
445
481
  if (!options) {
446
482
  // dont print this during updates or templating which retrieves fields DE-by-DE
447
- Util.logger.info('- Caching dependent Metadata: dataExtensionField');
483
+ Util.logger.info(' - Caching dependent Metadata: dataExtensionField');
448
484
  }
449
- DataExtensionField.cache = this.metadata;
450
485
  DataExtensionField.client = this.client;
451
486
  DataExtensionField.properties = this.properties;
452
487
 
@@ -455,10 +490,11 @@ class DataExtension extends MetadataType {
455
490
  }
456
491
  /**
457
492
  * helps retrieving fields during templating and deploy where we dont want the full list
493
+ *
458
494
  * @private
459
- * @param {DataExtensionMap} metadata list of DEs
495
+ * @param {TYPE.DataExtensionMap} metadata list of DEs
460
496
  * @param {string} customerKey external key of single DE
461
- * @returns {Promise<void>} updates are made directly to `metadata`
497
+ * @returns {Promise.<void>} -
462
498
  */
463
499
  static async _retrieveFieldsForSingleDe(metadata, customerKey) {
464
500
  const fieldOptions = {
@@ -470,34 +506,28 @@ class DataExtension extends MetadataType {
470
506
  };
471
507
  const fieldsObj = await this._retrieveFields(fieldOptions);
472
508
 
473
- DataExtensionField.cache = this.metadata;
474
509
  DataExtensionField.client = this.client;
475
510
  DataExtensionField.properties = this.properties;
476
511
  const fieldArr = DataExtensionField.convertToSortedArray(fieldsObj);
477
512
 
478
513
  // remove attributes that we do not want to retrieve
479
514
  // * do this after sorting on the DE's field list
480
- fieldArr.forEach((field) => {
515
+ for (const field of fieldArr) {
481
516
  DataExtensionField.postRetrieveTasks(field, true);
482
- });
517
+ }
483
518
 
484
519
  metadata[customerKey].Fields = fieldArr;
485
520
  }
486
521
 
487
522
  /**
488
523
  * prepares a DataExtension for deployment
489
- * @param {DataExtensionItem} metadata a single data Extension
490
- * @returns {Promise<DataExtensionItem>} Promise of updated single DE
524
+ *
525
+ * @param {TYPE.DataExtensionItem} metadata a single data Extension
526
+ * @returns {Promise.<TYPE.DataExtensionItem>} Promise of updated single DE
491
527
  */
492
528
  static async preDeployTasks(metadata) {
493
529
  // folder
494
- metadata.CategoryID = Util.getFromCache(
495
- this.cache,
496
- 'folder',
497
- metadata.r__folder_Path,
498
- 'Path',
499
- 'ID'
500
- );
530
+ metadata.CategoryID = cache.searchForField('folder', metadata.r__folder_Path, 'Path', 'ID');
501
531
  delete metadata.r__folder_Path;
502
532
 
503
533
  // DataExtensionTemplate
@@ -519,8 +549,7 @@ class DataExtension extends MetadataType {
519
549
  // get template's CustomerKey
520
550
  try {
521
551
  metadata.Template = {
522
- CustomerKey: Util.getFromCache(
523
- this.cache,
552
+ CustomerKey: cache.searchForField(
524
553
  'dataExtensionTemplate',
525
554
  metadata.r__dataExtensionTemplate_Name,
526
555
  'Name',
@@ -561,9 +590,10 @@ class DataExtension extends MetadataType {
561
590
  /**
562
591
  * Saves json content to a html table in the local file system. Will create the parent directory if it does not exist.
563
592
  * The json's first level of keys must represent the rows and the secend level the columns
593
+ *
564
594
  * @private
565
- * @param {DataExtensionItem} json single dataextension
566
- * @param {Array} tabled prepped array for output in tabular format
595
+ * @param {TYPE.DataExtensionItem} json single dataextension
596
+ * @param {object[][]} tabled prepped array for output in tabular format
567
597
  * @returns {string} file content
568
598
  */
569
599
  static _generateDocHtml(json, tabled) {
@@ -577,37 +607,34 @@ class DataExtension extends MetadataType {
577
607
 
578
608
  output += `<p><b>Description:</b> ${json.Description || 'n/a'}</p>`;
579
609
  output += `<p><b>Folder:</b> ${
580
- json.r__folder_Path
581
- ? json.r__folder_Path
582
- : '<i>Hidden! Could not find folder with ID ' + json.CategoryID + '</i>'
610
+ json.r__folder_Path ||
611
+ '<i>Hidden! Could not find folder with ID ' + json.CategoryID + '</i>'
583
612
  }</p>`;
584
613
  output += `<p><b>Fields in table:</b> ${tabled.length - 1}</p>`;
585
614
  output += '<p><b>Sendable:</b> ';
586
- if (json.IsSendable === 'true') {
587
- output +=
588
- 'Yes (<i>' +
589
- json.SendableDataExtensionField.Name +
590
- '</i> to <i>' +
591
- json.SendableSubscriberField.Name +
592
- '</i>)</p>\n\n';
593
- } else {
594
- output += `No</p>\n\n`;
595
- }
596
- output += `<p><b>Testable:</b> ${json.IsTestable === 'true' ? 'Yes' : 'No'}</p>\n\n`;
615
+ output +=
616
+ json.IsSendable === true
617
+ ? 'Yes (<i>' +
618
+ json.SendableDataExtensionField.Name +
619
+ '</i> to <i>' +
620
+ json.SendableSubscriberField.Name +
621
+ '</i>)</p>\n\n'
622
+ : `No</p>\n\n`;
623
+ output += `<p><b>Testable:</b> ${json.IsTestable === true ? 'Yes' : 'No'}</p>\n\n`;
597
624
  if (json.r__dataExtensionTemplate_Name) {
598
625
  output += `<p><b>Template:</b> ${json.r__dataExtensionTemplate_Name}</p>`;
599
626
  }
600
627
 
601
628
  output += '<table><thead><tr>';
602
- tabled[0].forEach((element) => {
629
+ for (const element of tabled[0]) {
603
630
  output += '<th>' + element + '</th>';
604
- });
631
+ }
605
632
  output += '</tr><thead><tbody>';
606
633
  for (let i = 1; i < tabled.length; i++) {
607
634
  output += '<tr>';
608
- tabled[i].forEach((field) => {
635
+ for (const field of tabled[i]) {
609
636
  output += `<td>${field}</td>`;
610
- });
637
+ }
611
638
  output += '</tr>';
612
639
  }
613
640
  output += '</tbody></table>';
@@ -618,9 +645,10 @@ class DataExtension extends MetadataType {
618
645
  * Experimental: Only working for DataExtensions:
619
646
  * Saves json content to a html table in the local file system. Will create the parent directory if it does not exist.
620
647
  * The json's first level of keys must represent the rows and the secend level the columns
648
+ *
621
649
  * @private
622
- * @param {DataExtensionItem} json dataextension
623
- * @param {Array} tabled prepped array for output in tabular format
650
+ * @param {TYPE.DataExtensionItem} json dataextension
651
+ * @param {object[][]} tabled prepped array for output in tabular format
624
652
  * @returns {string} file content
625
653
  */
626
654
  static _generateDocMd(json, tabled) {
@@ -632,37 +660,34 @@ class DataExtension extends MetadataType {
632
660
  output +=
633
661
  `**Description:** ${json.Description || 'n/a'}\n\n` +
634
662
  `**Folder:** ${
635
- json.r__folder_Path
636
- ? json.r__folder_Path
637
- : '_Hidden! Could not find folder with ID ' + json.CategoryID + '_'
663
+ json.r__folder_Path ||
664
+ '_Hidden! Could not find folder with ID ' + json.CategoryID + '_'
638
665
  }/\n\n` +
639
666
  `**Fields in table:** ${tabled.length - 1}\n\n`;
640
667
  output += '**Sendable:** ';
641
- if (json.IsSendable === 'true') {
642
- output +=
643
- 'Yes (`' +
644
- json.SendableDataExtensionField.Name +
645
- '` to `' +
646
- json.SendableSubscriberField.Name +
647
- '`)\n\n';
648
- } else {
649
- output += `No\n\n`;
650
- }
651
- output += `**Testable:** ${json.IsTestable === 'true' ? 'Yes' : 'No'}\n\n`;
668
+ output +=
669
+ json.IsSendable === true
670
+ ? 'Yes (`' +
671
+ json.SendableDataExtensionField.Name +
672
+ '` to `' +
673
+ json.SendableSubscriberField.Name +
674
+ '`)\n\n'
675
+ : `No\n\n`;
676
+ output += `**Testable:** ${json.IsTestable === true ? 'Yes' : 'No'}\n\n`;
652
677
  if (json.r__dataExtensionTemplate_Name) {
653
678
  output += `**Template:** ${json.r__dataExtensionTemplate_Name}\n\n`;
654
679
  }
655
680
 
656
681
  let tableSeparator = '';
657
- tabled[0].forEach((column) => {
682
+ for (const column of tabled[0]) {
658
683
  output += `| ${column} `;
659
684
  tableSeparator += '| --- ';
660
- });
685
+ }
661
686
  output += `|\n${tableSeparator}|\n`;
662
687
  for (let i = 1; i < tabled.length; i++) {
663
- tabled[i].forEach((field) => {
688
+ for (const field of tabled[i]) {
664
689
  output += `| ${field} `;
665
- });
690
+ }
666
691
  output += '|\n';
667
692
  }
668
693
  return output;
@@ -671,28 +696,26 @@ class DataExtension extends MetadataType {
671
696
  /**
672
697
  * Saves json content to a html table in the local file system. Will create the parent directory if it does not exist.
673
698
  * The json's first level of keys must represent the rows and the secend level the columns
699
+ *
674
700
  * @private
675
701
  * @param {string} directory directory the file will be written to
676
702
  * @param {string} filename name of the file without '.json' ending
677
- * @param {DataExtensionItem} json dataextension.columns
703
+ * @param {TYPE.DataExtensionItem} json dataextension.columns
678
704
  * @param {'html'|'md'} mode html or md
679
705
  * @param {string[]} [fieldsToKeep] list of keys(columns) to show. This will also specify
680
- * @returns {Promise<boolean>} Promise of success of saving the file
706
+ * @returns {Promise.<boolean>} Promise of success of saving the file
681
707
  */
682
708
  static async _writeDoc(directory, filename, json, mode, fieldsToKeep) {
683
- if (!File.existsSync(directory)) {
684
- File.mkdirpSync(directory);
685
- }
686
709
  let fieldsJson = Object.values(json.Fields);
687
710
  if (fieldsToKeep) {
688
711
  const newJson = [];
689
- fieldsJson.forEach((element) => {
712
+ for (const element of fieldsJson) {
690
713
  const newJsonElement = {};
691
- fieldsToKeep.forEach((field) => {
714
+ for (const field of fieldsToKeep) {
692
715
  newJsonElement[field] = element[field];
693
- });
716
+ }
694
717
  newJson.push(newJsonElement);
695
- });
718
+ }
696
719
  fieldsJson = newJson;
697
720
  }
698
721
  const tabled = jsonToTable(fieldsJson);
@@ -704,41 +727,44 @@ class DataExtension extends MetadataType {
704
727
  }
705
728
  try {
706
729
  // write to disk
707
- await File.writeToFile(directory, filename + '.dataExtension', mode, output);
730
+ await File.writeToFile(directory, filename + '.dataExtension-doc', mode, output);
708
731
  } catch (ex) {
709
732
  Util.logger.error(`DataExtension.writeDeToX(${mode}):: error | ` + ex.message);
710
733
  }
711
734
  }
712
735
  /**
713
736
  * Parses metadata into a readable Markdown/HTML format then saves it
714
- * @param {Util.BuObject} buObject properties for auth
715
- * @param {DataExtensionMap} [metadata] a list of dataExtension definitions
716
- * @param {boolean} [isDeploy] used to skip non-supported message during deploy
717
- * @returns {Promise<void>} -
737
+ *
738
+ * @param {TYPE.BuObject} buObject properties for auth
739
+ * @param {TYPE.DataExtensionMap} [metadata] a list of dataExtension definitions
740
+ * @returns {Promise.<void>} -
718
741
  */
719
- static async document(buObject, metadata, isDeploy) {
720
- if (!metadata) {
721
- metadata = this.readBUMetadataForType(
722
- File.normalizePath([
723
- this.properties.directories.retrieve,
724
- buObject.credential,
725
- buObject.businessUnit,
726
- ]),
727
- true
728
- ).dataExtension;
742
+ static async document(buObject, metadata) {
743
+ try {
744
+ if (!metadata) {
745
+ metadata = this.readBUMetadataForType(
746
+ File.normalizePath([
747
+ this.properties.directories.retrieve,
748
+ buObject.credential,
749
+ buObject.businessUnit,
750
+ ]),
751
+ true
752
+ ).dataExtension;
753
+ }
754
+ } catch (ex) {
755
+ Util.logger.error(ex.message);
756
+ return;
729
757
  }
730
758
  const docPath = File.normalizePath([
731
- this.properties.directories.dataExtension,
759
+ this.properties.directories.retrieve,
732
760
  buObject.credential,
733
761
  buObject.businessUnit,
762
+ this.definition.type,
734
763
  ]);
735
764
  if (!metadata || !Object.keys(metadata).length) {
736
765
  // as part of retrieve & manual execution we could face an empty folder
737
766
  return;
738
767
  }
739
- if (!isDeploy) {
740
- File.removeSync(docPath);
741
- }
742
768
  const columnsToIterateThrough = ['IsNullable', 'IsPrimaryKey'];
743
769
  const columnsToPrint = [
744
770
  'Name',
@@ -748,130 +774,101 @@ class DataExtension extends MetadataType {
748
774
  'IsNullable',
749
775
  'DefaultValue',
750
776
  ];
751
- for (const customerKey in metadata) {
752
- if (metadata[customerKey].Fields && metadata[customerKey].Fields.length) {
753
- metadata[customerKey].Fields.forEach((field) => {
754
- field.IsNullable = (!(field.IsRequired === 'true')).toString();
755
- columnsToIterateThrough.forEach((key) => {
756
- if (field[key] === 'true') {
757
- field[key] = '+';
758
- } else if (field[key] === 'false') {
759
- field[key] = '-';
777
+ return Promise.all(
778
+ Object.keys(metadata).map((customerKey) => {
779
+ // for (const customerKey in metadata) {
780
+ if (metadata[customerKey]?.Fields?.length) {
781
+ for (const field of metadata[customerKey].Fields) {
782
+ field.IsNullable = !Util.isTrue(field.IsRequired);
783
+ for (const key of columnsToIterateThrough) {
784
+ if (Util.isTrue(field[key])) {
785
+ field[key] = '+';
786
+ } else if (Util.isFalse(field[key])) {
787
+ field[key] = '-';
788
+ }
760
789
  }
761
- });
762
- });
790
+ }
763
791
 
764
- if (['html', 'both'].includes(this.properties.options.documentType)) {
765
- this._writeDoc(
766
- docPath + '/',
767
- customerKey,
768
- metadata[customerKey],
769
- 'html',
770
- columnsToPrint
771
- );
772
- }
773
- if (['md', 'both'].includes(this.properties.options.documentType)) {
774
- this._writeDoc(
775
- docPath + '/',
776
- customerKey,
777
- metadata[customerKey],
778
- 'md',
779
- columnsToPrint
780
- );
792
+ if (['html', 'both'].includes(this.properties.options.documentType)) {
793
+ return this._writeDoc(
794
+ docPath + '/',
795
+ customerKey,
796
+ metadata[customerKey],
797
+ 'html',
798
+ columnsToPrint
799
+ );
800
+ }
801
+ if (['md', 'both'].includes(this.properties.options.documentType)) {
802
+ return this._writeDoc(
803
+ docPath + '/',
804
+ customerKey,
805
+ metadata[customerKey],
806
+ 'md',
807
+ columnsToPrint
808
+ );
809
+ }
781
810
  }
782
- }
783
- }
784
- if (['html', 'both'].includes(this.properties.options.documentType)) {
785
- Util.logger.info(`Created ${docPath}/*.dataExtension.html`);
786
- }
787
- if (['md', 'both'].includes(this.properties.options.documentType)) {
788
- Util.logger.info(`Created ${docPath}/*.dataExtension.md`);
789
- }
811
+ })
812
+ );
790
813
  }
791
814
 
792
815
  /**
793
- * Delete a data extension from the specified business unit
794
- * @param {Object} buObject references credentials
816
+ * Delete a metadata item from the specified business unit
817
+ *
818
+ * @param {TYPE.BuObject} buObject references credentials
795
819
  * @param {string} customerKey Identifier of data extension
796
- * @returns {Promise<void>} -
820
+ * @returns {Promise.<boolean>} deletion success status
797
821
  */
798
- static async deleteByKey(buObject, customerKey) {
799
- let client;
800
- try {
801
- client = await Util.getETClient(buObject);
802
- } catch (ex) {
803
- Util.logger.error(ex.message);
804
- return;
805
- }
806
- const config = {
807
- props: { CustomerKey: customerKey },
808
- };
822
+ static deleteByKey(buObject, customerKey) {
823
+ return super.deleteByKeySOAP(buObject, customerKey, false);
824
+ }
809
825
 
810
- client.dataExtension(config).delete((error, response) => {
811
- if (error && error.results && error.results[0]) {
812
- if (error.results[0].ErrorCode === '310007') {
813
- Util.logger.error(
814
- 'mcdev.deleteDE:: It seems the DataExtension you were trying to delete does not exist on the given BU or its External Key was changed.'
815
- );
816
- } else {
817
- Util.logger.error('mcdev.deleteDE:: ' + error.results[0].StatusMessage);
818
- }
819
- } else if (error) {
820
- Util.logger.error('mcdev.deleteDE:: ' + JSON.stringify(error));
821
- } else if (
822
- response &&
823
- response.body &&
824
- response.body.Results &&
825
- response.body.Results[0]
826
- ) {
827
- Util.logger.info(
828
- `mcdev.deleteDE:: Success: ${response.body.Results[0].StatusMessage} (${customerKey})`
829
- );
830
- // delete local copy: retrieve/cred/bu/dataExtension/...json
831
- const jsonFile = File.normalizePath([
832
- this.properties.directories.retrieve,
833
- buObject.credential,
834
- buObject.businessUnit,
835
- this.definition.type,
836
- `${customerKey}.${this.definition.type}-meta.json`,
837
- ]);
838
- if (File.existsSync(jsonFile)) {
839
- File.unlinkSync(jsonFile);
840
- }
841
- // delete local copy: doc/dataExtension/cred/bu/...md
842
- const mdFile = File.normalizePath([
843
- this.properties.directories.dataExtension,
844
- buObject.credential,
845
- buObject.businessUnit,
846
- `${customerKey}.${this.definition.type}.md`,
847
- ]);
848
- if (File.existsSync(mdFile)) {
849
- File.unlinkSync(mdFile);
850
- }
851
- } else {
852
- Util.logger.info('mcdev.deleteDE:: Success: ' + JSON.stringify(response));
853
- }
854
- });
826
+ /**
827
+ * clean up after deleting a metadata item
828
+ *
829
+ * @param {TYPE.BuObject} buObject references credentials
830
+ * @param {string} customerKey Identifier of metadata item
831
+ * @returns {void}
832
+ */
833
+ static async postDeleteTasks(buObject, customerKey) {
834
+ // delete local copy: retrieve/cred/bu/dataExtension/...json
835
+ const jsonFile = File.normalizePath([
836
+ this.properties.directories.retrieve,
837
+ buObject.credential,
838
+ buObject.businessUnit,
839
+ this.definition.type,
840
+ `${customerKey}.${this.definition.type}-meta.json`,
841
+ ]);
842
+ await File.remove(jsonFile);
843
+ // delete local copy: doc/dataExtension/cred/bu/...md
844
+ const mdFile = File.normalizePath([
845
+ this.properties.directories.docs,
846
+ 'dataExtension',
847
+ buObject.credential,
848
+ buObject.businessUnit,
849
+ `${customerKey}.${this.definition.type}.md`,
850
+ ]);
851
+ await File.remove(mdFile);
855
852
  }
856
853
 
857
854
  /**
858
855
  * Retrieves folder metadata into local filesystem. Also creates a uniquePath attribute for each folder.
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
862
- * @returns {Promise} Promise
856
+ *
857
+ * @param {TYPE.BuObject} buObject properties for auth
858
+ * @returns {Promise.<{metadata: TYPE.DataExtensionMap, type: string}>} Promise
863
859
  */
864
- static async retrieveForCache(buObject, _, isDeploy) {
865
- return this.retrieve(null, ['ObjectID', 'CustomerKey', 'Name'], buObject, null, isDeploy);
860
+ static async retrieveForCache(buObject) {
861
+ return this.retrieve(null, ['ObjectID', 'CustomerKey', 'Name'], buObject, null, null);
866
862
  }
867
863
  /**
868
864
  * Retrieves dataExtension metadata in template format.
865
+ *
869
866
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
870
867
  * @param {string} name name of the metadata item
871
- * @param {Util.TemplateMap} variables variables to be replaced in the metadata
872
- * @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of items
868
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
869
+ * @returns {Promise.<{metadata: TYPE.DataExtensionMap, type: string}>} Promise of items
873
870
  */
874
- static async retrieveAsTemplate(templateDir, name, variables) {
871
+ static async retrieveAsTemplate(templateDir, name, templateVariables) {
875
872
  const options = {
876
873
  filter: {
877
874
  leftOperand: 'Name',
@@ -879,8 +876,7 @@ class DataExtension extends MetadataType {
879
876
  rightOperand: name,
880
877
  },
881
878
  };
882
- /** @type DataExtensionMap */
883
- // const metadata = super.parseResponseBody(response.body);
879
+
884
880
  const metadata = await this._retrieveAll(null, options);
885
881
 
886
882
  if (!Object.keys(metadata).length) {
@@ -896,46 +892,42 @@ class DataExtension extends MetadataType {
896
892
  // API returns field unsorted
897
893
  metadata[key].Fields.sort((a, b) => a.Ordinal - b.Ordinal);
898
894
 
895
+ const originalKey = key;
899
896
  const metadataCleaned = JSON.parse(
900
- JSON.stringify(await this.postRetrieveTasks(metadata[key], null, !!variables))
897
+ JSON.stringify(await this.postRetrieveTasks(metadata[key]))
901
898
  );
902
899
 
903
900
  this.keepTemplateFields(metadataCleaned);
904
901
  const metadataTemplated = JSON.parse(
905
- Util.replaceByObject(JSON.stringify(metadataCleaned), variables)
902
+ Util.replaceByObject(JSON.stringify(metadataCleaned), templateVariables)
906
903
  );
907
- File.writeJSONToFile(
904
+ await File.writeJSONToFile(
908
905
  [templateDir, this.definition.type].join('/'),
909
- metadataTemplated[this.definition.keyField] +
910
- '.' +
911
- this.definition.type +
912
- '-meta',
906
+ originalKey + '.' + this.definition.type + '-meta',
913
907
  metadataTemplated
914
908
  );
915
909
  } catch (ex) {
916
910
  Util.metadataLogger('error', this.definition.type, 'retrieve', ex, key);
917
911
  }
918
912
  }
913
+ Util.logger.info(`- templated ${this.definition.type}: ${customerKey}`);
919
914
 
920
- Util.logger.info(
921
- `DataExtension.retrieveAsTemplate:: All records written to filesystem (${customerKey})`
922
- );
923
- return { metadata: metadata, type: 'dataExtension' };
915
+ return { metadata: metadata[customerKey], type: 'dataExtension' };
924
916
  }
925
917
 
926
918
  /**
927
919
  * parses retrieved Metadata before saving
920
+ *
928
921
  * @private
929
- * @param {DataExtensionItem} metadata a single dataExtension definition
930
- * @returns {DataExtensionItem} a single dataExtension definition
922
+ * @param {TYPE.DataExtensionItem} metadata a single dataExtension definition
923
+ * @returns {TYPE.DataExtensionItem} a single dataExtension definition
931
924
  */
932
925
  static _parseMetadata(metadata) {
933
926
  let error = false;
934
927
  let verbose = false;
935
928
  // data extension type (from folder)
936
929
  try {
937
- metadata.r__folder_ContentType = Util.getFromCache(
938
- this.cache,
930
+ metadata.r__folder_ContentType = cache.searchForField(
939
931
  'folder',
940
932
  metadata.CategoryID,
941
933
  'ID',
@@ -947,13 +939,12 @@ class DataExtension extends MetadataType {
947
939
  metadata.r__folder_ContentType = 'synchronizeddataextension';
948
940
  } else {
949
941
  error = true;
950
- Util.logger.warn(`Data Extension '${metadata.Name}': ${ex.message}`);
942
+ Util.logger.warn(` - dataExtension '${metadata.Name}': ${ex.message}`);
951
943
  }
952
944
  }
953
945
  // folder
954
946
  try {
955
- metadata.r__folder_Path = Util.getFromCache(
956
- this.cache,
947
+ metadata.r__folder_Path = cache.searchForField(
957
948
  'folder',
958
949
  metadata.CategoryID,
959
950
  'ID',
@@ -975,10 +966,9 @@ class DataExtension extends MetadataType {
975
966
  }
976
967
  }
977
968
  // DataExtensionTemplate
978
- if (metadata.Template && metadata.Template.CustomerKey) {
969
+ if (metadata.Template?.CustomerKey) {
979
970
  try {
980
- metadata.r__dataExtensionTemplate_Name = Util.getFromCache(
981
- this.cache,
971
+ metadata.r__dataExtensionTemplate_Name = cache.searchForField(
982
972
  'dataExtensionTemplate',
983
973
  metadata.Template.CustomerKey,
984
974
  'CustomerKey',
@@ -992,26 +982,29 @@ class DataExtension extends MetadataType {
992
982
  // A workaround exists but it's likely not beneficial to explain to users:
993
983
  // Create a DE based on the not-supported template on the target BU, retrieve it, copy the Template.CustomerKey into the to-be-deployed DE (or use mcdev-templating), done
994
984
  Util.logger.warn(
995
- `Issue with DataExtension '${
985
+ ` - Issue with dataExtension '${
996
986
  metadata[this.definition.nameField]
997
987
  }': Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`
998
988
  );
999
989
  }
1000
990
  }
991
+ // remove the date fields manually here because we need them in the changelog but not in the saved json
992
+ delete metadata.CreatedDate;
993
+ delete metadata.ModifiedDate;
1001
994
 
1002
995
  return metadata;
1003
996
  }
1004
997
 
1005
998
  /**
1006
999
  * Retrieves dataExtension metadata and cleans it
1000
+ *
1007
1001
  * @private
1008
1002
  * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
1009
- * @param {Object} [options] e.g. filter
1010
- * @returns {Promise<DataExtensionMap>} keyField => metadata map
1003
+ * @param {TYPE.SoapRequestParams} [options] e.g. filter
1004
+ * @returns {Promise.<TYPE.DataExtensionMap>} keyField => metadata map
1011
1005
  */
1012
1006
  static async _retrieveAll(additionalFields, options) {
1013
- const metadata = (await super.retrieveSOAPgeneric(null, null, options, additionalFields))
1014
- .metadata;
1007
+ const { metadata } = await super.retrieveSOAP(null, null, options, additionalFields);
1015
1008
  for (const key in metadata) {
1016
1009
  // some system data extensions do not have CategoryID which throws errors in other places. These do not need to be parsed
1017
1010
  if (!metadata[key].CategoryID) {
@@ -1022,14 +1015,36 @@ class DataExtension extends MetadataType {
1022
1015
  }
1023
1016
  return metadata;
1024
1017
  }
1018
+ /**
1019
+ * should return only the json for all but asset, query and script that are saved as multiple files
1020
+ * additionally, the documentation for dataExtension and automation should be returned
1021
+ *
1022
+ * @param {string[]} keyArr customerkey of the metadata
1023
+ * @returns {string[]} list of all files that need to be committed in a flat array ['path/file1.ext', 'path/file2.ext']
1024
+ */
1025
+ static getFilesToCommit(keyArr) {
1026
+ if (!this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
1027
+ // document dataExtension is not active upon retrieve, run default method instead
1028
+ return super.getFilesToCommit(keyArr);
1029
+ } else {
1030
+ // document dataExtension is active. assume we want to commit the MD file as well
1031
+ const path = File.normalizePath([
1032
+ this.properties.directories.retrieve,
1033
+ this.buObject.credential,
1034
+ this.buObject.businessUnit,
1035
+ this.definition.type,
1036
+ ]);
1037
+
1038
+ const fileList = keyArr.flatMap((key) => [
1039
+ File.normalizePath([path, `${key}.${this.definition.type}-meta.json`]),
1040
+ File.normalizePath([path, `${key}.${this.definition.type}-doc.md`]),
1041
+ ]);
1042
+ return fileList;
1043
+ }
1044
+ }
1025
1045
  }
1026
1046
 
1027
1047
  // Assign definition to static attributes
1028
1048
  DataExtension.definition = require('../MetadataTypeDefinitions').dataExtension;
1029
- /**
1030
- * @type {Util.ET_Client}
1031
- */
1032
- DataExtension.client = undefined;
1033
- DataExtension.cache = {};
1034
1049
 
1035
1050
  module.exports = DataExtension;