mcdev 7.10.1 → 8.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 (144) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +1 -0
  2. package/.github/workflows/code-test.yml +8 -100
  3. package/@types/lib/Deployer.d.ts.map +1 -1
  4. package/@types/lib/Retriever.d.ts.map +1 -1
  5. package/@types/lib/index.d.ts +16 -4
  6. package/@types/lib/index.d.ts.map +1 -1
  7. package/@types/lib/metadataTypes/Asset.d.ts +45 -8
  8. package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
  9. package/@types/lib/metadataTypes/Automation.d.ts +13 -4
  10. package/@types/lib/metadataTypes/Automation.d.ts.map +1 -1
  11. package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
  12. package/@types/lib/metadataTypes/DataExtensionField.d.ts.map +1 -1
  13. package/@types/lib/metadataTypes/Event.d.ts +6 -0
  14. package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
  15. package/@types/lib/metadataTypes/Folder.d.ts +49 -12
  16. package/@types/lib/metadataTypes/Folder.d.ts.map +1 -1
  17. package/@types/lib/metadataTypes/ImportFile.d.ts +14 -0
  18. package/@types/lib/metadataTypes/ImportFile.d.ts.map +1 -1
  19. package/@types/lib/metadataTypes/Journey.d.ts +166 -6
  20. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  21. package/@types/lib/metadataTypes/MetadataType.d.ts +5 -3
  22. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  23. package/@types/lib/metadataTypes/definitions/Asset.definition.d.ts +45 -8
  24. package/@types/lib/metadataTypes/definitions/Automation.definition.d.ts +6 -0
  25. package/@types/lib/metadataTypes/definitions/Event.definition.d.ts +6 -0
  26. package/@types/lib/metadataTypes/definitions/Folder.definition.d.ts +49 -12
  27. package/@types/lib/metadataTypes/definitions/ImportFile.definition.d.ts +3 -0
  28. package/@types/lib/metadataTypes/definitions/Journey.definition.d.ts +159 -6
  29. package/@types/lib/util/cache.d.ts +2 -1
  30. package/@types/lib/util/cache.d.ts.map +1 -1
  31. package/@types/lib/util/devops.d.ts.map +1 -1
  32. package/@types/types/mcdev.d.d.ts +16 -0
  33. package/@types/types/mcdev.d.d.ts.map +1 -1
  34. package/boilerplate/files/.vscode/settings.json +5 -0
  35. package/eslint.config.js +2 -2
  36. package/lib/Deployer.js +3 -0
  37. package/lib/Retriever.js +8 -2
  38. package/lib/cli.js +29 -0
  39. package/lib/index.js +25 -28
  40. package/lib/metadataTypes/Asset.js +126 -10
  41. package/lib/metadataTypes/Automation.js +135 -40
  42. package/lib/metadataTypes/DataExtension.js +0 -10
  43. package/lib/metadataTypes/DataExtensionField.js +9 -15
  44. package/lib/metadataTypes/Event.js +42 -19
  45. package/lib/metadataTypes/Folder.js +98 -12
  46. package/lib/metadataTypes/ImportFile.js +90 -29
  47. package/lib/metadataTypes/Journey.js +182 -23
  48. package/lib/metadataTypes/MetadataType.js +38 -12
  49. package/lib/metadataTypes/definitions/Asset.definition.js +196 -142
  50. package/lib/metadataTypes/definitions/Automation.definition.js +6 -0
  51. package/lib/metadataTypes/definitions/DataExtension.definition.js +7 -6
  52. package/lib/metadataTypes/definitions/Event.definition.js +6 -0
  53. package/lib/metadataTypes/definitions/Folder.definition.js +69 -22
  54. package/lib/metadataTypes/definitions/ImportFile.definition.js +3 -0
  55. package/lib/metadataTypes/definitions/Journey.definition.js +165 -11
  56. package/lib/util/cache.js +24 -5
  57. package/lib/util/devops.js +20 -5
  58. package/package.json +16 -17
  59. package/test/general.test.js +8 -8
  60. package/test/mockRoot/.mcdev-validations.js +2 -3
  61. package/test/mockRoot/.mcdevrc.json +1 -1
  62. package/test/mockRoot/deploy/testInstance/testBU/importFile/testExisting_importFile.importFile-meta.json +2 -1
  63. package/test/mockRoot/deploy/testInstance/testBU/importFile/testNew_importFile.importFile-meta.json +2 -1
  64. package/test/resourceFactory.js +31 -5
  65. package/test/resources/1111111/dataExtension/update-expected.json +1 -1
  66. package/test/resources/9999999/asset/test_coderesource_js-retrieve-expected.js +1 -0
  67. package/test/resources/9999999/asset/test_coderesource_js-retrieve-expected.json +47 -0
  68. package/test/resources/9999999/asset/test_coderesource_json-retrieve-expected.json +47 -0
  69. package/test/resources/9999999/asset/test_coderesource_json-retrieve-expected.jsonc +1 -0
  70. package/test/resources/9999999/asset/test_coderesource_xml-retrieve-expected.json +46 -0
  71. package/test/resources/9999999/asset/test_coderesource_xml-retrieve-expected.xml +1 -0
  72. package/test/resources/9999999/asset/test_interactivecontent-retrieve-expected.json +43 -0
  73. package/test/resources/9999999/asset/test_landingpage-retrieve-expected.json +42 -0
  74. package/test/resources/9999999/asset/test_microsite-retrieve-expected.json +43 -0
  75. package/test/resources/9999999/asset/v1/content/assets/9451/get-response.json +61 -0
  76. package/test/resources/9999999/asset/v1/content/assets/9456/get-response.json +56 -0
  77. package/test/resources/9999999/asset/v1/content/assets/9458/get-response.json +56 -0
  78. package/test/resources/9999999/asset/v1/content/assets/9460/get-response.json +59 -0
  79. package/test/resources/9999999/asset/v1/content/assets/9463/get-response.json +61 -0
  80. package/test/resources/9999999/asset/v1/content/assets/9465/get-response.json +54 -0
  81. package/test/resources/9999999/asset/v1/content/assets/query/post-response-assetType.idIN1,3,4,14,15,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,205,206,210,211,212,213,214,215,216,217,218,219,220,221,222.json +94 -1
  82. package/test/resources/9999999/asset/v1/content/assets/query/post-response-assetType.idIN219,220,221,222,223,224,225,226,227,228,230,232,240,241,242,243,244,245.json +168 -0
  83. package/test/resources/9999999/asset/v1/content/assets/query/post-response-assetType.idIN240,241,242,243,244,245.json +144 -0
  84. package/test/resources/9999999/asset/v1/content/assets/query/post-response-assetType.idIN246,247,248,249.json +132 -0
  85. package/test/resources/9999999/dataExtension/update-callout-afterCreatedViaEvent-expected.xml +1 -1
  86. package/test/resources/9999999/dataFolder/{retrieve-ContentTypeINasset,asset-sha,automatio,dataexten,hidden,journey,list,mysubs,publicati,queryacti,salesforc,shared_da,shared_da,shared_sa,ssjsactiv,synchroni,triggered,triggered,useriniti-response.xml → retrieve-ContentTypeINasset,asset-sha,automatio,cloudpage,dataexten,hidden,journey,list,mysubs,publicati,queryacti,salesforc,shared_da,shared_da,shared_sa,ssjsactiv,synchroni,triggered,triggered,useriniti-response.xml} +44 -1
  87. package/test/resources/9999999/dataFolder/{retrieve-ContentTypeINasset,asset-sha,automatio,dataexten,hidden,journey,list,mysubs,publicati,queryacti,salesforc,shared_da,shared_da,shared_sa,ssjsactiv,synchroni,useriniti-response.xml → retrieve-ContentTypeINasset,asset-sha,automatio,cloudpage,dataexten,hidden,journey,list,mysubs,publicati,queryacti,salesforc,shared_da,shared_da,shared_sa,ssjsactiv,synchroni,useriniti-response.xml} +44 -0
  88. package/test/resources/9999999/dataFolder/{retrieve-ContentTypeINasset,asset-shared,dataextension,hidden,salesforcedataextension,shared_data,shared_dataextension,shared_salesforcedataextension,synchronizeddataextension-response.xml → retrieve-ContentTypeINasset,asset-shared,cloudpages,dataextension,hidden,salesforcedataextension,shared_data,shared_dataextension,shared_salesforcedataextension,synchronizeddataextension-response.xml} +44 -0
  89. package/test/resources/9999999/dataFolder/{retrieve-ContentTypeINasset,asset-shared,journey-response.xml → retrieve-ContentTypeINasset,asset-shared,cloudpages,journey-response.xml} +44 -0
  90. package/test/resources/9999999/dataFolder/retrieve-ContentTypeINasset,asset-shared,cloudpages,ssjsactivity-response.xml +136 -0
  91. package/test/resources/9999999/dataFolder/{retrieve-ContentTypeINasset,asset-shared,ssjsactivity-response.xml → retrieve-ContentTypeINasset,asset-shared,cloudpages-response.xml} +29 -7
  92. package/test/resources/9999999/dataFolder/{+retrieve-response.xml → retrieve-response.xml} +23 -0
  93. package/test/resources/9999999/dataFolder/update-response.xml +31 -0
  94. package/test/resources/9999999/event/build-expected.json +0 -1
  95. package/test/resources/9999999/event/get-automation-expected.json +0 -1
  96. package/test/resources/9999999/event/get-expected.json +0 -2
  97. package/test/resources/9999999/event/get-published-expected.json +0 -2
  98. package/test/resources/9999999/event/post_withExistingDE-expected.json +0 -2
  99. package/test/resources/9999999/event/post_withSchema-callout-expected.json +3 -0
  100. package/test/resources/9999999/event/post_withSchema-expected.json +0 -2
  101. package/test/resources/9999999/event/put-callout-expected.json +3 -0
  102. package/test/resources/9999999/event/put-expected.json +0 -2
  103. package/test/resources/9999999/event/template-expected.json +0 -1
  104. package/test/resources/9999999/folder-deploy/Data Extensions/testExisting_folder.folder-meta.json +9 -0
  105. package/test/resources/9999999/importFile/get-dataImport-expected.json +1 -1
  106. package/test/resources/9999999/importFile/get-expected.json +4 -1
  107. package/test/resources/9999999/interaction/v1/interactions/0175b971-71a3-4d8e-98ac-48121f3fbf4f/audit/all/get-response-versionNumber=1.json +86 -0
  108. package/test/resources/9999999/interaction/v1/interactions/0175b971-71a3-4d8e-98ac-48121f3fbf4f/audit/all/get-response-versionNumber=2.json +101 -0
  109. package/test/resources/9999999/interaction/v1/interactions/0175b971-71a3-4d8e-98ac-48121f3fbf4f/audit/all/get-response-versionNumber=3.json +86 -0
  110. package/test/resources/9999999/interaction/v1/interactions/0175b971-71a3-4d8e-98ac-48121f3fbf4f/delete-response-versionNumber=1.txt +1 -0
  111. package/test/resources/9999999/interaction/v1/interactions/0175b971-71a3-4d8e-98ac-48121f3fbf4f/get-response-versionNumber=1.json +461 -0
  112. package/test/resources/9999999/interaction/v1/interactions/0175b971-71a3-4d8e-98ac-48121f3fbf4f/get-response-versionNumber=3.json +461 -0
  113. package/test/resources/9999999/interaction/v1/interactions/dsfdsafdsa-922c-4568-85a5-e5cc77efc3be/audit/all/get-response-versionNumber=1.json +38 -0
  114. package/test/resources/9999999/interaction/v1/interactions/dsfdsafdsa-922c-4568-85a5-e5cc77efc3be/audit/all/get-response-versionNumber=2.json +53 -0
  115. package/test/resources/9999999/interaction/v1/interactions/get-response.json +2 -2
  116. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_journey_Multistep/get-response-versionNumber=1.json +461 -0
  117. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_journey_Quicksend/get-response-versionNumber=1.json +253 -0
  118. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_temail/get-response-versionNumber=1.json +219 -0
  119. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_temail_notPublished/get-response-versionNumber=1.json +226 -0
  120. package/test/resources/9999999/interaction/v1/interactions/publishAsync/0175b971-71a3-4d8e-98ac-48121f3fbf4f/post-response-versionNumber=3.json +4 -0
  121. package/test/resources/9999999/journey/build-expected.json +0 -1
  122. package/test/resources/9999999/journey/create-transactionaEmail-publish-expected.json +0 -1
  123. package/test/resources/9999999/journey/get-multistep-expected.json +0 -4
  124. package/test/resources/9999999/journey/get-published-expected.json +0 -1
  125. package/test/resources/9999999/journey/get-quicksend-expected.json +0 -1
  126. package/test/resources/9999999/journey/get-quicksend-rcb-id-expected.json +0 -1
  127. package/test/resources/9999999/journey/get-quicksend-rcb-key-expected.json +0 -1
  128. package/test/resources/9999999/journey/get-quicksend-rcb-name-expected.json +0 -1
  129. package/test/resources/9999999/journey/get-transactionalEmail-expected.json +0 -1
  130. package/test/resources/9999999/journey/template-expected.json +0 -1
  131. package/test/resources/9999999/query/patch-expected.sql +1 -1
  132. package/test/resources/9999999/query/post-expected.sql +1 -1
  133. package/test/resources/9999999/script/patch-expected.ssjs +1 -0
  134. package/test/type.asset.test.js +123 -13
  135. package/test/type.automation.test.js +3 -1
  136. package/test/type.folder.test.js +12 -2
  137. package/test/type.journey.test.js +130 -1
  138. package/test/type.script.test.js +1 -0
  139. package/test/type.senderProfile.test.js +1 -0
  140. package/test/utils.js +2 -0
  141. package/types/mcdev.d.js +4 -0
  142. package/test/resources/9999999/asset/v1/content/assets/query/post-response-assetType.idIN219,220,221,222,223,224,225,226,227,228,230,232.json +0 -32
  143. /package/test/resources/9999999/dataFolder/{+retrieve-QAA-response.xml → retrieve-QAA-response.xml} +0 -0
  144. /package/test/resources/9999999/interaction/v1/interactions/publishAsync/0175b971-71a3-4d8e-98ac-48121f3fbf4f/{post-response.json → post-response-versionNumber=1.json} +0 -0
