mcdev 4.3.4 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/.coverage-comment-template.md +20 -0
  2. package/.coverage-comment-template.svelte +178 -0
  3. package/.eslintrc.json +2 -0
  4. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  5. package/.github/workflows/code-test.yml +36 -0
  6. package/.github/workflows/coverage-base-update.yml +57 -0
  7. package/.github/workflows/coverage-develop-branch.yml +41 -0
  8. package/.github/workflows/coverage-main-branch.yml +41 -0
  9. package/.github/workflows/coverage.yml +77 -0
  10. package/.husky/post-checkout +1 -1
  11. package/.prettierrc +1 -1
  12. package/.vscode/extensions.json +0 -4
  13. package/README.md +1 -1
  14. package/boilerplate/config.json +1 -1
  15. package/boilerplate/files/.prettierrc +1 -1
  16. package/boilerplate/files/.vscode/extensions.json +1 -1
  17. package/boilerplate/forcedUpdates.json +4 -0
  18. package/docs/dist/documentation.md +1196 -430
  19. package/lib/Builder.js +6 -1
  20. package/lib/Deployer.js +30 -5
  21. package/lib/MetadataTypeDefinitions.js +8 -6
  22. package/lib/MetadataTypeInfo.js +8 -6
  23. package/lib/cli.js +54 -42
  24. package/lib/index.js +82 -8
  25. package/lib/metadataTypes/Asset.js +73 -1
  26. package/lib/metadataTypes/AttributeGroup.js +0 -1
  27. package/lib/metadataTypes/Automation.js +48 -5
  28. package/lib/metadataTypes/Campaign.js +20 -7
  29. package/lib/metadataTypes/ContentArea.js +1 -1
  30. package/lib/metadataTypes/DataExtension.js +221 -184
  31. package/lib/metadataTypes/DataExtensionField.js +12 -19
  32. package/lib/metadataTypes/DataExtensionTemplate.js +1 -1
  33. package/lib/metadataTypes/DataExtract.js +1 -1
  34. package/lib/metadataTypes/DataExtractType.js +1 -1
  35. package/lib/metadataTypes/Email.js +1 -1
  36. package/lib/metadataTypes/{EmailSendDefinition.js → EmailSend.js} +5 -5
  37. package/lib/metadataTypes/{EventDefinition.js → Event.js} +17 -35
  38. package/lib/metadataTypes/{FtpLocation.js → FileLocation.js} +2 -2
  39. package/lib/metadataTypes/FileTransfer.js +8 -7
  40. package/lib/metadataTypes/Filter.js +1 -1
  41. package/lib/metadataTypes/Folder.js +8 -3
  42. package/lib/metadataTypes/ImportFile.js +6 -6
  43. package/lib/metadataTypes/{Interaction.js → Journey.js} +311 -147
  44. package/lib/metadataTypes/List.js +2 -2
  45. package/lib/metadataTypes/MetadataType.js +318 -90
  46. package/lib/metadataTypes/MobileCode.js +0 -1
  47. package/lib/metadataTypes/MobileKeyword.js +336 -40
  48. package/lib/metadataTypes/MobileMessage.js +473 -0
  49. package/lib/metadataTypes/Query.js +114 -32
  50. package/lib/metadataTypes/Role.js +60 -21
  51. package/lib/metadataTypes/Script.js +2 -3
  52. package/lib/metadataTypes/SendClassification.js +40 -0
  53. package/lib/metadataTypes/SetDefinition.js +1 -7
  54. package/lib/metadataTypes/TransactionalEmail.js +2 -3
  55. package/lib/metadataTypes/TransactionalMessage.js +1 -2
  56. package/lib/metadataTypes/TransactionalSMS.js +8 -15
  57. package/lib/metadataTypes/{TriggeredSendDefinition.js → TriggeredSend.js} +35 -27
  58. package/lib/metadataTypes/User.js +1185 -0
  59. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  60. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +1 -0
  61. package/lib/metadataTypes/definitions/Automation.definition.js +3 -2
  62. package/lib/metadataTypes/definitions/Campaign.definition.js +79 -4
  63. package/lib/metadataTypes/definitions/ContentArea.definition.js +1 -0
  64. package/lib/metadataTypes/definitions/DataExtension.definition.js +2 -1
  65. package/lib/metadataTypes/definitions/DataExtensionField.definition.js +1 -0
  66. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +1 -0
  67. package/lib/metadataTypes/definitions/DataExtract.definition.js +1 -0
  68. package/lib/metadataTypes/definitions/DataExtractType.definition.js +1 -0
  69. package/lib/metadataTypes/definitions/Discovery.definition.js +1 -0
  70. package/lib/metadataTypes/definitions/Email.definition.js +1 -0
  71. package/lib/metadataTypes/definitions/{EmailSendDefinition.definition.js → EmailSend.definition.js} +4 -2
  72. package/lib/metadataTypes/definitions/{EventDefinition.definition.js → Event.definition.js} +2 -1
  73. package/lib/metadataTypes/definitions/{FtpLocation.definition.js → FileLocation.definition.js} +4 -3
  74. package/lib/metadataTypes/definitions/FileTransfer.definition.js +3 -2
  75. package/lib/metadataTypes/definitions/Filter.definition.js +1 -0
  76. package/lib/metadataTypes/definitions/Folder.definition.js +2 -0
  77. package/lib/metadataTypes/definitions/ImportFile.definition.js +4 -3
  78. package/lib/metadataTypes/definitions/{Interaction.definition.js → Journey.definition.js} +11 -2
  79. package/lib/metadataTypes/definitions/List.definition.js +1 -0
  80. package/lib/metadataTypes/definitions/MobileCode.definition.js +3 -1
  81. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +27 -17
  82. package/lib/metadataTypes/definitions/MobileMessage.definition.js +743 -0
  83. package/lib/metadataTypes/definitions/Query.definition.js +3 -2
  84. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  85. package/lib/metadataTypes/definitions/Script.definition.js +1 -0
  86. package/lib/metadataTypes/definitions/SendClassification.definition.js +114 -0
  87. package/lib/metadataTypes/definitions/SetDefinition.definition.js +1 -0
  88. package/lib/metadataTypes/definitions/TransactionalEmail.definition.js +2 -1
  89. package/lib/metadataTypes/definitions/TransactionalPush.definition.js +1 -0
  90. package/lib/metadataTypes/definitions/TransactionalSMS.definition.js +1 -0
  91. package/lib/metadataTypes/definitions/{TriggeredSendDefinition.definition.js → TriggeredSend.definition.js} +5 -3
  92. package/lib/metadataTypes/definitions/User.definition.js +365 -0
  93. package/lib/retrieveChangelog.js +1 -2
  94. package/lib/util/auth.js +38 -9
  95. package/lib/util/businessUnit.js +3 -3
  96. package/lib/util/cli.js +55 -7
  97. package/lib/util/devops.js +6 -4
  98. package/lib/util/file.js +55 -13
  99. package/lib/util/init.config.js +1 -2
  100. package/lib/util/init.npm.js +3 -3
  101. package/lib/util/util.js +23 -14
  102. package/package.json +16 -15
  103. package/test/general.test.js +62 -0
  104. package/test/mockRoot/.mcdevrc.json +7 -5
  105. package/test/mockRoot/deploy/testInstance/_ParentBU_/user/testBlocked_user.user-meta.json +23 -0
  106. package/test/mockRoot/deploy/testInstance/_ParentBU_/user/testExisting_user.user-meta.json +31 -0
  107. package/test/mockRoot/deploy/testInstance/_ParentBU_/user/testNew_user.user-meta.json +27 -0
  108. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/{childBU_dataextension_test.dataExtension-meta.json → testExisting_dataExtension.dataExtension-meta.json} +2 -2
  109. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/{testDataExtension.dataExtension-meta.json → testNew_dataExtension.dataExtension-meta.json} +2 -2
  110. package/test/mockRoot/deploy/testInstance/testBU/journey/testExisting_interaction.interaction-meta.json +576 -0
  111. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword.mobileKeyword-meta.amp +2 -0
  112. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword.mobileKeyword-meta.json +10 -0
  113. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword_blocked.mobileKeyword-meta.amp +2 -0
  114. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword_blocked.mobileKeyword-meta.json +10 -0
  115. package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/NTIzOjc4OjA.mobileMessage-meta.amp +1 -0
  116. package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/NTIzOjc4OjA.mobileMessage-meta.json +61 -0
  117. package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/new.mobileMessage-meta.amp +1 -0
  118. package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/new.mobileMessage-meta.json +60 -0
  119. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +1 -1
  120. package/test/mockRoot/deploy/testInstance/testBU/query/testNewQuery.query-meta.json +1 -1
  121. package/test/mockRoot/deploy/testInstance/testBU/query/testNewQuery.query-meta.sql +1 -1
  122. package/test/mockRoot/deploy/testInstance/testBU/transactionalEmail/testExisting_temail.transactionalEmail-meta.json +1 -1
  123. package/test/mockRoot/deploy/testInstance/testBU/transactionalEmail/testNew_temail.transactionalEmail-meta.json +1 -1
  124. package/test/resourceFactory.js +13 -0
  125. package/test/resources/1111111/accountUser/configure-response.xml +70 -0
  126. package/test/resources/1111111/accountUser/create-response.xml +97 -0
  127. package/test/resources/1111111/accountUser/retrieve-response.xml +156 -0
  128. package/test/resources/1111111/accountUser/update-response.xml +111 -0
  129. package/test/resources/1111111/accountUserAccount/retrieve-response.xml +77 -0
  130. package/test/resources/1111111/platform/v1/setup/quickflow/data/get-response.json +455 -0
  131. package/test/resources/1111111/role/retrieve-response.xml +76 -0
  132. package/test/resources/1111111/user/build-expected.json +16 -0
  133. package/test/resources/1111111/user/create-expected.json +21 -0
  134. package/test/resources/1111111/user/retrieve-expected.json +24 -0
  135. package/test/resources/1111111/user/template-expected.json +16 -0
  136. package/test/resources/1111111/user/update-expected.json +21 -0
  137. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/delete-response.json +1 -0
  138. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/get-response.json +17 -0
  139. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +3 -3
  140. package/test/resources/9999999/automation/v1/queries/get-response.json +21 -4
  141. package/test/resources/9999999/automation/v1/queries/post-response.json +4 -4
  142. package/test/resources/9999999/data/v1/customobjectdata/key/{childBU_dataextension_test → testExisting_dataExtension}/rowset/get-response.json +1 -1
  143. package/test/resources/9999999/dataExtension/build-expected.json +3 -3
  144. package/test/resources/9999999/dataExtension/create-expected.json +2 -2
  145. package/test/resources/9999999/dataExtension/create-response.xml +8 -3
  146. package/test/resources/9999999/dataExtension/retrieve-expected.json +3 -3
  147. package/test/resources/9999999/dataExtension/retrieve-response.xml +9 -4
  148. package/test/resources/9999999/dataExtension/template-expected.json +3 -3
  149. package/test/resources/9999999/dataExtension/update-expected.json +3 -3
  150. package/test/resources/9999999/dataExtension/update-response.xml +9 -4
  151. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +14 -9
  152. package/test/resources/9999999/interaction/v1/interactions/get-response.json +312 -0
  153. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_interaction/get-response.json +312 -0
  154. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_interaction/put-response.json +592 -0
  155. package/test/resources/9999999/journey/build-expected.json +572 -0
  156. package/test/resources/9999999/journey/get-expected.json +576 -0
  157. package/test/resources/9999999/journey/put-expected.json +576 -0
  158. package/test/resources/9999999/journey/template-expected.json +572 -0
  159. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/NXV4ZFMwTEFwRVczd3RaLUF5X3p5dzo4Njow/get-response.json +42 -0
  160. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/cTVJaG5oSDJPVUNHcUh6Z3pQT2tVdzo4Njow/delete-response.json +0 -0
  161. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/get-response.json +1 -0
  162. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/post-response.json +3 -0
  163. package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/delete-response.json +0 -0
  164. package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/get-response.json +106 -0
  165. package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/post-response.json +0 -0
  166. package/test/resources/9999999/legacy/v1/beta/mobile/message/NTQ3Ojc4OjA/get-response.json +127 -0
  167. package/test/resources/9999999/legacy/v1/beta/mobile/message/get-response.json +129 -0
  168. package/test/resources/9999999/legacy/v1/beta/mobile/message/post-response.json +3 -0
  169. package/test/resources/9999999/legacy/v1/beta2/data/campaign/get-response.json +29 -0
  170. package/test/resources/9999999/messaging/v1/email/definitions/post-response.json +1 -1
  171. package/test/resources/9999999/messaging/v1/email/definitions/testExisting_temail/get-response.json +1 -1
  172. package/test/resources/9999999/messaging/v1/email/definitions/testExisting_temail/patch-response.json +1 -1
  173. package/test/resources/9999999/mobileKeyword/build-expected.amp +2 -0
  174. package/test/resources/9999999/mobileKeyword/build-expected.json +9 -0
  175. package/test/resources/9999999/mobileKeyword/get-expected.amp +2 -0
  176. package/test/resources/9999999/mobileKeyword/get-expected.json +15 -0
  177. package/test/resources/9999999/mobileKeyword/post-create-expected.amp +2 -0
  178. package/test/resources/9999999/mobileKeyword/post-create-expected.json +17 -0
  179. package/test/resources/9999999/mobileKeyword/template-expected.amp +2 -0
  180. package/test/resources/9999999/mobileKeyword/template-expected.json +9 -0
  181. package/test/resources/9999999/mobileMessage/build-expected.amp +1 -0
  182. package/test/resources/9999999/mobileMessage/build-expected.json +60 -0
  183. package/test/resources/9999999/mobileMessage/get-expected.amp +1 -0
  184. package/test/resources/9999999/mobileMessage/get-expected.json +61 -0
  185. package/test/resources/9999999/mobileMessage/post-create-expected.amp +1 -0
  186. package/test/resources/9999999/mobileMessage/post-create-expected.json +63 -0
  187. package/test/resources/9999999/mobileMessage/post-update-expected.amp +1 -0
  188. package/test/resources/9999999/mobileMessage/post-update-expected.json +61 -0
  189. package/test/resources/9999999/mobileMessage/template-expected.amp +1 -0
  190. package/test/resources/9999999/mobileMessage/template-expected.json +60 -0
  191. package/test/resources/9999999/query/build-expected.json +1 -1
  192. package/test/resources/9999999/query/get-expected.json +1 -1
  193. package/test/resources/9999999/query/get2-expected.json +11 -0
  194. package/test/resources/9999999/query/patch-expected.json +1 -1
  195. package/test/resources/9999999/query/post-expected.json +1 -1
  196. package/test/resources/9999999/query/template-expected.json +1 -1
  197. package/test/resources/9999999/queryDefinition/retrieve-response.xml +30 -0
  198. package/test/resources/9999999/transactionalEmail/build-expected.json +5 -5
  199. package/test/resources/9999999/transactionalEmail/get-expected.json +1 -1
  200. package/test/resources/9999999/transactionalEmail/patch-expected.json +1 -1
  201. package/test/resources/9999999/transactionalEmail/post-expected.json +1 -1
  202. package/test/resources/9999999/transactionalEmail/template-expected.json +5 -5
  203. package/test/resources/9999999/transactionalPush/build-expected.json +2 -2
  204. package/test/resources/9999999/transactionalPush/template-expected.json +2 -2
  205. package/test/resources/9999999/transactionalSMS/build-expected.json +3 -3
  206. package/test/resources/9999999/transactionalSMS/template-expected.json +3 -3
  207. package/test/{dataExtension.test.js → type.dataExtension.test.js} +78 -21
  208. package/test/{interaction.test.js → type.journey.test.js} +64 -30
  209. package/test/type.mobileKeyword.test.js +250 -0
  210. package/test/type.mobileMessage.test.js +205 -0
  211. package/test/{query.test.js → type.query.test.js} +102 -5
  212. package/test/{transactionalEmail.test.js → type.transactionalEmail.test.js} +40 -2
  213. package/test/{transactionalPush.test.js → type.transactionalPush.test.js} +41 -2
  214. package/test/{transactionalSMS.test.js → type.transactionalSMS.test.js} +73 -3
  215. package/test/type.user.test.js +160 -0
  216. package/test/utils.js +17 -5
  217. package/types/mcdev.d.js +48 -15
  218. package/.github/workflows/code-analysis.yml +0 -57
  219. package/lib/metadataTypes/AccountUser.js +0 -426
  220. package/lib/metadataTypes/definitions/AccountUser.definition.js +0 -227
  221. package/test/mockRoot/deploy/testInstance/testBU/interaction/testExisting_interaction.interaction-meta.json +0 -266
  222. package/test/resources/9999999/interaction/build-expected.json +0 -260
  223. package/test/resources/9999999/interaction/get-expected.json +0 -264
  224. package/test/resources/9999999/interaction/put-expected.json +0 -264
  225. package/test/resources/9999999/interaction/template-expected.json +0 -260
  226. package/test/resources/9999999/interaction/v1/interactions/put-response.json +0 -280
  227. /package/test/mockRoot/deploy/testInstance/testBU/{interaction → journey}/testNew_interaction.interaction-meta.json +0 -0
  228. /package/test/resources/9999999/{interaction → journey}/post-expected.json +0 -0
