mcdev 5.0.1 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.coverage-comment-template.svelte +177 -161
  2. package/.eslintrc.json +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  4. package/.github/dependabot.yml +8 -0
  5. package/.github/workflows/coverage-base-update.yml +6 -2
  6. package/.github/workflows/coverage-develop-branch.yml +7 -6
  7. package/.github/workflows/coverage-main-branch.yml +7 -6
  8. package/.github/workflows/coverage.yml +7 -2
  9. package/.husky/commit-msg +1 -1
  10. package/.husky/post-checkout +35 -3
  11. package/.husky/post-merge +21 -0
  12. package/.husky/pre-commit +1 -0
  13. package/docs/dist/documentation.md +222 -62
  14. package/lib/Deployer.js +3 -4
  15. package/lib/cli.js +36 -8
  16. package/lib/index.js +175 -3
  17. package/lib/metadataTypes/Asset.js +4 -2
  18. package/lib/metadataTypes/Automation.js +413 -195
  19. package/lib/metadataTypes/DataExtension.js +6 -8
  20. package/lib/metadataTypes/DataExtensionField.js +5 -5
  21. package/lib/metadataTypes/Journey.js +11 -10
  22. package/lib/metadataTypes/MetadataType.js +76 -22
  23. package/lib/metadataTypes/MobileKeyword.js +165 -20
  24. package/lib/metadataTypes/MobileMessage.js +20 -28
  25. package/lib/metadataTypes/Query.js +26 -0
  26. package/lib/metadataTypes/Role.js +2 -3
  27. package/lib/metadataTypes/TransactionalSMS.js +5 -5
  28. package/lib/metadataTypes/User.js +3 -17
  29. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  30. package/lib/metadataTypes/definitions/Automation.definition.js +52 -6
  31. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -0
  32. package/lib/metadataTypes/definitions/DataExtract.definition.js +1 -0
  33. package/lib/metadataTypes/definitions/EmailSend.definition.js +1 -0
  34. package/lib/metadataTypes/definitions/Event.definition.js +1 -0
  35. package/lib/metadataTypes/definitions/Filter.definition.js +1 -0
  36. package/lib/metadataTypes/definitions/ImportFile.definition.js +1 -0
  37. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +20 -7
  38. package/lib/metadataTypes/definitions/MobileMessage.definition.js +50 -8
  39. package/lib/metadataTypes/definitions/Query.definition.js +1 -0
  40. package/lib/metadataTypes/definitions/Role.definition.js +1 -0
  41. package/lib/metadataTypes/definitions/TriggeredSend.definition.js +1 -0
  42. package/lib/metadataTypes/definitions/User.definition.js +2 -0
  43. package/lib/util/auth.js +4 -1
  44. package/lib/util/cli.js +1 -1
  45. package/lib/util/devops.js +13 -11
  46. package/lib/util/file.js +5 -3
  47. package/lib/util/util.js +153 -129
  48. package/package.json +11 -11
  49. package/test/general.test.js +26 -0
  50. package/test/mockRoot/.mcdevrc.json +3 -1
  51. package/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json +53 -0
  52. package/test/mockRoot/deploy/testInstance/testBU/automation/testNew_automation.automation-meta.json +46 -0
  53. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword.mobileKeyword-meta.json → 4912312345678.TESTNEW_KEYWORD.mobileKeyword-meta.json} +1 -4
  54. package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword_blocked.mobileKeyword-meta.json → 4912312345678.TESTNEW_KEYWORD_BLOCKED.mobileKeyword-meta.json} +1 -4
  55. package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.json → testExisting_query.query-meta.json} +2 -2
  56. package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.json → testNew_query.query-meta.json} +2 -2
  57. package/test/mockRoot/deploy/testInstance/testBU/transactionalSMS/testExisting_tsms.transactionalSMS-meta.json +1 -1
  58. package/test/mockRoot/deploy/testInstance/testBU/transactionalSMS/testNew_tsms.transactionalSMS-meta.json +1 -1
  59. package/test/resourceFactory.js +64 -21
  60. package/test/resources/1111111/user/retrieve-expected.md +19 -0
  61. package/test/resources/9999999/automation/build-expected.json +58 -0
  62. package/test/resources/9999999/automation/create-expected.json +46 -0
  63. package/test/resources/9999999/automation/create-testNew_automation-expected.md +28 -0
  64. package/test/resources/9999999/automation/delete-response.xml +40 -0
  65. package/test/resources/9999999/automation/retrieve-expected.json +58 -0
  66. package/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md +30 -0
  67. package/test/resources/9999999/automation/template-expected.json +58 -0
  68. package/test/resources/9999999/automation/update-expected.json +46 -0
  69. package/test/resources/9999999/automation/update-testExisting_automation-expected.md +28 -0
  70. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  71. package/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json +85 -0
  72. package/test/resources/9999999/automation/v1/automations/a8afb0e2-b00a-4c88-ad2e-1f7f8788c560/get-response.json +85 -0
  73. package/test/resources/9999999/automation/v1/automations/post-response.json +85 -0
  74. package/test/resources/9999999/automation/v1/dataextracts/56c5370a-f988-4f36-b0ee-0f876573f6d7/get-response.json +38 -0
  75. package/test/resources/9999999/automation/v1/dataextracts/get-response.json +20 -0
  76. package/test/resources/9999999/automation/v1/filetransfers/72c328ac-f5b0-4e37-91d3-a775666f15a6/get-response.json +18 -0
  77. package/test/resources/9999999/automation/v1/filetransfers/get-response.json +15 -0
  78. package/test/resources/9999999/automation/v1/imports/get-response.json +38 -0
  79. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/actions/start/post-response.txt +1 -0
  80. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/get-response.json +2 -2
  81. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +2 -2
  82. package/test/resources/9999999/automation/v1/queries/get-response.json +4 -4
  83. package/test/resources/9999999/automation/v1/queries/post-response.json +2 -2
  84. package/test/resources/9999999/automation/v1/scripts/get-response.json +17 -0
  85. package/test/resources/9999999/dataExtension/retrieve-expected.md +18 -0
  86. package/test/resources/9999999/dataFolder/retrieve-ContentType=automations-response.xml +48 -0
  87. package/test/resources/9999999/dataFolder/retrieve-ContentType=queryactivity-response.xml +48 -0
  88. package/test/resources/9999999/dataFolder/retrieve-response.xml +22 -0
  89. package/test/resources/9999999/emailSendDefinition/retrieve-response.xml +85 -0
  90. package/test/resources/9999999/journey/build-expected.json +1 -1
  91. package/test/resources/9999999/journey/get-expected.json +1 -1
  92. package/test/resources/9999999/journey/put-expected.json +1 -1
  93. package/test/resources/9999999/journey/template-expected.json +1 -1
  94. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json +21 -0
  95. package/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/post-response.json +0 -0
  96. package/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json +30 -0
  97. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/NXV4ZFMwTEFwRVczd3RaLUF5X3p5dzo4Njow/get-response.json +1 -1
  98. package/test/resources/9999999/legacy/v1/beta/mobile/keyword/get-response.json +1 -1
  99. package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/get-response.json +1 -1
  100. package/test/resources/9999999/legacy/v1/beta/mobile/message/get-response.json +1 -1
  101. package/test/resources/9999999/messaging/v1/sms/definitions/post-response.json +1 -1
  102. package/test/resources/9999999/messaging/v1/sms/definitions/testExisting_tsms/get-response.json +1 -1
  103. package/test/resources/9999999/messaging/v1/sms/definitions/testExisting_tsms/patch-response.json +1 -1
  104. package/test/resources/9999999/mobileKeyword/build-expected.json +1 -4
  105. package/test/resources/9999999/mobileKeyword/get-expected.json +1 -4
  106. package/test/resources/9999999/mobileKeyword/post-create-expected.json +1 -4
  107. package/test/resources/9999999/mobileKeyword/template-expected.json +1 -4
  108. package/test/resources/9999999/program/retrieve-CustomerKey=testExisting_automation-response.xml +30 -0
  109. package/test/resources/9999999/program/retrieve-CustomerKey=testNew_automation-response.xml +30 -0
  110. package/test/resources/9999999/program/retrieve-Name=testExisting_automation-response.xml +31 -0
  111. package/test/resources/9999999/program/retrieve-response.xml +32 -0
  112. package/test/resources/9999999/query/build-expected.json +2 -2
  113. package/test/resources/9999999/query/build-expected.sql +1 -1
  114. package/test/resources/9999999/query/get-expected.json +2 -2
  115. package/test/resources/9999999/query/get-expected.sql +1 -1
  116. package/test/resources/9999999/query/get2-expected.json +2 -2
  117. package/test/resources/9999999/query/patch-expected.json +2 -2
  118. package/test/resources/9999999/query/patch-expected.sql +1 -1
  119. package/test/resources/9999999/query/post-expected.json +2 -2
  120. package/test/resources/9999999/query/post-expected.sql +1 -1
  121. package/test/resources/9999999/query/template-expected.json +2 -2
  122. package/test/resources/9999999/query/template-expected.sql +1 -1
  123. package/test/resources/9999999/transactionalSMS/build-expected.json +1 -1
  124. package/test/resources/9999999/transactionalSMS/get-expected.json +1 -1
  125. package/test/resources/9999999/transactionalSMS/patch-expected.json +1 -1
  126. package/test/resources/9999999/transactionalSMS/post-expected.json +1 -1
  127. package/test/resources/9999999/transactionalSMS/template-expected.json +1 -1
  128. package/test/type.automation.test.js +259 -0
  129. package/test/type.dataExtension.test.js +16 -1
  130. package/test/type.mobileKeyword.test.js +57 -19
  131. package/test/type.query.test.js +39 -26
  132. package/test/type.user.test.js +44 -1
  133. package/test/utils.js +16 -5
  134. package/.coverage-comment-template.md +0 -20
  135. /package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword.mobileKeyword-meta.amp → 4912312345678.TESTNEW_KEYWORD.mobileKeyword-meta.amp} +0 -0
  136. /package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/{testNew_keyword_blocked.mobileKeyword-meta.amp → 4912312345678.TESTNEW_KEYWORD_BLOCKED.mobileKeyword-meta.amp} +0 -0
  137. /package/test/mockRoot/deploy/testInstance/testBU/query/{testExistingQuery.query-meta.sql → testExisting_query.query-meta.sql} +0 -0
  138. /package/test/mockRoot/deploy/testInstance/testBU/query/{testNewQuery.query-meta.sql → testNew_query.query-meta.sql} +0 -0