@@ -410,6 +410,9 @@ class Asset extends MetadataType {
410
410
  'availableViews',
411
411
  'data',
412
412
  'tags',
413
+
414
+ 'meta',
415
+ 'content',
413
416
  ];
414
417
  }
415
418
 
@@ -502,7 +505,7 @@ class Asset extends MetadataType {
502
505
  await this.requestAndSaveExtended(items, subType, retrieveDir, templateVariables);
503
506
  }
504
507
  // always show the summary even if we already had the progress bar in the console
505
- Util.logger.info(` Downloaded asset-${subType}: ${items.length}`);
508
+ Util.logger.info(` Downloaded asset${subType ? '-' + subType : ''}: ${items.length}`);
506
509
  }
507
510
 
508
511
  return items;
@@ -766,6 +769,44 @@ class Asset extends MetadataType {
766
769
  }
767
770
  }
768
771
 
772
+ // get asset-asset
773
+ try {
774
+ if (metadata.meta?.thumbnailRefAssetId) {
775
+ // cloudpages', 'landingpage', 'microsite', 'interactivecontent'
776
+ metadata.meta.r__asset_key = cache.searchForField(
777
+ 'asset',
778
+ metadata.meta?.thumbnailRefAssetId,
779
+ 'id',
780
+ 'customerKey'
781
+ );
782
+ delete metadata.meta.thumbnailRefAssetId;
783
+ }
784
+ } catch {
785
+ Util.logger.warn(
786
+ ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
787
+ metadata[this.definition.keyField]
788
+ }): Could not find associated cloudpage content with id (${metadata.meta.thumbnailRefAssetId})`
789
+ );
790
+ }
791
+ if (
792
+ ['landingpage', 'microsite', 'interactivecontent'].includes(metadata.assetType.name) &&
793
+ typeof metadata.content === 'string'
794
+ ) {
795
+ // cloudpages', 'landingpage', 'microsite', 'interactivecontent'
796
+ metadata.content = JSON.parse(metadata.content);
797
+ }
798
+
799
+ if (typeof metadata.data?.site?.content === 'string') {
800
+ // for all xx-coderesource types
801
+ metadata.data.site.content = JSON.parse(metadata.data.site.content);
802
+ }
803
+
804
+ if (metadata.meta?.cloudPages) {
805
+ metadata.meta.cloudPages.c__published = metadata.meta.cloudPages.publishDate
806
+ ? true
807
+ : false;
808
+ }
809
+
769
810
  // extract HTML for selected subtypes and convert payload for easier processing in MetadataType.saveResults()
770
811
  return this._extractCode(metadata);
771
812
  }
@@ -858,14 +899,22 @@ class Asset extends MetadataType {
858
899
  // folder
859
900
  this.setFolderId(metadata);
860
901
 
861
- if (
862
- metadata.assetType.name === 'webpage' &&
863
- !cache.getByKey(this.definition.type, metadata[this.definition.keyField])
864
- ) {
865
- // we are attempting to CREATE a cloudpage asset which needs to be prevented.
866
- throw new Error(
867
- 'CloudPages cannot be created via mcdev. Please create it via the UI first, then change its key to the value required by you, and finally, re-run this deployment.'
868
- );
902
+ if (!cache.getByKey(this.definition.type, metadata[this.definition.keyField])) {
903
+ if (metadata.assetType.name === 'webpage') {
904
+ // we are attempting to CREATE a cloudpage asset which needs to be prevented.
905
+ throw new Error(
906
+ 'CloudPage content cannot be created via mcdev. Please create it via the UI first, then you can update the content or key.'
907
+ );
908
+ }
909
+ if (
910
+ this.definition.extendedSubTypes.cloudpage.includes(metadata.assetType.name) ||
911
+ this.definition.extendedSubTypes.coderesource.includes(metadata.assetType.name)
912
+ ) {
913
+ // if we do create those, we get the error "Asset <id> not found" because the internal endpoint internal/v2/cloudpages/sites won't find it.
914
+ throw new Error(
915
+ 'CloudPage assets cannot be created via mcdev. Please create it via the UI first, then you can update the content or key.'
916
+ );
917
+ }
869
918
  }
870
919
 
871
920
  // template-based emails
@@ -901,6 +950,33 @@ class Asset extends MetadataType {
901
950
  delete metadata.views.html.template.r__asset_key;
902
951
  }
903
952
 
953
+ if (metadata.meta?.r__asset_key) {
954
+ // cloudpages: 'landingpage', 'microsite', 'interactivecontent'
955
+ // get asset-asset
956
+ metadata.meta.thumbnailRefAssetId = cache.searchForField(
957
+ 'asset',
958
+ metadata.meta.r__asset_key,
959
+ 'customerKey',
960
+ 'id'
961
+ );
962
+ delete metadata.meta.r__asset_key;
963
+ }
964
+ if (metadata.meta?.cloudPages?.c__published) {
965
+ // TODO: removal should be done via asset.definition instead
966
+ delete metadata.meta.cloudPages.c__published;
967
+ }
968
+ if (
969
+ ['landingpage', 'microsite', 'interactivecontent'].includes(metadata.assetType.name) &&
970
+ typeof metadata.content === 'object'
971
+ ) {
972
+ // cloudpages: 'landingpage', 'microsite', 'interactivecontent'
973
+ metadata.content = JSON.stringify(metadata.content);
974
+ }
975
+ if (typeof metadata.data?.site?.content === 'object') {
976
+ // coderesource: for all xx-coderesource types
977
+ metadata.data.site.content = JSON.stringify(metadata.data.site.content);
978
+ }
979
+
904
980
  // restore asset type id which is needed for deploy
905
981
  metadata.assetType.id = this.definition.typeMapping[metadata.assetType.name];
906
982
 
@@ -1575,6 +1651,12 @@ class Asset extends MetadataType {
1575
1651
 
1576
1652
  break;
1577
1653
  }
1654
+ case 'jscoderesource': // coderesource
1655
+ case 'csscoderesource': // coderesource
1656
+ case 'jsoncoderesource': // coderesource
1657
+ case 'rsscoderesource': // coderesource
1658
+ case 'textcoderesource': // coderesource
1659
+ case 'xmlcoderesource': // coderesource
1578
1660
  case 'buttonblock': // block - Button Block
1579
1661
  case 'freeformblock': // block
1580
1662
  case 'htmlblock': // block
@@ -1587,7 +1669,17 @@ class Asset extends MetadataType {
1587
1669
  // metadata.content
1588
1670
  subDirArr = [this.definition.type, subType];
1589
1671
  readDirArr = [deployDir, ...subDirArr];
1590
- const fileExtArr = ['html', 'ssjs', 'amp'];
1672
+ const fileExtArr = [
1673
+ 'html',
1674
+ 'ssjs',
1675
+ 'amp',
1676
+ 'js',
1677
+ 'css',
1678
+ 'rss',
1679
+ 'txt',
1680
+ 'xml',
1681
+ 'jsonc',
1682
+ ];
1591
1683
  for (const ext of fileExtArr) {
1592
1684
  if (
1593
1685
  await File.pathExists(
@@ -1858,6 +1950,30 @@ class Asset extends MetadataType {
1858
1950
  subFolder: [customerKey],
1859
1951
  };
1860
1952
  }
1953
+ case 'jscoderesource': // coderesource
1954
+ case 'csscoderesource': // coderesource
1955
+ case 'jsoncoderesource': // coderesource
1956
+ case 'rsscoderesource': // coderesource
1957
+ case 'textcoderesource': // coderesource
1958
+ case 'xmlcoderesource': {
1959
+ // metadata.content
1960
+ const content = metadata?.content;
1961
+ const fileExt = metadata.assetType.name
1962
+ .replace('coderesource', '')
1963
+ .replace('text', 'txt')
1964
+ .replace('json', 'jsonc');
1965
+
1966
+ if (content?.length) {
1967
+ codeArr.push({
1968
+ subFolder: null,
1969
+ fileName: customerKey,
1970
+ fileExt: fileExt,
1971
+ content: content,
1972
+ });
1973
+ delete metadata.content;
1974
+ }
1975
+ return { json: metadata, codeArr: codeArr, subFolder: null };
1976
+ }
1861
1977
  case 'buttonblock': // block-buttonblock
1862
1978
  case 'freeformblock': // block-freeformblock
1863
1979
  case 'htmlblock': // block-htmlblock
@@ -6,6 +6,7 @@ import File from '../util/file.js';
6
6
  import Definitions from '../MetadataTypeDefinitions.js';
7
7
  import cache from '../util/cache.js';
8
8
  import pLimit from 'p-limit';
9
+ import Retriever from '../Retriever.js';
9
10
 
10
11
  /**
11
12
  * @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
@@ -92,7 +93,7 @@ class Automation extends MetadataType {
92
93
  id: objectID,
93
94
  uri: '/automation/v1/automations/' + objectID,
94
95
  })),
95
- 50,
96
+ 10,
96
97
  !key
97
98
  )
98
99
  : null;
@@ -125,20 +126,32 @@ class Automation extends MetadataType {
125
126
  * helper for {@link this.retrieveRESTcollection}
126
127
  *
127
128
  * @param {SDKError} ex exception
128
- * @param {string} id id or key of item
129
+ * @param {string} key id or key of item
130
+ * @param {string} url url to call for retry
129
131
  * @returns {Promise.<any>} can return retry-result
130
132
  */
131
- static async handleRESTErrors(ex, id) {
133
+ static async handleRESTErrors(ex, key, url) {
132
134
  try {
133
- if (ex.message == 'socket hang up') {
135
+ if (ex.message == 'socket hang up' || ex.code == 'ERR_BAD_RESPONSE') {
134
136
  // one more retry; it's a rare case but retrying again should solve the issue gracefully
135
- return await this.client.rest.get('/automation/v1/automations/' + id);
137
+ Util.logger.info(
138
+ ` - Connection problem (Code: ${ex.code}). Retrying once
139
+ ${
140
+ ex.endpoint
141
+ ? Util.getGrayMsg(
142
+ ' - ' + ex.endpoint.split('rest.marketingcloudapis.com')[1]
143
+ )
144
+ : ''
145
+ }`
146
+ );
147
+ Util.logger.errorStack(ex);
148
+ return await this.client.rest.get(url);
136
149
  }
137
150
  } catch {
138
151
  // no extra action needed, handled below
139
152
  }
140
153
  // if we do get here, we should log the error and continue instead of failing to download all automations
141
- Util.logger.error(` ☇ skipping Automation ${id}: ${ex.message} ${ex.code}`);
154
+ Util.logger.error(` ☇ skipping ${this.definition.type} ${key}: ${ex.message} ${ex.code}`);
142
155
  return null;
143
156
  }
144
157
 
@@ -216,6 +229,10 @@ class Automation extends MetadataType {
216
229
  if (extended?.scheduleObject?.id && item.schedule) {
217
230
  // save schedule id in cached metadata for retrieval during scheduling
218
231
  item.schedule.id = extended.scheduleObject.id;
232
+ item.schedule.description = extended.scheduleObject.description;
233
+ item.schedule.icalRecur ||= extended.scheduleObject.iCalRecur;
234
+ item.schedule.startDate ||= extended.scheduleObject.startDate;
235
+ item.schedule.timezoneName ||= extended.scheduleObject.timeZone;
219
236
  }
220
237
 
221
238
  // add timezone to wait activities
@@ -717,22 +734,56 @@ class Automation extends MetadataType {
717
734
  Util.logger.info(
718
735
  ` ☇ skipping ${Util.getTypeKeyName(this.definition, item)}: already ${mode === 'schedule' ? 'activated' : 'paused'}.`
719
736
  );
720
- } else {
721
- // schedule
722
- promiseResults.push(
723
- this.#schedulePauseItem(mode, key, item.legacyId, item.schedule.id)
724
- );
737
+ continue;
738
+ }
739
+ if (mode === 'schedule') {
740
+ try {
741
+ this._checkSchedule(item.schedule, true);
742
+ } catch (ex) {
743
+ Util.logger.error(
744
+ ` ☇ skipping ${this.definition.type} ${key} (${item.schedule.description}): ${ex.message}`
745
+ );
746
+ continue;
747
+ }
725
748
  }
749
+
750
+ // schedule
751
+ promiseResults.push(
752
+ this.#schedulePauseItem(
753
+ mode,
754
+ key,
755
+ item.legacyId,
756
+ item.schedule.id,
757
+ item.schedule.description
758
+ )
759
+ );
726
760
  }
727
761
  const results = await Promise.all(promiseResults);
728
762
  const updatedKeyArr = results
729
763
  .filter(Boolean)
730
- .filter((r) => r.response.id)
764
+ .filter((r) => r.response?.id)
731
765
  .map((r) => r.key);
732
766
  Util.logger.info(
733
767
  `${mode === 'schedule' ? 'Activated' : 'Paused'} ${updatedKeyArr.length} of ${keyArr.length} items`
734
768
  );
735
769
 
770
+ if (updatedKeyArr.length) {
771
+ Util.logger.info(
772
+ Util.getGrayMsg(
773
+ `Caching ${this.definition.type} post-${mode === 'schedule' ? 'activation' : 'pausing'} to update local files.`
774
+ )
775
+ );
776
+ // re-retrieve the items that were activated / paused
777
+ const retriever = new Retriever(this.properties, this.buObject);
778
+ try {
779
+ await retriever.retrieve([this.definition.type], updatedKeyArr);
780
+ } catch (ex) {
781
+ Util.logger.warn(
782
+ `Could not re-retrieve ${mode === 'schedule' ? 'activated' : 'paused'} ${this.definition.type}s: ${ex.message}`
783
+ );
784
+ }
785
+ }
786
+
736
787
  return updatedKeyArr;
737
788
  }
738
789
 
@@ -743,15 +794,39 @@ class Automation extends MetadataType {
743
794
  * @param {string} key automation key
744
795
  * @param {string} automationLegacyId automation id
745
796
  * @param {string} [scheduleLegacyId] schedule id
797
+ * @param {string} [description] schedule description
746
798
  * @returns {Promise.<{key:string, response:object}>} metadata key and API response
747
799
  */
748
- static async #schedulePauseItem(mode, key, automationLegacyId, scheduleLegacyId) {
800
+ static async #schedulePauseItem(mode, key, automationLegacyId, scheduleLegacyId, description) {
749
801
  if (!scheduleLegacyId) {
750
802
  const extended = await this.client.rest.get(
751
803
  `/legacy/v1/beta/bulk/automations/automation/definition/` + automationLegacyId
752
804
  );
753
- if (extended.scheduleObject?.id) {
754
- scheduleLegacyId = extended.scheduleObject.id;
805
+ /** @type {AutomationSchedule} */
806
+ const scheduleObjectLegacy = extended.scheduleObject;
807
+ if (scheduleObjectLegacy?.id) {
808
+ scheduleLegacyId = scheduleObjectLegacy.id;
809
+ if (mode === 'schedule') {
810
+ // convert legacy API schedule to new schedule
811
+ /** @type {AutomationSchedule} */
812
+ const scheduleObject = {
813
+ id: scheduleObjectLegacy.id,
814
+ typeId: null,
815
+ endDate: null,
816
+ startDate: scheduleObjectLegacy.startDate,
817
+ timezoneName: scheduleObjectLegacy.timeZone,
818
+ icalRecur: scheduleObjectLegacy.iCalRecur,
819
+ };
820
+ try {
821
+ this._checkSchedule(scheduleObject);
822
+ } catch (ex) {
823
+ Util.logger.error(
824
+ ` ☇ skipping ${this.definition.type} ${key}: ${ex.message}`
825
+ );
826
+ return null;
827
+ }
828
+ }
829
+ description = scheduleObjectLegacy.description;
755
830
  } else {
756
831
  Util.logger.error(
757
832
  ` ☇ skipping ${this.definition.type} ${key}: no valid schedule settings found.`
@@ -759,21 +834,28 @@ class Automation extends MetadataType {
759
834
  return null;
760
835
  }
761
836
  }
762
- const response = await this.client.rest.post(
763
- '/legacy/v1/beta/bulk/automations/automation/definition/?action=' +
764
- (mode === 'schedule' ? 'schedule' : 'pauseSchedule'),
765
- {
766
- id: automationLegacyId,
767
- scheduleObject: {
768
- id: scheduleLegacyId,
769
- },
837
+ let response;
838
+ try {
839
+ response = await this.client.rest.post(
840
+ '/legacy/v1/beta/bulk/automations/automation/definition/?action=' +
841
+ (mode === 'schedule' ? 'schedule' : 'pauseSchedule'),
842
+ {
843
+ id: automationLegacyId,
844
+ scheduleObject: {
845
+ id: scheduleLegacyId,
846
+ },
847
+ }
848
+ );
849
+ if (response?.id === automationLegacyId) {
850
+ const item = {};
851
+ item[this.definition.keyField] = key;
852
+ Util.logger.info(
853
+ ` - ${mode === 'schedule' ? '✅ activated' : '🛑 paused'} scheduled ${Util.getTypeKeyName(this.definition, item)}${mode === 'schedule' ? ' (' + description + ')' : ''}`
854
+ );
770
855
  }
771
- );
772
- if (response?.id === automationLegacyId) {
773
- const item = {};
774
- item[this.definition.keyField] = key;
775
- Util.logger.info(
776
- ` - ${mode === 'schedule' ? '✅ activated' : '🛑 paused'} scheduled ${Util.getTypeKeyName(this.definition, item)}`
856
+ } catch (ex) {
857
+ Util.logger.error(
858
+ ` error ${mode === 'schedule' ? 'activating' : 'pausing'} ${this.definition.type} ${key} (${description}): ${ex.message}`
777
859
  );
778
860
  }
779
861
 
@@ -1258,15 +1340,6 @@ class Automation extends MetadataType {
1258
1340
  'Path'
1259
1341
  );
1260
1342
  delete metadata[folderIdField];
1261
- if (metadata.r__folder_Path !== 'my automations') {
1262
- Util.logger.verbose(
1263
- `- automation '${
1264
- metadata[this.definition.nameField]
1265
- }' is located in subfolder ${
1266
- metadata.r__folder_Path
1267
- }. Please note that creating automation folders is not supported via API and hence you will have to create it manually in the GUI if you choose to deploy this automation.`
1268
- );
1269
- }
1270
1343
  } catch (ex) {
1271
1344
  Util.logger.warn(
1272
1345
  ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
@@ -1281,9 +1354,10 @@ class Automation extends MetadataType {
1281
1354
  * based on combination of ical string and start/end dates.
1282
1355
  *
1283
1356
  * @param {AutomationSchedule} scheduleObject child of automation metadata used for scheduling
1357
+ * @param {boolean} [errorOnNotSchedulable] used if run for schedule command
1284
1358
  * @returns {void} throws and error in case of problems
1285
1359
  */
1286
- static _checkSchedule(scheduleObject) {
1360
+ static _checkSchedule(scheduleObject, errorOnNotSchedulable = false) {
1287
1361
  // build recurrence
1288
1362
  const recurHelper = {};
1289
1363
  // ical values are split by ; then have key values split by =
@@ -1300,7 +1374,6 @@ class Automation extends MetadataType {
1300
1374
  'The smallest interval you can configure is 5 minutes. Please adjust your schedule.'
1301
1375
  );
1302
1376
  }
1303
-
1304
1377
  if (this.definition.timeZoneMapping[scheduleObject.timezoneName]) {
1305
1378
  scheduleObject.timezoneId =
1306
1379
  this.definition.timeZoneMapping[scheduleObject.timezoneName];
@@ -1309,6 +1382,28 @@ class Automation extends MetadataType {
1309
1382
  `Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