@@ -9,6 +9,7 @@ const Util = require('../util/util');
9
9
  const File = require('../util/file');
10
10
  const auth = require('../util/auth');
11
11
  const cache = require('../util/cache');
12
+ const pLimit = require('p-limit');
12
13
 
13
14
  /**
14
15
  * DataExtension MetadataType
@@ -20,16 +21,13 @@ class DataExtension extends MetadataType {
20
21
  * Upserts dataExtensions after retrieving them from source and target to compare
21
22
  * if create or update operation is needed.
22
23
  *
23
- * @param {TYPE.DataExtensionMap} desToDeploy dataExtensions mapped by their customerKey
24
+ * @param {TYPE.DataExtensionMap} metadataMap dataExtensions mapped by their customerKey
24
25
  * @returns {Promise} Promise
25
26
  */
26
- static async upsert(desToDeploy) {
27
- // get dataExtensions from target BU for add/update decision
28
- /** @type {TYPE.DataExtensionMap} */
29
- const targetMetadata = cache.getCache().dataExtension || {};
27
+ static async upsert(metadataMap) {
30
28
  // get existing DE-fields for DE-keys in deployment package to properly handle add/update/delete of fields
31
29
  const fieldOptions = {};
32
- for (const key of Object.keys(desToDeploy)) {
30
+ for (const key of Object.keys(metadataMap)) {
33
31
  fieldOptions.filter = fieldOptions.filter
34
32
  ? {
35
33
  leftOperand: {
@@ -47,80 +45,71 @@ class DataExtension extends MetadataType {
47
45
  };
48
46
  }
49
47
  Util.logger.info(` - Caching dependent Metadata: dataExtensionField`);
50
- await this._attachFields(desToDeploy, fieldOptions);
48
+ await this._attachFields(metadataMap, fieldOptions);
51
49
 
52
- /** @type {Promise[]} */
53
- const deCreatePromises = [];
54
- /** @type {Promise[]} */
55
- const deUpdatePromises = [];
50
+ /** @type {object[]} */
51
+ const metadataToCreate = [];
52
+ /** @type {object[]} */
53
+ const metadataToUpdate = [];
56
54
  let filteredByPreDeploy = 0;
57
- for (const dataExtension in desToDeploy) {
58
- if (desToDeploy[dataExtension].Name.startsWith('_')) {
59
- Util.logger.warn(
60
- ` ☇ skipping dataExtension ${desToDeploy[dataExtension].Name}: Cannot Upsert Strongly Typed Data Extensions`
61
- );
62
- filteredByPreDeploy++;
63
- continue;
64
- }
65
- if (
66
- this.buObject.eid !== this.buObject.mid &&
67
- desToDeploy[dataExtension].r__folder_Path.startsWith('Shared Items')
68
- ) {
69
- // this needs to be run before executing preDeployTasks()
70
- Util.logger.warn(
71
- ` ☇ skipping dataExtension ${desToDeploy[dataExtension].Name}: Cannot Create/Update a Shared Data Extension from the Child BU`
72
- );
73
- filteredByPreDeploy++;
74
- continue;
75
- }
76
- if (
77
- desToDeploy[dataExtension].r__folder_Path.startsWith('Synchronized Data Extensions')
78
- ) {
79
- // this needs to be run before executing preDeployTasks()
80
- Util.logger.warn(
81
- ` ☇ skipping dataExtension ${desToDeploy[dataExtension].Name}:Cannot Create/Update a Synchronized Data Extension. Please use Contact Builder to maintain these`
82
- );
83
- filteredByPreDeploy++;
84
- continue;
85
- }
55
+ for (const metadataKey in metadataMap) {
86
56
  try {
87
- desToDeploy[dataExtension] = await this.preDeployTasks(desToDeploy[dataExtension]);
57
+ metadataMap[metadataKey] = await this.preDeployTasks(metadataMap[metadataKey]);
88
58
  } catch (ex) {
89
- // problem with retrieving folder for this DE found
90
59
  // output error & remove from deploy list
91
60
  Util.logger.error(
92
- `- dataExtension ${desToDeploy[dataExtension].Name}: ${ex.message}`
61
+ ` skipping ${this.definition.type} ${
62
+ metadataMap[this.definition.keyField]
63
+ } / ${metadataMap[this.definition.nameField]}: ${ex.message}`
93
64
  );
94
- delete desToDeploy[dataExtension];
65
+ delete metadataMap[metadataKey];
95
66
  // skip rest of handling for this DE
67
+ filteredByPreDeploy++;
96
68
  continue;
97
69
  }
98
- if (targetMetadata[dataExtension]) {
99
- // data extension already exists in target and needs to be updated
100
- deUpdatePromises.push(DataExtension.update(desToDeploy[dataExtension]));
101
- } else {
102
- // data extension does not exist in target and has to be created
103
- deCreatePromises.push(DataExtension.create(desToDeploy[dataExtension]));
104
- }
70
+ await this.createOrUpdate(
71
+ metadataMap,
72
+ metadataKey,
73
+ false,
74
+ metadataToUpdate,
75
+ metadataToCreate
76
+ );
105
77
  }
106
- if (deUpdatePromises.length) {
78
+ if (metadataToUpdate.length) {
107
79
  Util.logger.info(
108
80
  ' - Please note that Data Retention Policies can only be set during creation, not during update.'
109
81
  );
110
82
  }
83
+ const createLimit = pLimit(10);
84
+ const createResults = (
85
+ await Promise.allSettled(
86
+ metadataToCreate
87
+ .filter((r) => r !== undefined && r !== null)
88
+ .map((metadataEntry) => createLimit(() => this.create(metadataEntry)))
89
+ )
90
+ )
91
+ .filter((r) => r !== undefined && r !== null)
92
+ .filter(this.#filterUpsertResults);
111
93
 
112
- const createResults = (await Promise.allSettled(deCreatePromises)).filter(
113
- this._filterUpsertResults
114
- );
115
- const updateResults = (await Promise.allSettled(deUpdatePromises)).filter(
116
- this._filterUpsertResults
117
- );
94
+ const updateLimit = pLimit(10);
95
+ const updateResults = (
96
+ await Promise.allSettled(
97
+ metadataToUpdate
98
+ .filter((r) => r !== undefined && r !== null)
99
+ .map((metadataEntry) =>
100
+ updateLimit(() => this.update(metadataEntry.after, metadataEntry.before))
101
+ )
102
+ )
103
+ )
104
+ .filter((r) => r !== undefined && r !== null)
105
+ .filter(this.#filterUpsertResults);
118
106
 
119
107
  const successfulResults = [...createResults, ...updateResults];
120
108
  Util.logger.info(
121
- `${this.definition.type} upsert: ${createResults.length} of ${deCreatePromises.length} created / ${updateResults.length} of ${deUpdatePromises.length} updated` +
109
+ `${this.definition.type} upsert: ${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated` +
122
110
  (filteredByPreDeploy > 0 ? ` / ${filteredByPreDeploy} filtered` : '')
123
111
  );
112
+ let upsertResults;
124
113
  if (successfulResults.length > 0) {
125
114
  const metadataResults = successfulResults
126
115
  .map((r) => r.value.Results[0].Object)
@@ -133,19 +122,94 @@ class DataExtension extends MetadataType {
133
122
  }
134
123
  return r;
135
124
  });
136
- return super.parseResponseBody({ Results: metadataResults });
125
+ upsertResults = super.parseResponseBody({ Results: metadataResults });
137
126
  } else {
138
- return {};
127
+ upsertResults = {};
139
128
  }
129
+ await this.postDeployTasks(upsertResults, metadataMap, {
130
+ created: createResults.length,
131
+ updated: updateResults.length,
132
+ });
133
+ return upsertResults;
140
134
  }
141
135
 
136
+ /**
137
+ * helper for {@link MetadataType.upsert}
138
+ *
139
+ * @param {TYPE.MetadataTypeMap} metadataMap list of metadata
140
+ * @param {string} metadataKey key of item we are looking at
141
+ * @param {boolean} hasError error flag from previous code
142
+ * @param {TYPE.MetadataTypeItemDiff[]} metadataToUpdate list of items to update
143
+ * @param {TYPE.MetadataTypeItem[]} metadataToCreate list of items to create
144
+ * @returns {'create' | 'update' | 'skip'} action to take
145
+ */
146
+ static async createOrUpdate(
147
+ metadataMap,
148
+ metadataKey,
149
+ hasError,
150
+ metadataToUpdate,
151
+ metadataToCreate
152
+ ) {
153
+ const action = await super.createOrUpdate(
154
+ metadataMap,
155
+ metadataKey,
156
+ hasError,
157
+ metadataToUpdate,
158
+ metadataToCreate
159
+ );
160
+
161
+ if (action === 'update') {
162
+ // Update dataExtension + Columns if they already exist; Create them if not
163
+ // Modify columns for update call
164
+ DataExtensionField.client = this.client;
165
+ DataExtensionField.properties = this.properties;
166
+ DataExtension.oldFields ||= {};
167
+ DataExtension.oldFields[metadataMap[metadataKey][this.definition.keyField]] =
168
+ await DataExtensionField.prepareDeployColumnsOnUpdate(
169
+ metadataMap[metadataKey].Fields,
170
+ metadataKey
171
+ );
172
+
173
+ if (
174
+ metadataMap[metadataKey][this.definition.keyField] !== metadataKey &&
175
+ metadataMap[metadataKey].Fields.length
176
+ ) {
177
+ // changeKeyValue / changeKeyField used
178
+ Util.logger.warn(
179
+ ` - ${this.definition.type} ${metadataKey}: Cannot change fields while updating the key. Skipping field update in favor of key update.`
180
+ );
181
+ metadataMap[metadataKey].Fields.length = 0;
182
+ }
183
+
184
+ // convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
185
+ // <Fields>
186
+ // <Field>
187
+ // <CustomerKey>SubscriberKey</CustomerKey>
188
+ // ..
189
+ // </Field>
190
+ // </Fields>
191
+ metadataMap[metadataKey].Fields = { Field: metadataMap[metadataKey].Fields };
192
+ } else if (action === 'create') {
193
+ this.#cleanupRetentionPolicyFields(metadataMap[metadataKey]);
194
+
195
+ // convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
196
+ // <Fields>
197
+ // <Field>
198
+ // <CustomerKey>SubscriberKey</CustomerKey>
199
+ // ..
200
+ // </Field>
201
+ // </Fields>
202
+ metadataMap[metadataKey].Fields = { Field: metadataMap[metadataKey].Fields };
203
+ }
204
+ }
142
205
  /**
143
206
  * helper for {@link upsert}
144
207
  *
208
+ * @private
145
209
  * @param {object} res -
146
210
  * @returns {boolean} true: keep, false: discard
147
211
  */
148
- static _filterUpsertResults(res) {
212
+ static #filterUpsertResults(res) {
149
213
  if (res.status === 'rejected') {
150
214
  // promise rejects, whole request failed
151
215
  Util.logger.error('- error upserting dataExtension: ' + res.reason);
@@ -178,17 +242,6 @@ class DataExtension extends MetadataType {
178
242
  * @returns {Promise} Promise
179
243
  */
180
244
  static async create(metadata) {
181
- this._cleanupRetentionPolicyFields(metadata);
182
-
183
- // convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
184
- // <Fields>
185
- // <Field>
186
- // <CustomerKey>SubscriberKey</CustomerKey>
187
- // ..
188
- // </Field>
189
- // </Fields>
190
- metadata.Fields = { Field: metadata.Fields };
191
-
192
245
  return super.createSOAP(metadata);
193
246
  }
194
247
 
@@ -201,7 +254,7 @@ class DataExtension extends MetadataType {
201
254
  * @param {TYPE.DataExtensionItem} metadata single metadata entry
202
255
  * @returns {void}
203
256
  */
204
- static _cleanupRetentionPolicyFields(metadata) {
257
+ static #cleanupRetentionPolicyFields(metadata) {
205
258
  if (
206
259
  metadata.DataRetentionPeriodLength &&
207
260
  metadata.DataRetentionPeriodUnitOfMeasure &&
@@ -220,26 +273,6 @@ class DataExtension extends MetadataType {
220
273
  * @returns {Promise} Promise
221
274
  */
222
275
  static async update(metadata) {
223
- // Update dataExtension + Columns if they already exist; Create them if not
224
- // Modify columns for update call
225
- DataExtensionField.client = this.client;
226
- DataExtensionField.properties = this.properties;
227
- DataExtension.oldFields = DataExtension.oldFields || {};
228
- DataExtension.oldFields[metadata[this.definition.keyField]] =
229
- await DataExtensionField.prepareDeployColumnsOnUpdate(
230
- metadata.Fields,
231
- metadata.CustomerKey
232
- );
233
-
234
- // convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
235
- // <Fields>
236
- // <Field>
237
- // <CustomerKey>SubscriberKey</CustomerKey>
238
- // ..
239
- // </Field>
240
- // </Fields>
241
-
242
- metadata.Fields = { Field: metadata.Fields };
243
276
  return super.updateSOAP(metadata);
244
277
  }
245
278
  /**
@@ -247,12 +280,19 @@ class DataExtension extends MetadataType {
247
280
  *
248
281
  * @param {TYPE.DataExtensionMap} upsertedMetadata metadata mapped by their keyField
249
282
  * @param {TYPE.DataExtensionMap} originalMetadata metadata to be updated (contains additioanl fields)
283
+ * @param {{created: number, updated: number}} createdUpdated counter representing successful creates/updates
250
284
  * @returns {void}
251
285
  */
252
- static postDeployTasks(upsertedMetadata, originalMetadata) {
286
+ static postDeployTasks(upsertedMetadata, originalMetadata, createdUpdated) {
253
287
  for (const key in upsertedMetadata) {
254
288
  const item = upsertedMetadata[key];
255
- const cachedVersion = cache.getByKey('dataExtension', item.CustomerKey);
289
+
290
+ const oldKey = Util.changedKeysMap?.[this.definition.type]?.[key] || key;
291
+ delete Util.changedKeysMap?.[this.definition.type]?.[key];
292
+
293
+ const cachedVersion = createdUpdated.updated
294
+ ? cache.getByKey(this.definition.type, oldKey)
295
+ : null;
256
296
  if (cachedVersion) {
257
297
  // UPDATE
258
298
  // restore retention values that are typically not returned by the update call
@@ -262,25 +302,25 @@ class DataExtension extends MetadataType {
262
302
  item.RetainUntil = cachedVersion.RetainUntil;
263
303
 
264
304
  const existingFields = DataExtension.oldFields[item[this.definition.keyField]];
305
+
265
306
  if (item.Fields === '') {
266
307
  // if no fields were updated, we need to set Fields to "empty string" for the API to work
267
308
  // reset here to get the correct field list
268
309
  item.Fields = Object.keys(existingFields)
269
- .map((key) => existingFields[key])
310
+ .map((el) => existingFields[el])
270
311
  .sort((a, b) => a.Ordinal - b.Ordinal);
271
- }
272
- if (existingFields) {
312
+ } else if (existingFields) {
273
313
  // get list of updated fields
274
314
  /** @type {TYPE.DataExtensionFieldItem[]} */
275
- const updatedFieldsArr = originalMetadata[key].Fields.Field.filter(
315
+ const updatedFieldsArr = originalMetadata[oldKey].Fields.Field.filter(
276
316
  (field) => field.ObjectID && field.ObjectID !== ''
277
317
  );
278
318
  // convert existing fields obj into array and sort
279
319
  /** @type {TYPE.DataExtensionFieldItem[]} */
280
320
  const finalFieldsArr = Object.keys(existingFields)
281
- .map((key) => {
321
+ .map((el) => {
282
322
  /** @type {TYPE.DataExtensionFieldItem} */
283
- const existingField = existingFields[key];
323
+ const existingField = existingFields[el];
284
324
  // check if the current field was updated and then override with it. otherwise use existing value
285
325
  const field =
286
326
  updatedFieldsArr.find(
@@ -296,7 +336,7 @@ class DataExtension extends MetadataType {
296
336
 
297
337
  // get list of new fields
298
338
  /** @type {TYPE.DataExtensionFieldItem[]} */
299
- const newFieldsArr = originalMetadata[key].Fields.Field.filter(
339
+ const newFieldsArr = originalMetadata[oldKey].Fields.Field.filter(
300
340
  (field) => !field.ObjectID
301
341
  );
302
342
  // push new fields to end of list
@@ -487,7 +527,36 @@ class DataExtension extends MetadataType {
487
527
  if (metadata.SendableSubscriberField?.Name === '_SubscriberKey') {
488
528
  metadata.SendableSubscriberField.Name = 'Subscriber Key';
489
529
  }
490
- return this._parseMetadata(JSON.parse(JSON.stringify(metadata)));
530
+ this.setFolderPath(metadata);
531
+
532
+ // DataExtensionTemplate
533
+ if (metadata.Template?.CustomerKey) {
534
+ try {
535
+ metadata.r__dataExtensionTemplate_Name = cache.searchForField(
536
+ 'dataExtensionTemplate',
537
+ metadata.Template.CustomerKey,
538
+ 'CustomerKey',
539
+ 'Name'
540
+ );
541
+ delete metadata.Template;
542
+ } catch (ex) {
543
+ Util.logger.debug(ex.message);
544
+ // Let's allow retrieving such DEs but warn that they cannot be deployed to another BU.
545
+ // Deploying to same BU still works!
546
+ // A workaround exists but it's likely not beneficial to explain to users:
547
+ // 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
548
+ Util.logger.warn(
549
+ ` - Issue with dataExtension '${
550
+ metadata[this.definition.nameField]
551
+ }': Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`
552
+ );
553
+ }
554
+ }
555
+ // remove the date fields manually here because we need them in the changelog but not in the saved json
556
+ delete metadata.CreatedDate;
557
+ delete metadata.ModifiedDate;
558
+
559
+ return metadata;
491
560
  }
492
561
 
493
562
  /**
@@ -539,6 +608,16 @@ class DataExtension extends MetadataType {
539
608
 
540
609
  metadata[customerKey].Fields = fieldArr;
541
610
  }
611
+ /**
612
+ * helper for {@link super.updateREST} and {@link super.updateSOAP} that removes old files after the key was changed
613
+ *
614
+ * @private
615
+ * @param {TYPE.MetadataTypeItem} metadataEntry a single metadata Entry
616
+ * @returns {void}
617
+ */
618
+ static async _postChangeKeyTasks(metadataEntry) {
619
+ super._postChangeKeyTasks(metadataEntry, true);
620
+ }
542
621
 
543
622
  /**
544
623
  * prepares a DataExtension for deployment
@@ -547,6 +626,21 @@ class DataExtension extends MetadataType {
547
626
  * @returns {Promise.<TYPE.DataExtensionItem>} Promise of updated single DE
548
627
  */
549
628
  static async preDeployTasks(metadata) {
629
+ if (metadata.Name.startsWith('_')) {
630
+ throw new Error(`Cannot Upsert Strongly Typed Data Extensions`);
631
+ }
632
+ if (
633
+ this.buObject.eid !== this.buObject.mid &&
634
+ metadata.r__folder_Path.startsWith('Shared Items')
635
+ ) {
636
+ throw new Error(`Cannot Create/Update a Shared Data Extension from the Child BU`);
637
+ }
638
+ if (metadata.r__folder_Path.startsWith('Synchronized Data Extensions')) {
639
+ throw new Error(
640
+ `Cannot Create/Update a Synchronized Data Extension. Please use Contact Builder to maintain these`
641
+ );
642
+ }
643
+
550
644
  // folder
551
645
  super.setFolderId(metadata);
552
646
 
@@ -584,9 +678,7 @@ class DataExtension extends MetadataType {
584
678
  // A workaround for cross-BU deploy exists but it's likely not beneficial to explain to users:
585
679
  // 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
586
680
  throw new Error(
587
- `Skipping DataExtension '${
588
- metadata[this.definition.nameField]
589
- }': Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`
681
+ `Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`
590
682
  );
591
683
  }
592
684
  }
@@ -755,13 +847,13 @@ class DataExtension extends MetadataType {
755
847
  /**
756
848
  * Parses metadata into a readable Markdown/HTML format then saves it
757
849
  *
758
- * @param {TYPE.DataExtensionMap} [metadata] a list of dataExtension definitions
850
+ * @param {TYPE.DataExtensionMap} [metadataMap] a list of dataExtension definitions
759
851
  * @returns {Promise.<void>} -
760
852
  */
761
- static async document(metadata) {
853
+ static async document(metadataMap) {
762
854
  try {
763
- if (!metadata) {
764
- metadata = this.readBUMetadataForType(
855
+ if (!metadataMap) {
856
+ metadataMap = this.readBUMetadataForType(
765
857
  File.normalizePath([
766
858
  this.properties.directories.retrieve,
767
859
  this.buObject.credential,
@@ -780,7 +872,7 @@ class DataExtension extends MetadataType {
780
872
  this.buObject.businessUnit,
781
873
  this.definition.type,
782
874
  ]);
783
- if (!metadata || !Object.keys(metadata).length) {
875
+ if (!metadataMap || !Object.keys(metadataMap).length) {
784
876
  // as part of retrieve & manual execution we could face an empty folder
785
877
  return;
786
878
  }
@@ -794,16 +886,15 @@ class DataExtension extends MetadataType {
794
886
  'DefaultValue',
795
887
  ];
796
888
  return Promise.all(
797
- Object.keys(metadata).map((customerKey) => {
798
- // for (const customerKey in metadata) {
799
- if (metadata[customerKey]?.Fields?.length) {
800
- for (const field of metadata[customerKey].Fields) {
889
+ Object.keys(metadataMap).map((key) => {
890
+ if (metadataMap[key]?.Fields?.length) {
891
+ for (const field of metadataMap[key].Fields) {
801
892
  field.IsNullable = !Util.isTrue(field.IsRequired);
802
- for (const key of columnsToIterateThrough) {
803
- if (Util.isTrue(field[key])) {
804
- field[key] = '+';
805
- } else if (Util.isFalse(field[key])) {
806
- field[key] = '-';
893
+ for (const prop of columnsToIterateThrough) {
894
+ if (Util.isTrue(field[prop])) {
895
+ field[prop] = '+';
896
+ } else if (Util.isFalse(field[prop])) {
897
+ field[prop] = '-';
807
898
  }
808
899
  }
809
900
  }
@@ -811,8 +902,8 @@ class DataExtension extends MetadataType {
811
902
  if (['html', 'both'].includes(this.properties.options.documentType)) {
812
903
  return this._writeDoc(
813
904
  docPath + '/',
814
- customerKey,
815
- metadata[customerKey],
905
+ key,
906
+ metadataMap[key],
816
907
  'html',
817
908
  columnsToPrint
818
909
  );
@@ -820,8 +911,8 @@ class DataExtension extends MetadataType {
820
911
  if (['md', 'both'].includes(this.properties.options.documentType)) {
821
912
  return this._writeDoc(
822
913
  docPath + '/',
823
- customerKey,
824
- metadata[customerKey],
914
+ key,
915
+ metadataMap[key],
825
916
  'md',
826
917
  columnsToPrint
827
918
  );
@@ -838,34 +929,19 @@ class DataExtension extends MetadataType {
838
929
  * @returns {Promise.<boolean>} deletion success status
839
930
  */
840
931
  static deleteByKey(customerKey) {
841
- return super.deleteByKeySOAP(customerKey, false);
932
+ return super.deleteByKeySOAP(customerKey);
842
933
  }
843
934
 
844
935
  /**
845
936
  * clean up after deleting a metadata item
846
937
  *
847
938
  * @param {string} customerKey Identifier of metadata item
848
- * @returns {void}
939
+ * @returns {Promise.<void>} - promise
849
940
  */
850
941
  static async postDeleteTasks(customerKey) {
851
- // delete local copy: retrieve/cred/bu/dataExtension/...json
852
- const jsonFile = File.normalizePath([
853
- this.properties.directories.retrieve,
854
- this.buObject.credential,
855
- this.buObject.businessUnit,
856
- this.definition.type,
857
- `${customerKey}.${this.definition.type}-meta.json`,
858
- ]);
859
- await File.remove(jsonFile);
942
+ // delete local copy: retrieve/cred/bu/dataExtension/...-meta.json
860
943
  // delete local copy: doc/dataExtension/cred/bu/...md
861
- const mdFile = File.normalizePath([
862
- this.properties.directories.docs,
863
- 'dataExtension',
864
- this.buObject.credential,
865
- this.buObject.businessUnit,
866
- `${customerKey}.${this.definition.type}.md`,
867
- ]);
868
- await File.remove(mdFile);
944
+ await super.postDeleteTasks(customerKey, [`${this.definition.type}-doc.md`]);
869
945
  }
870
946
 
871
947
  /**
@@ -988,45 +1064,6 @@ class DataExtension extends MetadataType {
988
1064
  }
989
1065
  }
990
1066
  }
991
- /**
992
- * parses retrieved Metadata before saving
993
- *
994
- * @private
995
- * @param {TYPE.DataExtensionItem} metadata a single dataExtension definition
996
- * @returns {TYPE.DataExtensionItem} a single dataExtension definition
997
- */
998
- static _parseMetadata(metadata) {
999
- this.setFolderPath(metadata);
1000
-
1001
- // DataExtensionTemplate
1002
- if (metadata.Template?.CustomerKey) {
1003
- try {
1004
- metadata.r__dataExtensionTemplate_Name = cache.searchForField(
1005
- 'dataExtensionTemplate',
1006
- metadata.Template.CustomerKey,
1007
- 'CustomerKey',
1008
- 'Name'
1009
- );
1010
- delete metadata.Template;
1011
- } catch (ex) {
1012
- Util.logger.debug(ex.message);
1013
- // Let's allow retrieving such DEs but warn that they cannot be deployed to another BU.
1014
- // Deploying to same BU still works!
1015
- // A workaround exists but it's likely not beneficial to explain to users:
1016
- // 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
1017
- Util.logger.warn(
1018
- ` - Issue with dataExtension '${
1019
- metadata[this.definition.nameField]
1020
- }': Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`
1021
- );
1022
- }
1023
- }
1024
- // remove the date fields manually here because we need them in the changelog but not in the saved json
1025
- delete metadata.CreatedDate;
1026
- delete metadata.ModifiedDate;
1027
-
1028
- return metadata;
1029
- }
1030
1067
 
1031
1068
  /**
1032
1069
  * Retrieves dataExtension metadata and cleans it
@@ -1037,7 +1074,7 @@ class DataExtension extends MetadataType {
1037
1074
  * @returns {Promise.<TYPE.DataExtensionMap>} keyField => metadata map
1038
1075
  */
1039
1076
  static async _retrieveAll(additionalFields, options) {
1040
- const { metadata } = await super.retrieveSOAP(null, options, additionalFields);
1077
+ const { metadata } = await super.retrieveSOAP(null, options, null, additionalFields);
1041
1078
  for (const key in metadata) {
1042
1079
  // some system data extensions do not have CategoryID which throws errors in other places. These do not need to be parsed
1043
1080
  if (metadata[key].CategoryID) {