@@ -59,8 +59,8 @@ class DataExtension extends MetadataType {
59
59
  // output error & remove from deploy list
60
60
  Util.logger.error(
61
61
  ` ☇ skipping ${this.definition.type} ${
62
- metadataMap[this.definition.keyField]
63
- } / ${metadataMap[this.definition.nameField]}: ${ex.message}`
62
+ metadataMap[metadataKey][this.definition.keyField]
63
+ } / ${metadataMap[metadataKey][this.definition.nameField]}: ${ex.message}`
64
64
  );
65
65
  delete metadataMap[metadataKey];
66
66
  // skip rest of handling for this DE
@@ -464,9 +464,7 @@ class DataExtension extends MetadataType {
464
464
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
465
465
  Util.getKeysString(key)
466
466
  );
467
- if (this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
468
- await this.document(savedMetadata);
469
- }
467
+ await this.runDocumentOnRetrieve(key, savedMetadata);
470
468
  }
471
469
  return { metadata: metadata, type: 'dataExtension' };
472
470
  }
@@ -626,16 +624,16 @@ class DataExtension extends MetadataType {
626
624
  * @returns {Promise.<TYPE.DataExtensionItem>} Promise of updated single DE
627
625
  */
628
626
  static async preDeployTasks(metadata) {
629
- if (metadata.Name.startsWith('_')) {
627
+ if (metadata.Name?.startsWith('_')) {
630
628
  throw new Error(`Cannot Upsert Strongly Typed Data Extensions`);
631
629
  }
632
630
  if (
633
631
  this.buObject.eid !== this.buObject.mid &&
634
- metadata.r__folder_Path.startsWith('Shared Items')
632
+ metadata.r__folder_Path?.startsWith('Shared Items')
635
633
  ) {
636
634
  throw new Error(`Cannot Create/Update a Shared Data Extension from the Child BU`);
637
635
  }
638
- if (metadata.r__folder_Path.startsWith('Synchronized Data Extensions')) {
636
+ if (metadata.r__folder_Path?.startsWith('Synchronized Data Extensions')) {
639
637
  throw new Error(
640
638
  `Cannot Create/Update a Synchronized Data Extension. Please use Contact Builder to maintain these`
641
639
  );
@@ -170,11 +170,11 @@ class DataExtensionField extends MetadataType {
170
170
 
171
171
  // enable renaming
172
172
  if (item.Name_new) {
173
+ Util.logger.info(
174
+ ` - Found Name_new='${item.Name_new}' for field ${deKey}.${item.Name} - trying to rename.`
175
+ );
173
176
  item.Name = item.Name_new;
174
177
  delete item.Name_new;
175
- Util.logger.warn(
176
- ` - Found 'Name_new' value '${item.Name_new}' for ${deKey}.${item.Name} - trying to rename.`
177
- );
178
178
  }
179
179
 
180
180
  // check if any changes were found
@@ -210,8 +210,8 @@ class DataExtensionField extends MetadataType {
210
210
  }
211
211
  }
212
212
  if (item.Name_new) {
213
- Util.logger.warn(
214
- ` - Found 'Name_new' value '${item.Name_new}' for ${deKey}.${item.Name} but could not find a corresponding DE field on the server - adding new field instead of updating.`
213
+ Util.logger.info(
214
+ ` - Found Name_new='${item.Name_new}' for field ${deKey}.${item.Name} but could not find a corresponding DE field on the server - adding new field instead of updating.`
215
215
  );
216
216
  delete item.Name_new;
217
217
  }
@@ -542,12 +542,13 @@ class Journey extends MetadataType {
542
542
  try {
543
543
  // mobileKeyword
544
544
  if (activity.configurationArguments?.keywordId) {
545
- activity.configurationArguments.c__mobileKeyword = cache.searchForField(
546
- 'mobileKeyword',
547
- activity.configurationArguments.keywordId,
548
- 'id',
549
- 'keyword'
550
- );
545
+ activity.configurationArguments.r__mobileKeyword_codeKeyword =
546
+ cache.searchForField(
547
+ 'mobileKeyword',
548
+ activity.configurationArguments.keywordId,
549
+ 'id',
550
+ 'c__codeKeyword'
551
+ );
551
552
  delete activity.configurationArguments.keywordId;
552
553
  }
553
554
  } catch (ex) {
@@ -833,14 +834,14 @@ class Journey extends MetadataType {
833
834
  delete activity.configurationArguments.c__mobileMessage_id;
834
835
  }
835
836
  // mobileKeyword
836
- if (activity.configurationArguments?.c__mobileKeyword) {
837
+ if (activity.configurationArguments?.r__mobileKeyword_codeKeyword) {
837
838
  activity.configurationArguments.keywordId = cache.searchForField(
838
839
  'mobileKeyword',
839
- activity.c__mobileKeyword,
840
- 'keyword',
840
+ activity.r__mobileKeyword_codeKeyword,
841
+ 'c__codeKeyword',
841
842
  'id'
842
843
  );
843
- delete activity.configurationArguments.c__mobileKeyword;
844
+ delete activity.configurationArguments.r__mobileKeyword_codeKeyword;
844
845
  }
845
846
  if (activity.configurationArguments?.c__next_mobileKeyword) {
846
847
  activity.configurationArguments.nextKeywordId = cache.searchForField(
@@ -58,15 +58,9 @@ class MetadataType {
58
58
  if (key === fileNameWithoutEnding || listBadKeys) {
59
59
  fileName2FileContent[fileNameWithoutEnding] = fileContent;
60
60
  } else {
61
- Util.metadataLogger(
62
- 'error',
63
- this.definition.type,
64
- 'getJsonFromFS',
65
- 'Name of the Metadata and the External Identifier must match',
66
- JSON.stringify({
67
- Filename: fileNameWithoutEnding,
68
- ExternalIdentifier: key,
69
- })
61
+ Util.logger.error(
62
+ ` ${this.definition.type} ${key}: Name of the metadata file and the JSON-key (${this.definition.keyField}) must match` +
63
+ Util.getGrayMsg(` - ${dir}/${fileName}`)
70
64
  );
71
65
  }
72
66
  }
@@ -460,6 +454,18 @@ class MetadataType {
460
454
  return;
461
455
  }
462
456
 
457
+ /**
458
+ * Abstract execute method that needs to be implemented in child metadata type
459
+ *
460
+ * @returns {void}
461
+ */
462
+ static execute() {
463
+ Util.logger.error(
464
+ ` ☇ skipping ${this.definition.type}: execute is not supported yet for ${this.definition.type}`
465
+ );
466
+ return;
467
+ }
468
+
463
469
  /**
464
470
  * test if metadata was actually changed or not to potentially skip it during deployment
465
471
  *
@@ -650,6 +656,7 @@ class MetadataType {
650
656
  metadataMap[metadataKey][this.definition.keyField]
651
657
  );
652
658
  // Update if it already exists; Create it if not
659
+ const maxKeyLength = this.definition.maxKeyLength || 36;
653
660
  if (
654
661
  Util.logger.level === 'debug' &&
655
662
  metadataMap[metadataKey][this.definition.idField] &&
@@ -703,11 +710,11 @@ class MetadataType {
703
710
  // NOTE: trim twice while getting the newKey value to remove leading spaces before limiting the length
704
711
  const newKey = (metadataMap[metadataKey][Util.OPTIONS.changeKeyField] + '')
705
712
  .trim()
706
- .slice(0, 36)
713
+ .slice(0, maxKeyLength)
707
714
  .trim();
708
- if (metadataMap[metadataKey][Util.OPTIONS.changeKeyField] + '' > 36) {
715
+ if (metadataMap[metadataKey][Util.OPTIONS.changeKeyField] + '' > maxKeyLength) {
709
716
  Util.logger.warn(
710
- `Customer Keys may not exceed 36 characters. Truncated the value in field ${Util.OPTIONS.changeKeyField} to ${newKey}`
717
+ `${this.definition.type} ${this.definition.keyField} may not exceed ${maxKeyLength} characters. Truncated the value in field ${Util.OPTIONS.changeKeyField} to ${newKey}`
711
718
  );
712
719
  }
713
720
  if (metadataKey == newKey) {
@@ -727,10 +734,10 @@ class MetadataType {
727
734
  }
728
735
  } else if (Util.OPTIONS.changeKeyValue) {
729
736
  // NOTE: trim twice while getting the newKey value to remove leading spaces before limiting the length
730
- const newKey = Util.OPTIONS.changeKeyValue.trim().slice(0, 36).trim();
731
- if (Util.OPTIONS.changeKeyValue.trim().length > 36) {
737
+ const newKey = Util.OPTIONS.changeKeyValue.trim().slice(0, maxKeyLength).trim();
738
+ if (Util.OPTIONS.changeKeyValue.trim().length > maxKeyLength) {
732
739
  Util.logger.warn(
733
- `Customer Keys may not exceed 36 characters. Truncated your value to ${newKey}`
740
+ `${this.definition.type} ${this.definition.keyField} may not exceed ${maxKeyLength} characters. Truncated your value to ${newKey}`
734
741
  );
735
742
  }
736
743
  if (this.definition.keyField == this.definition.idField) {
@@ -1016,12 +1023,7 @@ class MetadataType {
1016
1023
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
1017
1024
  Util.getKeysString(singleRetrieve)
1018
1025
  );
1019
- if (
1020
- this.buObject &&
1021
- this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)
1022
- ) {
1023
- await this.document(savedMetadata);
1024
- }
1026
+ await this.runDocumentOnRetrieve(singleRetrieve, savedMetadata);
1025
1027
  }
1026
1028
  return { metadata: metadata, type: this.definition.type };
1027
1029
  }
@@ -1063,6 +1065,7 @@ class MetadataType {
1063
1065
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
1064
1066
  Util.getKeysString(singleRetrieve)
1065
1067
  );
1068
+ await this.runDocumentOnRetrieve(singleRetrieve, savedMetadata);
1066
1069
  }
1067
1070
 
1068
1071
  return {
@@ -1070,6 +1073,54 @@ class MetadataType {
1070
1073
  type: this.definition.type,
1071
1074
  };
1072
1075
  }
1076
+ /**
1077
+ * Used to execute a query/automation etc.
1078
+ *
1079
+ * @param {string} uri REST endpoint where the POST request should be sent
1080
+ * @param {string} key item key
1081
+ * @returns {Promise.<string>} 'OK' if started execution successfully, otherwise - 'Error'
1082
+ */
1083
+ static async executeREST(uri, key) {
1084
+ try {
1085
+ const response = await this.client.rest.post(uri, {}); // payload is empty for this request
1086
+ if (response === 'OK') {
1087
+ Util.logger.info(`Executed ${this.definition.type}: ${key}`);
1088
+ } else {
1089
+ throw new Error(response);
1090
+ }
1091
+ return response;
1092
+ } catch (ex) {
1093
+ Util.logger.error(`Failed to execute ${this.definition.type} ${key}: ${ex.message}`);
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * helper for {@link retrieveREST} and {@link retrieveSOAP}
1099
+ *
1100
+ * @param {string|number} [singleRetrieve] key of single item to filter by
1101
+ * @param {TYPE.MetadataTypeMap} metadataMap saved metadata
1102
+ * @returns {Promise.<void>} -
1103
+ */
1104
+ static async runDocumentOnRetrieve(singleRetrieve, metadataMap) {
1105
+ if (
1106
+ this.buObject &&
1107
+ this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)
1108
+ ) {
1109
+ if (!singleRetrieve || (singleRetrieve && !this.definition.documentInOneFile)) {
1110
+ const count = Object.keys(metadataMap).length;
1111
+ Util.logger.debug(
1112
+ ` - Running document for ${count} record${count === 1 ? '' : 's'}`
1113
+ );
1114
+ await this.document(metadataMap);
1115
+ } else {
1116
+ Util.logger.info(
1117
+ Util.getGrayMsg(
1118
+ ` - Skipped running document because you supplied keys and ${this.definition.type} is documented in a single file for all.`
1119
+ )
1120
+ );
1121
+ }
1122
+ }
1123
+ }
1073
1124
 
1074
1125
  /**
1075
1126
  * Builds map of metadata entries mapped to their keyfields
@@ -1528,7 +1579,10 @@ class MetadataType {
1528
1579
  // so its in retrieve but not in save. Here we put into the clone so that the original
1529
1580
  // object used for caching doesnt have the Id removed.
1530
1581
  const saveClone = JSON.parse(JSON.stringify(results[originalKey]));
1531
- if (!this.definition.keepId) {
1582
+ if (
1583
+ !this.definition.keepId &&
1584
+ this.definition.idField !== this.definition.keyField
1585
+ ) {
1532
1586
  delete saveClone[this.definition.idField];
1533
1587
  }
1534
1588
 
@@ -24,14 +24,12 @@ class MobileKeyword extends MetadataType {
24
24
  */
25
25
  static retrieve(retrieveDir, _, __, key) {
26
26
  try {
27
+ let queryParams;
28
+ [key, queryParams] = this.#getRetrieveKeyAndUrl(key);
29
+
27
30
  return super.retrieveREST(
28
31
  retrieveDir,
29
- '/legacy/v1/beta/mobile/keyword/' +
30
- (key
31
- ? key.startsWith('id:')
32
- ? key.slice(3)
33
- : `?view=simple&$where=keyword%20eq%20%27${key}%27%20`
34
- : '?view=simple'),
32
+ '/legacy/v1/beta/mobile/keyword/' + queryParams,
35
33
  null,
36
34
  key
37
35
  );
@@ -47,6 +45,109 @@ class MobileKeyword extends MetadataType {
47
45
  }
48
46
  }
49
47
 
48
+ /**
49
+ * Builds map of metadata entries mapped to their keyfields
50
+ *
51
+ * @param {object} body json of response body
52
+ * @param {string|number} [singleRetrieve] key of single item to filter by
53
+ * @returns {TYPE.MetadataTypeMap} keyField => metadata map
54
+ */
55
+ static parseResponseBody(body, singleRetrieve) {
56
+ const bodyIteratorField = this.definition.bodyIteratorField;
57
+ const keyField = this.definition.keyField;
58
+ const metadataStructure = {};
59
+ if (body !== null) {
60
+ if (Array.isArray(body)) {
61
+ // in some cases data is just an array
62
+ for (const item of body) {
63
+ this.#createCustomKeyField(item);
64
+ const key = item[keyField];
65
+ metadataStructure[key] = item;
66
+ }
67
+ } else if (body[bodyIteratorField]) {
68
+ for (const item of body[bodyIteratorField]) {
69
+ this.#createCustomKeyField(item);
70
+ const key = item[keyField];
71
+ metadataStructure[key] = item;
72
+ }
73
+ } else if (singleRetrieve) {
74
+ // some types will return a single item intead of an array if the key is supported by their api
75
+ this.#createCustomKeyField(body);
76
+ // ! currently, the id: prefix is only supported by journey (interaction)
77
+ if (singleRetrieve.startsWith('id:')) {
78
+ singleRetrieve = body[keyField];
79
+ }
80
+ metadataStructure[singleRetrieve] = body;
81
+ return metadataStructure;
82
+ }
83
+ if (
84
+ metadataStructure[singleRetrieve] &&
85
+ (typeof singleRetrieve === 'string' || typeof singleRetrieve === 'number')
86
+ ) {
87
+ // in case we really just wanted one entry but couldnt do so in the api call, filter it here
88
+ const single = {};
89
+ single[singleRetrieve] = metadataStructure[singleRetrieve];
90
+ return single;
91
+ } else if (singleRetrieve) {
92
+ return {};
93
+ }
94
+ }
95
+ return metadataStructure;
96
+ }
97
+
98
+ /**
99
+ * helper for {@link parseResponseBody} that creates a custom key field for this type based on mobileCode and keyword
100
+ *
101
+ * @private
102
+ * @param {TYPE.MetadataType} metadata single item
103
+ */
104
+ static #createCustomKeyField(metadata) {
105
+ metadata.c__codeKeyword = metadata.code.code + '.' + metadata.keyword;
106
+ }
107
+
108
+ /**
109
+ * helper for {@link preDeployTasks} and {@link createOrUpdate} to ensure we have code & keyword properly set
110
+ *
111
+ * @private
112
+ * @param {TYPE.MetadataType} metadata single item
113
+ */
114
+ static #setCodeAndKeyword(metadata) {
115
+ const [code, keyword] = metadata.c__codeKeyword.split('.');
116
+
117
+ // mobileCode
118
+ metadata.code = {
119
+ id: cache.searchForField('mobileCode', code, 'code', 'id'),
120
+ };
121
+
122
+ // keyword
123
+ metadata.keyword = keyword;
124
+ }
125
+
126
+ /**
127
+ * helper for {@link MetadataType.upsert}
128
+ *
129
+ * @param {TYPE.MetadataTypeMap} metadataMap list of metadata
130
+ * @param {string} metadataKey key of item we are looking at
131
+ * @param {boolean} hasError error flag from previous code
132
+ * @param {TYPE.MetadataTypeItemDiff[]} metadataToUpdate list of items to update
133
+ * @param {TYPE.MetadataTypeItem[]} metadataToCreate list of items to create
134
+ * @returns {'create' | 'update' | 'skip'} action to take
135
+ */
136
+ static createOrUpdate(metadataMap, metadataKey, hasError, metadataToUpdate, metadataToCreate) {
137
+ const createOrUpdateAction = super.createOrUpdate(
138
+ metadataMap,
139
+ metadataKey,
140
+ hasError,
141
+ metadataToUpdate,
142
+ metadataToCreate
143
+ );
144
+ if (createOrUpdateAction === 'update') {
145
+ // in case --changeKeyField or --changeKeyValue was used, let's ensure we set code & keyword here again
146
+ this.#setCodeAndKeyword(metadataMap[metadataKey]);
147
+ }
148
+ return createOrUpdateAction;
149
+ }
150
+
50
151
  /**
51
152
  * Retrieves event definition metadata for caching
52
153
  *
@@ -60,26 +161,29 @@ class MobileKeyword extends MetadataType {
60
161
  }
61
162
 
62
163
  /**
63
- * Retrieve a specific keyword
164
+ * retrieve an item and create a template from it
64
165
  *
65
166
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
66
- * @param {string} name name of the metadata file
167
+ * @param {string} key name of the metadata file
67
168
  * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
68
169
  * @returns {Promise.<TYPE.MetadataTypeItemObj>} Promise of metadata
69
170
  */
70
- static async retrieveAsTemplate(templateDir, name, templateVariables) {
171
+ static async retrieveAsTemplate(templateDir, key, templateVariables) {
71
172
  try {
173
+ let queryParams;
174
+ [key, queryParams] = this.#getRetrieveKeyAndUrl(key);
175
+
72
176
  return super.retrieveTemplateREST(
73
177
  templateDir,
74
- `/legacy/v1/beta/mobile/keyword/?view=simple&$where=keyword%20eq%20%27${name}%27%20`,
178
+ `/legacy/v1/beta/mobile/keyword/` + queryParams,
75
179
  templateVariables,
76
- name
180
+ key
77
181
  );
78
182
  } catch (ex) {
79
183
  // if the mobileMessage does not exist, the API returns the error "Request failed with status code 400 (ERR_BAD_REQUEST)" which would otherwise bring execution to a hold
80
- if (name && ex.code === 'ERR_BAD_REQUEST') {
184
+ if (key && ex.code === 'ERR_BAD_REQUEST') {
81
185
  Util.logger.info(
82
- `Downloaded: ${this.definition.type} (0)${Util.getKeysString(name)}`
186
+ `Downloaded: ${this.definition.type} (0)${Util.getKeysString(key)}`
83
187
  );
84
188
  } else {
85
189
  throw ex;
@@ -88,13 +192,43 @@ class MobileKeyword extends MetadataType {
88
192
  }
89
193
 
90
194
  /**
91
- * Creates a single Event Definition
195
+ * helper for {@link retrieve} and {@link retrieveAsTemplate}
196
+ *
197
+ * @private
198
+ * @param {string} key customer key of single item to retrieve / name of the metadata file
199
+ * @returns {Array} key, queryParams
200
+ */
201
+ static #getRetrieveKeyAndUrl(key) {
202
+ let queryParams;
203
+ if (key) {
204
+ if (key.startsWith('id:')) {
205
+ // overwrite queryParams
206
+ queryParams = key.slice(3);
207
+ } else if (key.includes('.')) {
208
+ // keywords are always uppercased
209
+ key = key.toUpperCase();
210
+ // format: code.keyword
211
+ const [code, keyword] = key.split('.');
212
+ queryParams = `?view=simple&$where=keyword%20eq%20%27${keyword}%27%20and%code%20eq%20%27${code}%27%20`;
213
+ } else {
214
+ throw new Error(
215
+ `key ${key} has unexpected format. Expected 'code.keyword' or 'id:yourId'`
216
+ );
217
+ }
218
+ } else {
219
+ queryParams = '?view=simple';
220
+ }
221
+ return [key, queryParams];
222
+ }
223
+
224
+ /**
225
+ * Creates a single item
92
226
  *
93
- * @param {TYPE.MetadataTypeItem} MobileKeyword a single Event Definition
227
+ * @param {TYPE.MetadataTypeItem} metadata a single item
94
228
  * @returns {Promise} Promise
95
229
  */
96
- static create(MobileKeyword) {
97
- return super.createREST(MobileKeyword, '/legacy/v1/beta/mobile/keyword/');
230
+ static create(metadata) {
231
+ return super.createREST(metadata, '/legacy/v1/beta/mobile/keyword/');
98
232
  }
99
233
  /**
100
234
  * Updates a single item
@@ -114,9 +248,21 @@ class MobileKeyword extends MetadataType {
114
248
  * manages post retrieve steps
115
249
  *
116
250
  * @param {TYPE.MetadataTypeItem} metadata a single item
117
- * @returns {TYPE.CodeExtractItem | TYPE.MetadataTypeItem} Array with one metadata object and one ssjs string
251
+ * @returns {TYPE.CodeExtractItem | TYPE.MetadataTypeItem | void} Array with one metadata object and one ssjs string; or single metadata object; nothing if filtered
118
252
  */
119
253
  static postRetrieveTasks(metadata) {
254
+ try {
255
+ cache.searchForField('mobileCode', metadata.code.code, 'code', 'id');
256
+ } catch {
257
+ // in case the the mobileCode cannot be found, do not save this keyword as its no longer accessible in the UI either
258
+ Util.logger.debug(
259
+ ` - skipping ${this.definition.type} ${
260
+ metadata[this.definition.keyField]
261
+ }. Could not find parent mobileCode ${metadata.code.code}`
262
+ );
263
+ return;
264
+ }
265
+
120
266
  if (metadata.responseMessage) {
121
267
  // extract message body
122
268
  const codeArr = [];
@@ -295,8 +441,7 @@ class MobileKeyword extends MetadataType {
295
441
  );
296
442
  }
297
443
 
298
- // mobileCode
299
- metadata.code.id = cache.searchForField('mobileCode', metadata.code.code, 'code', 'id');
444
+ this.#setCodeAndKeyword(metadata);
300
445
  return metadata;
301
446
  }
302
447
  /**
@@ -163,21 +163,17 @@ class MobileMessage extends MetadataType {
163
163
 
164
164
  // mobileKeyword
165
165
  try {
166
- if (metadata.keyword?.keyword) {
167
- cache.searchForField(
168
- 'mobileKeyword',
169
- metadata.keyword.keyword,
170
- 'keyword',
171
- 'keyword'
172
- );
173
- }
174
- if (metadata.subscriptionKeyword?.keyword) {
175
- cache.searchForField(
176
- 'mobileKeyword',
177
- metadata.subscriptionKeyword.keyword,
178
- 'keyword',
179
- 'keyword'
180
- );
166
+ for (const attr of ['keyword', 'subscriptionKeyword', 'nextKeyword']) {
167
+ if (metadata[attr]?.id) {
168
+ metadata[attr] = {
169
+ c__codeKeyword: cache.searchForField(
170
+ 'mobileKeyword',
171
+ metadata[attr].id,
172
+ 'id',
173
+ 'c__codeKeyword'
174
+ ),
175
+ };
176
+ }
181
177
  }
182
178
  } catch (ex) {
183
179
  Util.logger.warn(
@@ -265,20 +261,16 @@ class MobileMessage extends MetadataType {
265
261
  metadata.code = code;
266
262
 
267
263
  // mobileKeyword
268
- if (metadata.keyword?.keyword) {
269
- const keyword = cache.getByKey('mobileKeyword', metadata.keyword.keyword);
270
- if (!keyword) {
271
- throw new Error(`mobileKeyword ${metadata.keyword.keyword} not found in cache`);
272
- }
273
- metadata.keyword = keyword;
274
- }
275
- if (metadata.subscriptionKeyword?.keyword) {
276
- const keyword = cache.getByKey('mobileKeyword', metadata.subscriptionKeyword.keyword);
277
- if (!keyword) {
278
- throw new Error(`mobileKeyword ${metadata.keyword.keyword} not found in cache`);
264
+ for (const attr of ['keyword', 'subscriptionKeyword', 'nextKeyword']) {
265
+ if (metadata[attr]?.c__codeKeyword) {
266
+ const keywordObj = cache.getByKey('mobileKeyword', metadata[attr].c__codeKeyword);
267
+ if (!keywordObj) {
268
+ throw new Error(
269
+ `mobileKeyword ${metadata[attr].c__codeKeyword} not found in cache`
270
+ );
271
+ }
272
+ metadata[attr] = keywordObj;
279
273
  }
280
-
281
- metadata.subscriptionKeyword.keyword = keyword;
282
274
  }
283
275
 
284
276
  // campaign
@@ -50,6 +50,32 @@ class Query extends MetadataType {
50
50
  key
51
51
  );
52
52
  }
53
+ /**
54
+ * a function to start query execution via API
55
+ *
56
+ * @param {string[]} keyArr customerkey of the metadata
57
+ * @returns {Promise.<boolean>} Returns true if all items were executed successfully, otherwise false
58
+ */
59
+ static async execute(keyArr) {
60
+ const results = [];
61
+ // works only with objectId
62
+ let objectId;
63
+ for (const key of keyArr) {
64
+ if (key) {
65
+ objectId = await this._getObjectIdForSingleRetrieve(key);
66
+ if (!objectId) {
67
+ Util.logger.info(`Skipping ${key} - did not find an item with such key`);
68
+ break;
69
+ }
70
+ }
71
+ results.push(
72
+ super.executeREST(`/automation/v1/queries/${objectId}/actions/start/`, key)
73
+ );
74
+ }
75
+ const successCounter = (await Promise.all(results)).filter((r) => r === 'OK').length;
76
+ Util.logger.info(`Executed ${successCounter} of ${keyArr.length} items`);
77
+ return successCounter === keyArr.length;
78
+ }
53
79
  /**
54
80
  * helper to allow us to select single metadata entries via REST
55
81
  *
@@ -102,9 +102,8 @@ class Role extends MetadataType {
102
102
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
103
103
  Util.getKeysString(key)
104
104
  );
105
- if (this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
106
- await this.document(savedMetadata);
107
- }
105
+
106
+ await this.runDocumentOnRetrieve(key, savedMetadata);
108
107
  }
109
108
  return { metadata: parsed, type: this.definition.type };
110
109
  }
@@ -58,9 +58,9 @@ class TransactionalSMS extends TransactionalMessage {
58
58
  // we merely want to be able to show an error if it does not exist
59
59
  cache.searchForField(
60
60
  'mobileKeyword',
61
- metadata.subscriptions.keyword,
62
- 'keyword',
63
- 'keyword'
61
+ metadata.subscriptions.shortCode + '.' + metadata.subscriptions.keyword,
62
+ 'c__codeKeyword',
63
+ 'id'
64
64
  );
65
65
  }
66
66
  return metadata;
@@ -142,9 +142,9 @@ class TransactionalSMS extends TransactionalMessage {
142
142
  // we merely want to be able to show a warning if it does not exist
143
143
  cache.searchForField(
144
144
  'mobileKeyword',
145
- metadata.subscriptions.keyword,
145
+ metadata.subscriptions.shortCode + '.' + metadata.subscriptions.keyword,
146
146
  'keyword',
147
- 'keyword'
147
+ 'id'
148
148
  );
149
149
  } catch {
150
150
  Util.logger.warn(