1310
1383
  );
1311
1384
  }
1385
+
1386
+ if (recurHelper.COUNT == 1) {
1387
+ const timezoneString = this.definition.timeZoneDifference[scheduleObject.timezoneId];
1388
+ // add tz to input date to ensure Date() creates a date object with the right tz
1389
+ const inputStartDateString = scheduleObject.startDate + timezoneString;
1390
+ if (new Date(inputStartDateString) > new Date()) {
1391
+ const msg =
1392
+ 'The automation will run only once. Please make sure this is intended and that the start date is in the future.';
1393
+ if (errorOnNotSchedulable) {
1394
+ throw new Error(msg);
1395
+ } else {
1396
+ Util.logger.warn(msg);
1397
+ }
1398
+ } else {
1399
+ const msg = `The automation will run only once but its start date is in the past. While this can be saved, it cannot be scheduled.`;
1400
+ if (errorOnNotSchedulable) {
1401
+ throw new Error(msg);
1402
+ } else {
1403
+ Util.logger.warn(msg);
1404
+ }
1405
+ }
1406
+ }
1312
1407
  }
1313
1408
 
1314
1409
  /**
@@ -84,11 +84,6 @@ class DataExtension extends MetadataType {
84
84
  continue;
85
85
  }
86
86
  }
87
- if (metadataToUpdate.length) {
88
- Util.logger.info(
89
- ' - Please note that Data Retention Policies can only be set during creation, not during update.'
90
- );
91
- }
92
87
  const createLimit = pLimit(10);
93
88
  const createResults = (
94
89
  await Promise.allSettled(
@@ -316,11 +311,6 @@ class DataExtension extends MetadataType {
316
311
  : null;
317
312
  if (cachedVersion) {
318
313
  // UPDATE
319
- // restore retention values that are typically not returned by the update call
320
- item.RowBasedRetention = cachedVersion.RowBasedRetention;
321
- item.ResetRetentionPeriodOnImport = cachedVersion.ResetRetentionPeriodOnImport;
322
- item.DeleteAtEndOfRetentionPeriod = cachedVersion.DeleteAtEndOfRetentionPeriod;
323
- item.RetainUntil = cachedVersion.RetainUntil;
324
314
 
325
315
  const existingFields = DataExtension.oldFields[item[this.definition.keyField]];
326
316
 
@@ -175,10 +175,9 @@ class DataExtensionField extends MetadataType {
175
175
  // Updating to a new FieldType will result in an error; warn & afterwards remove it
176
176
  if (itemOld.FieldType !== item.FieldType) {
177
177
  // applicable: with or without data but simply ignored by API
178
- Util.logger.warn(
179
- ` - The Field Type of an existing field cannot be changed. Keeping the original value for [${deKey}].[${item.Name}]: '${itemOld.FieldType}'`
178
+ Util.logger.error(
179
+ ` - The Field Type of an existing field cannot be changed. [${deKey}].[${item.Name}]: '${itemOld.FieldType}'`
180
180
  );
181
- item.FieldType = itemOld.FieldType;
182
181
  }
183
182
  if (item.FieldType !== 'Decimal') {
184
183
  // remove scale - it's only used for "Decimal" to define the digits behind the decimal
@@ -188,17 +187,15 @@ class DataExtensionField extends MetadataType {
188
187
 
189
188
  if (itemOld.MaxLength > item.MaxLength) {
190
189
  // applicable: with or without data (Code 310007)
191
- Util.logger.warn(
192
- ` - The length of an existing field cannot be decreased. Keeping the original value for [${deKey}].[${item.Name}]: '${itemOld.MaxLength}'`
190
+ Util.logger.error(
191
+ ` - The length of an existing field cannot be decreased. [${deKey}].[${item.Name}]: '${itemOld.MaxLength}'`
193
192
  );
194
- item.MaxLength = itemOld.MaxLength;
195
193
  }
196
194
  if (Util.isFalse(itemOld.IsRequired) && Util.isTrue(item.IsRequired)) {
197
195
  // applicable: with or without data (Code 310007)
198
- Util.logger.warn(
199
- ` - A field cannot be changed to be required on update after it was created to allow nulls. Resetting to not equired: [${deKey}].[${item.Name}]`
196
+ Util.logger.error(
197
+ ` - A field cannot be changed to be required on update after it was created to allow nulls. [${deKey}].[${item.Name}]`
200
198
  );
201
- item.IsRequired = itemOld.IsRequired;
202
199
  }
203
200
 
204
201
  // enable renaming
@@ -241,9 +238,8 @@ class DataExtensionField extends MetadataType {
241
238
  item.IsRequired = false;
242
239
  } else {
243
240
  Util.logger.error(
244
- `- You cannot add a new primary key field to an existing table that has data. Removing [${deKey}].[${item.Name}] from deployment`
241
+ `- You cannot add a new primary key field to an existing table that has data. [${deKey}].[${item.Name}]`
245
242
  );
246
- deployColumns.splice(i, 1);
247
243
  }
248
244
  }
249
245
  if (item.Name_new) {
@@ -268,15 +264,13 @@ class DataExtensionField extends MetadataType {
268
264
  // filter bad manual changes to the json
269
265
  if (!Util.isTrue(item.IsRequired) && !Util.isFalse(item.IsRequired)) {
270
266
  Util.logger.error(
271
- `- Invalid value for 'IsRequired' of [${deKey}].[${item.Name}]. Found '${item.IsRequired}' instead of 'true'/'false'. Removing field from deploy!`
267
+ `- Invalid value for 'IsRequired' of [${deKey}].[${item.Name}]. Found '${item.IsRequired}' instead of 'true'/'false'.`
272
268
  );
273
- deployColumns.splice(i, 1);
274
269
  }
275
270
  if (!Util.isTrue(item.IsPrimaryKey) && !Util.isFalse(item.IsPrimaryKey)) {
276
271
  Util.logger.error(
277
- `- Invalid value for 'IsPrimaryKey' of [${deKey}].[${item.Name}]. Found '${item.IsPrimaryKey}' instead of 'true'/'false'. Removing field from deploy!`
272
+ `- Invalid value for 'IsPrimaryKey' of [${deKey}].[${item.Name}]. Found '${item.IsPrimaryKey}' instead of 'true'/'false'.`
278
273
  );
279
- deployColumns.splice(i, 1);
280
274
  }
281
275
  }
282
276
 
@@ -225,6 +225,11 @@ class Event extends MetadataType {
225
225
  }
226
226
  }
227
227
 
228
+ // filter
229
+ if (!metadata.filterDefinitionId) {
230
+ metadata.filterDefinitionId = '00000000-0000-0000-0000-000000000000';
231
+ }
232
+
228
233
  // automation
229
234
  if (metadata.r__automation_key) {
230
235
  metadata.automationId = cache.searchForField(
@@ -236,8 +241,14 @@ class Event extends MetadataType {
236
241
  if (metadata.arguments) {
237
242
  metadata.arguments.automationId = metadata.automationId;
238
243
  }
239
- delete metadata.arguments.automationId;
244
+ } else if (!metadata.automationId) {
245
+ // if no automation was linked during retrieve we remove the field and hence need to re-set it before deployment
246
+ metadata.automationId = '00000000-0000-0000-0000-000000000000';
247
+ if (metadata.arguments) {
248
+ metadata.arguments.automationId = metadata.automationId;
249
+ }
240
250
  }
251
+
241
252
  // dataExteension
242
253
  // is resolved in createOrUpdate
243
254
 
@@ -529,28 +540,40 @@ class Event extends MetadataType {
529
540
  );
530
541
  }
531
542
 
543
+ // filter
544
+ if (
545
+ metadata?.filterDefinitionId &&
546
+ metadata.filterDefinitionId === '00000000-0000-0000-0000-000000000000'
547
+ ) {
548
+ // if no filter is linked this placeholder value is set which holds no value
549
+ delete metadata.filterDefinitionId;
550
+ }
551
+
532
552
  // automation
533
- try {
534
- if (
535
- metadata?.automationId &&
536
- metadata?.automationId !== '00000000-0000-0000-0000-000000000000'
537
- ) {
538
- metadata.r__automation_key = cache.searchForField(
539
- 'automation',
540
- metadata.automationId,
541
- 'id',
542
- 'key'
543
- );
553
+ if (metadata?.automationId) {
554
+ if (metadata?.automationId === '00000000-0000-0000-0000-000000000000') {
555
+ // if no automation is linked this placeholder value is set which holds no value
544
556
  delete metadata.automationId;
545
- delete metadata.arguments?.automationId;
557
+ } else {
558
+ try {
559
+ metadata.r__automation_key = cache.searchForField(
560
+ 'automation',
561
+ metadata.automationId,
562
+ 'id',
563
+ 'key'
564
+ );
565
+ delete metadata.automationId;
566
+ delete metadata.arguments?.automationId;
567
+ } catch (ex) {
568
+ Util.logger.warn(
569
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
570
+ metadata[this.definition.keyField]
571
+ }): ${ex.message}.`
572
+ );
573
+ }
546
574
  }
547
- } catch (ex) {
548
- Util.logger.warn(
549
- ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
550
- metadata[this.definition.keyField]
551
- }): ${ex.message}.`
552
- );
553
575
  }
576
+
554
577
  // dataExtension
555
578
  try {
556
579
  metadata.r__dataExtension_key = cache.searchForField(