mcdev 7.6.1 → 7.6.3

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 (83) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/.github/workflows/close_issues_on_merge.yml +1 -1
  3. package/.github/workflows/coverage-base-update.yml +2 -2
  4. package/.github/workflows/coverage.yml +1 -1
  5. package/.vscode/extensions.json +1 -0
  6. package/.vscode/settings.json +21 -1
  7. package/@types/lib/index.d.ts +15 -15
  8. package/@types/lib/index.d.ts.map +1 -1
  9. package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
  10. package/@types/lib/metadataTypes/Automation.d.ts +6 -0
  11. package/@types/lib/metadataTypes/Automation.d.ts.map +1 -1
  12. package/@types/lib/metadataTypes/DataExtract.d.ts +7 -0
  13. package/@types/lib/metadataTypes/DataExtract.d.ts.map +1 -1
  14. package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
  15. package/@types/lib/metadataTypes/FileLocation.d.ts +39 -5
  16. package/@types/lib/metadataTypes/FileLocation.d.ts.map +1 -1
  17. package/@types/lib/metadataTypes/Journey.d.ts +13 -3
  18. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  19. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  20. package/@types/lib/metadataTypes/SendClassification.d.ts +7 -0
  21. package/@types/lib/metadataTypes/SendClassification.d.ts.map +1 -1
  22. package/@types/lib/metadataTypes/SenderProfile.d.ts +7 -0
  23. package/@types/lib/metadataTypes/SenderProfile.d.ts.map +1 -1
  24. package/@types/lib/metadataTypes/definitions/Automation.definition.d.ts +6 -0
  25. package/@types/lib/metadataTypes/definitions/FileLocation.definition.d.ts +24 -0
  26. package/@types/lib/util/cache.d.ts +1 -1
  27. package/@types/lib/util/cache.d.ts.map +1 -1
  28. package/@types/lib/util/config.d.ts.map +1 -1
  29. package/@types/lib/util/file.d.ts.map +1 -1
  30. package/LICENSE +1 -1
  31. package/README.md +1 -1
  32. package/boilerplate/config.json +1 -0
  33. package/boilerplate/files/.vscode/extensions.json +1 -0
  34. package/boilerplate/forcedUpdates.json +4 -0
  35. package/lib/index.js +34 -17
  36. package/lib/metadataTypes/Asset.js +31 -8
  37. package/lib/metadataTypes/Automation.js +45 -25
  38. package/lib/metadataTypes/DataExtension.js +1 -1
  39. package/lib/metadataTypes/DataExtract.js +20 -0
  40. package/lib/metadataTypes/Event.js +20 -5
  41. package/lib/metadataTypes/FileLocation.js +43 -5
  42. package/lib/metadataTypes/Journey.js +176 -77
  43. package/lib/metadataTypes/MetadataType.js +22 -9
  44. package/lib/metadataTypes/SendClassification.js +20 -0
  45. package/lib/metadataTypes/SenderProfile.js +20 -0
  46. package/lib/metadataTypes/definitions/Automation.definition.js +6 -0
  47. package/lib/metadataTypes/definitions/FileLocation.definition.js +22 -2
  48. package/lib/util/cache.js +8 -3
  49. package/lib/util/config.js +6 -0
  50. package/lib/util/file.js +17 -14
  51. package/package.json +17 -18
  52. package/prepare-release.js +37 -0
  53. package/test/general.test.js +69 -6
  54. package/test/mockRoot/.mcdevrc.json +2 -1
  55. package/test/mockRoot/deploy/testInstance/testBU/journey/testExisting_journey_Multistep.journey-meta.json +418 -0
  56. package/test/mockRoot/deploy/testInstance/testBU/journey/testExisting_temail.journey-meta.json +212 -0
  57. package/test/mockRoot/deploy/testInstance/testBU/journey/testExisting_temail_notPublished.journey-meta.json +217 -0
  58. package/test/resourceFactory.js +12 -10
  59. package/test/resources/9999999/asset/build-templatebasedemail-expected.json +0 -1
  60. package/test/resources/9999999/asset/retrieve-templatebasedemail-expected.json +0 -1
  61. package/test/resources/9999999/asset/template-templatebasedemail-expected.json +0 -1
  62. package/test/resources/9999999/asset-deploy/block/testNew_asset_badExtension.bad-type-extension.json +39 -0
  63. package/test/resources/9999999/asset-deploy/block/testNew_asset_badName_bad.asset-block-meta.json +39 -0
  64. package/test/resources/9999999/dataExtract/patch-expected.json +3 -1
  65. package/test/resources/9999999/event/get-published-expected.json +30 -0
  66. package/test/resources/9999999/event/post_withExistingDE-callout-expected.json +211 -0
  67. package/test/resources/9999999/event/put-expected.json +12 -11
  68. package/test/resources/9999999/event-deploy/testNew_event_badExtension.bad-type-extension.json +200 -0
  69. package/test/resources/9999999/event-deploy/testNew_event_badName_bad.event-meta.json +200 -0
  70. package/test/resources/9999999/interaction/v1/eventDefinitions/key_DEAudience-2e3c73b6-48cc-2ec0-5522-48636e1a236e/get-response.json +39 -0
  71. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_journey_Multistep/put-response.json +461 -0
  72. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_temail/put-response.json +219 -0
  73. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_temail_notPublished/put-response.json +226 -0
  74. package/test/resources/9999999/journey/get-published-expected.json +217 -0
  75. package/test/resources/9999999/sendClassification/patch-expected.json +3 -1
  76. package/test/resources/9999999/senderProfile/patch-expected.json +12 -8
  77. package/test/resources/9999999/transactionalEmail/get-published-expected.json +20 -0
  78. package/test/type.dataExtract.test.js +1 -1
  79. package/test/type.event.test.js +3 -3
  80. package/test/type.journey.test.js +222 -13
  81. package/test/type.sendClassification.test.js +1 -1
  82. package/test/type.senderProfile.test.js +1 -1
  83. package/test/utils.js +5 -1
@@ -23,6 +23,7 @@ import yoctoSpinner from 'yocto-spinner';
23
23
  * @typedef {import('../../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj
24
24
  * @typedef {import('../../types/mcdev.d.js').SoapRequestParams} SoapRequestParams
25
25
  * @typedef {import('../../types/mcdev.d.js').TemplateMap} TemplateMap
26
+ * @typedef {import('../../types/mcdev.d.js').TypeKeyCombo} TypeKeyCombo
26
27
  */
27
28
 
28
29
  /**
@@ -737,7 +738,10 @@ class Journey extends MetadataType {
737
738
  }
738
739
  }
739
740
  if (linkedTE.options) {
740
- triggeredSend.isTrackingClicks = linkedTE.options.trackLinks;
741
+ triggeredSend.isTrackingClicks =
742
+ linkedTE.options.trackLinks || false;
743
+ triggeredSend.ccEmail = linkedTE.options.cc || '';
744
+ triggeredSend.bccEmail = linkedTE.options.bcc || '';
741
745
  }
742
746
 
743
747
  // send classification
@@ -1293,6 +1297,11 @@ class Journey extends MetadataType {
1293
1297
  break;
1294
1298
  }
1295
1299
  case 'Transactional': {
1300
+ const cachedVersion = cache.getByKey('journey', metadata.key);
1301
+ if (cachedVersion.status === 'Published') {
1302
+ throw new Error(`Cannot update transactional-send journey in Published status`);
1303
+ }
1304
+
1296
1305
  // Transactional Send Journey
1297
1306
  // ~~~ TRIGGERS ~~~~
1298
1307
  // ! journeys so far transactional EMAIL messages. SMS and Push do not create their own journey.
@@ -1319,13 +1328,16 @@ class Journey extends MetadataType {
1319
1328
  activity.configurationArguments.triggeredSendKey =
1320
1329
  activity.configurationArguments.r__transactionalEmail_key;
1321
1330
  } catch (ex) {
1322
- const isCreateMode = !cache.getByKey('journey', metadata.key);
1323
- if (isCreateMode && !Util.OPTIONS.publish) {
1331
+ const isCreateMode = !cachedVersion;
1332
+ if (
1333
+ (isCreateMode && !Util.OPTIONS.publish) ||
1334
+ (!isCreateMode && cachedVersion.status === 'Draft')
1335
+ ) {
1324
1336
  // no need to add a log entry if the publish-option was provided
1325
1337
  Util.logger.info(
1326
1338
  ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
1327
1339
  metadata[this.definition.keyField]
1328
- }): To activate this transactional journey (and create the associated transactionalEmail record), please run 'mcdev publish ${this.buObject.credential}/${this.buObject.businessUnit} journey ${metadata.key}' or click on "Activate" in the GUI.`
1340
+ }): To activate this transactional journey (and create the associated transactionalEmail record), please run 'mcdev publish ${this.buObject.credential}/${this.buObject.businessUnit} -m journey:"${metadata.key}" ' or click on "Activate" in the GUI.`
1329
1341
  );
1330
1342
  } else if (!isCreateMode) {
1331
1343
  // block deployment if we are in update mode
@@ -1357,9 +1369,7 @@ class Journey extends MetadataType {
1357
1369
  default: {
1358
1370
  // it is expected that we'll see 'sms' and 'push' here in the future
1359
1371
  throw new Error(
1360
- ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
1361
- metadata[this.definition.keyField]
1362
- }): channel ${
1372
+ `channel ${
1363
1373
  metadata.channel
1364
1374
  } is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
1365
1375
  );
@@ -1370,9 +1380,7 @@ class Journey extends MetadataType {
1370
1380
  }
1371
1381
  default: {
1372
1382
  throw new Error(
1373
- ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
1374
- metadata[this.definition.keyField]
1375
- }): definitionType ${
1383
+ `definitionType ${
1376
1384
  metadata.definitionType
1377
1385
  } is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
1378
1386
  );
@@ -1834,15 +1842,31 @@ class Journey extends MetadataType {
1834
1842
  * @param {MetadataTypeMap} upsertResults metadata mapped by their keyField as returned by update/create
1835
1843
  */
1836
1844
  static async postDeployTasks(upsertResults) {
1845
+ if (!upsertResults || !Object.keys(upsertResults).length) {
1846
+ // nothing to do. skip here to avoid unnecessary logs / api calls
1847
+ return;
1848
+ }
1849
+ let postDeployFlags = 0;
1850
+ if (Util.OPTIONS.publish) {
1851
+ postDeployFlags++;
1852
+ }
1853
+ if (Util.OPTIONS.validate) {
1854
+ postDeployFlags++;
1855
+ }
1856
+ if (postDeployFlags > 1) {
1857
+ Util.logger.warn(
1858
+ `Please provide only one of the following options (--publish, --validate). Flags are processed in this order and only the first one found is executed.`
1859
+ );
1860
+ }
1861
+
1837
1862
  if (Util.OPTIONS.publish) {
1838
1863
  Util.logger.info(`Publishing: ${this.definition.type}`);
1839
1864
  // pubslih
1840
1865
  const idArr = Object.values(upsertResults).map(
1841
1866
  (item) => 'id:' + item.id + '/' + item.version
1842
1867
  );
1843
- await this.publish(idArr);
1844
- }
1845
- if (Util.OPTIONS.validate) {
1868
+ await this.publish(idArr, upsertResults);
1869
+ } else if (Util.OPTIONS.validate) {
1846
1870
  Util.logger.info(`Validating: ${this.definition.type}`);
1847
1871
  // pubslih
1848
1872
  const idArr = Object.values(upsertResults).map(
@@ -1856,14 +1880,18 @@ class Journey extends MetadataType {
1856
1880
  * a function to publish the journey via API
1857
1881
  *
1858
1882
  * @param {string[]} keyArr keys or ids of the metadata
1883
+ * @param {MetadataTypeMap} [upsertResults] metadata mapped by their keyField as returned by update/create
1859
1884
  * @returns {Promise.<string[]>} Returns list of updated keys/ids that were published. Success could only be seen with a delay in the UI because the publish-endpoint is async
1860
1885
  */
1861
- static async publish(keyArr) {
1886
+ static async publish(keyArr, upsertResults) {
1862
1887
  const resultsTransactional = [];
1863
1888
  // works only with objectId
1864
1889
  const statusUrls = [];
1865
1890
  const executedKeyArr = [];
1866
- const metadataMap = await this.retrieveForCache();
1891
+ const refreshTransactionalKeys = [];
1892
+ const metadataMap = upsertResults
1893
+ ? { metadata: upsertResults }
1894
+ : await this.retrieveForCache();
1867
1895
  const spinnerTransactional = yoctoSpinner({
1868
1896
  text: `Publishing transactional journey…`,
1869
1897
  });
@@ -1927,11 +1955,20 @@ class Journey extends MetadataType {
1927
1955
  }
1928
1956
  if (journey.status === 'Published') {
1929
1957
  // api would return error code 30000 and ask to open a support case when in fact we simply already have a transactionalEmail created based on this status
1930
- Util.logger.error(
1931
- ` ☇ skipping ${this.definition.type} ${
1932
- journey[this.definition.nameField]
1933
- } (${journey[this.definition.keyField]}): already published`
1934
- );
1958
+ if (journey.definitionType === 'Transactional') {
1959
+ Util.logger.info(
1960
+ ` ☇ skipping ${this.definition.type} ${
1961
+ journey[this.definition.nameField]
1962
+ } (${journey[this.definition.keyField]}): already published. Queueing for refresh.`
1963
+ );
1964
+ refreshTransactionalKeys.push(journey.key);
1965
+ } else {
1966
+ Util.logger.warn(
1967
+ ` ☇ skipping ${this.definition.type} ${
1968
+ journey[this.definition.nameField]
1969
+ } (${journey[this.definition.keyField]}): already published.`
1970
+ );
1971
+ }
1935
1972
  continue;
1936
1973
  }
1937
1974
 
@@ -1957,7 +1994,7 @@ class Journey extends MetadataType {
1957
1994
  statusUrls.push({ key, statusUrl: response.statusUrl });
1958
1995
  }
1959
1996
  spinnerTransactional.start();
1960
- return key;
1997
+ return journey[this.definition.keyField];
1961
1998
  } catch (ex) {
1962
1999
  spinnerTransactional.stop();
1963
2000
  if (
@@ -2006,6 +2043,11 @@ class Journey extends MetadataType {
2006
2043
  } else {
2007
2044
  throw new Error(response);
2008
2045
  }
2046
+ if (Util.OPTIONS.skipStatusCheck) {
2047
+ Util.logger.warn(
2048
+ ` - Skipping status check for publishing journey ${key} due to --skipStatusCheck flag`
2049
+ );
2050
+ }
2009
2051
  if (!Util.OPTIONS.skipStatusCheck && statusUrl) {
2010
2052
  const spinner = yoctoSpinner({
2011
2053
  text: `Publishing journey…`,
@@ -2064,6 +2106,10 @@ class Journey extends MetadataType {
2064
2106
  }
2065
2107
  } // for loop
2066
2108
 
2109
+ const publishedJourneyCounter = {
2110
+ multiStep: executedKeyArr.filter(Boolean).length,
2111
+ transactional: 0,
2112
+ };
2067
2113
  // Transactional Send Journeys
2068
2114
  if (resultsTransactional.length) {
2069
2115
  const transactionalKeyArr = (await Promise.all(resultsTransactional)).filter(Boolean);
@@ -2072,56 +2118,103 @@ class Journey extends MetadataType {
2072
2118
  // if all publish actions failed, we don't need to re-retrieve anything here
2073
2119
  if (transactionalKeyArr.length) {
2074
2120
  executedKeyArr.push(...transactionalKeyArr);
2121
+ publishedJourneyCounter.transactional = transactionalKeyArr.length;
2122
+ // reset transactionalEmail cache to trigger re-caching it.
2123
+ cache.clearCache(this.buObject.mid, 'transactionalEmail');
2124
+ }
2125
+ }
2075
2126
 
2076
- Util.logger.info('Retrieving relevant journeys');
2077
- const retriever = new Retriever(this.properties, this.buObject);
2127
+ // reload published journeys including their events/transactionalEmails
2128
+ await this._reRetrieve(
2129
+ executedKeyArr,
2130
+ publishedJourneyCounter.transactional,
2131
+ publishedJourneyCounter.multiStep
2132
+ );
2078
2133
 
2079
- try {
2080
- const updatedJourneyRetrieve = await retriever.retrieve(
2081
- ['journey'],
2082
- transactionalKeyArr
2134
+ Util.logger.info(
2135
+ `Published ${executedKeyArr.filter(Boolean).length} of ${keyArr.length} items`
2136
+ );
2137
+
2138
+ if (refreshTransactionalKeys.length) {
2139
+ // in case we tried to publish a transactional journey that was already published we will instead run a refresh for those
2140
+ executedKeyArr.push(...(await this.refresh(refreshTransactionalKeys)));
2141
+ }
2142
+ return executedKeyArr.filter(Boolean);
2143
+ }
2144
+
2145
+ /**
2146
+ *
2147
+ * @param {string[]} executedKeyArr list of journey keys
2148
+ * @param {number} transactionalCounter how many transactiona-send journeys did we expect to refresh
2149
+ * @param {number} multiStepCounter how many multi-step journeys did we expect to refresh
2150
+ * @returns {Promise.<void>} -
2151
+ */
2152
+ static async _reRetrieve(executedKeyArr, transactionalCounter, multiStepCounter) {
2153
+ if (!executedKeyArr.filter(Boolean).length) {
2154
+ return;
2155
+ }
2156
+ Util.logger.info('Re-retrieving published journeys');
2157
+ const retriever = new Retriever(this.properties, this.buObject);
2158
+ try {
2159
+ // we need to retrieve the updated journeys and all dependencies
2160
+ const updatedJourneyRetrieve = await retriever.retrieve(
2161
+ ['journey'],
2162
+ executedKeyArr.filter(Boolean)
2163
+ );
2164
+
2165
+ /** @type {MetadataTypeItem[]} */
2166
+ const updatedJourneys =
2167
+ updatedJourneyRetrieve?.journey?.length > 1
2168
+ ? Object.values(
2169
+ updatedJourneyRetrieve?.journey.reduce(
2170
+ (previousValue, currentValue) =>
2171
+ Object.assign(previousValue, currentValue),
2172
+ {}
2173
+ )
2174
+ )
2175
+ : Object.values(updatedJourneyRetrieve?.journey[0]);
2176
+
2177
+ if (updatedJourneys) {
2178
+ // regardless of upsert vs publish-only mode, we need to retrieve the events/transactionalEmail and their dependencies
2179
+ const updatedEvents = [];
2180
+ const updatedTransactionalEmails = [];
2181
+ for (const journey of updatedJourneys) {
2182
+ // multi-step journeys
2183
+ updatedEvents.push(journey.triggers?.[0]?.metaData?.r__event_key);
2184
+ // transactional-send journeys
2185
+ updatedTransactionalEmails.push(
2186
+ journey.activities?.[0]?.configurationArguments?.r__transactionalEmail_key
2083
2187
  );
2188
+ }
2189
+ /** @type {TypeKeyCombo} */
2190
+ const eventTransEmailCombo = {};
2191
+ if (updatedEvents.filter(Boolean).length) {
2192
+ eventTransEmailCombo.event = updatedEvents.filter(Boolean);
2193
+ } else if (multiStepCounter) {
2194
+ Util.logger.error(`Could not find events for the published journeys`);
2195
+ }
2196
+ if (updatedTransactionalEmails.filter(Boolean).length) {
2197
+ eventTransEmailCombo.transactionalEmail =
2198
+ updatedTransactionalEmails.filter(Boolean);
2199
+ Util.logger.info('Retrieving relevant transactionalEmails');
2200
+ } else if (transactionalCounter) {
2201
+ Util.logger.error(
2202
+ `Could not find transactional Emails for the published journeys`
2203
+ );
2204
+ }
2084
2205
 
2085
- /** @type {MetadataTypeItem[]} */
2086
- const updatedJourneys =
2087
- updatedJourneyRetrieve?.journey?.length > 1
2088
- ? Object.values(
2089
- updatedJourneyRetrieve?.journey.reduce(
2090
- (previousValue, currentValue) =>
2091
- Object.assign(previousValue, currentValue),
2092
- {}
2093
- )
2094
- )
2095
- : Object.values(updatedJourneyRetrieve?.journey[0]);
2096
- if (updatedJourneys) {
2097
- const updatedTransactionalEmails = [];
2098
- for (const journey of updatedJourneys) {
2099
- updatedTransactionalEmails.push(
2100
- journey.activities?.[0]?.configurationArguments
2101
- ?.r__transactionalEmail_key
2102
- );
2103
- }
2104
- if (updatedTransactionalEmails.filter(Boolean).length) {
2105
- Util.logger.info('Retrieving relevant transactionalEmails');
2106
- await retriever.retrieve(
2107
- ['transactionalEmail'],
2108
- updatedTransactionalEmails.filter(Boolean)
2109
- );
2110
- } else {
2111
- Util.logger.error(
2112
- `Could not find transactional Emails for the published journeys`
2113
- );
2114
- }
2115
- }
2116
- } catch (ex) {
2117
- Util.logger.errorStack(ex, 'retrieve failed');
2206
+ const toBeRetrievedTypes = Object.keys(eventTransEmailCombo);
2207
+ if (toBeRetrievedTypes.length) {
2208
+ Util.logger.info(
2209
+ 'Retrieving relevant ' +
2210
+ toBeRetrievedTypes.map((item) => item + 's').join(', ')
2211
+ );
2212
+ await retriever.retrieve(toBeRetrievedTypes, eventTransEmailCombo);
2118
2213
  }
2119
2214
  }
2215
+ } catch (ex) {
2216
+ Util.logger.errorStack(ex, 'retrieve failed');
2120
2217
  }
2121
- Util.logger.info(
2122
- `Published ${executedKeyArr.filter(Boolean).length} of ${keyArr.length} items`
2123
- );
2124
- return executedKeyArr.filter(Boolean);
2125
2218
  }
2126
2219
 
2127
2220
  /**
@@ -2675,31 +2768,28 @@ class Journey extends MetadataType {
2675
2768
  * TSD-specific refresh method that finds active TSDs and refreshes them
2676
2769
  *
2677
2770
  * @param {string[]} keyArr metadata keys
2678
- * @param {boolean} [checkKey] whether to check if the key is valid
2679
2771
  * @returns {Promise.<string[]>} Returns list of keys that were refreshed
2680
2772
  */
2681
- static async refresh(keyArr, checkKey = true) {
2773
+ static async refresh(keyArr) {
2682
2774
  console.time('Time'); // eslint-disable-line no-console
2683
- if (!keyArr) {
2775
+ if (!Array.isArray(keyArr) || !keyArr.length) {
2684
2776
  Util.logger.error('No refresh-keys provided');
2685
2777
  return [];
2686
2778
  // keyArr = await this.getKeysForValidTSDs((await this.findRefreshableItems()).metadata);
2687
2779
  // checkKey = false;
2688
2780
  }
2689
- let journeyCache;
2690
- if (checkKey) {
2691
- journeyCache = await this.retrieveForCache();
2692
- }
2781
+ const journeyCache = await this.retrieveForCache();
2693
2782
  // then executes pause, publish, start on them.
2694
2783
  Util.logger.info(`Refreshing ${keyArr.length} ${this.definition.typeName}...`);
2695
2784
  Util.logger.debug(`Refreshing keys: ${keyArr.join(', ')}`);
2696
2785
  const refreshedKeyArr = [];
2697
2786
  const tsKeys = [];
2698
2787
  const rateLimit = pLimit(10);
2788
+ const transactionalJourneyKeys = [];
2699
2789
  await Promise.all(
2700
2790
  keyArr.map((key) =>
2701
2791
  rateLimit(async () => {
2702
- if (checkKey && !journeyCache.metadata[key]) {
2792
+ if (!journeyCache.metadata[key]) {
2703
2793
  Util.logger.error(
2704
2794
  ` ☇ skipping refresh of ${this.definition.type} ${key}: not found on server`
2705
2795
  );
@@ -2707,15 +2797,16 @@ class Journey extends MetadataType {
2707
2797
  }
2708
2798
  switch (journeyCache.metadata[key].definitionType) {
2709
2799
  case 'Transactional': {
2710
- if (checkKey && journeyCache.metadata[key]?.status !== 'Published') {
2711
- Util.logger.error(
2712
- ` ☇ skipping refresh of ${this.definition.type} ${key}: Can only refresh journeys with status 'Published'. Found status: ${journeyCache.metadata[key]?.status}`
2713
- );
2714
- } else {
2800
+ if (journeyCache.metadata[key]?.status === 'Published') {
2715
2801
  const result = await this._refreshItem(key, journeyCache);
2716
2802
  if (result) {
2717
2803
  refreshedKeyArr.push(key);
2804
+ transactionalJourneyKeys.push(key);
2718
2805
  }
2806
+ } else {
2807
+ Util.logger.error(
2808
+ ` ☇ skipping refresh of ${this.definition.type} ${key}: Can only refresh journeys with status 'Published'. Found status: ${journeyCache.metadata[key]?.status}`
2809
+ );
2719
2810
  }
2720
2811
  break;
2721
2812
  }
@@ -2804,6 +2895,14 @@ class Journey extends MetadataType {
2804
2895
  Util.logger.info(Util.getGrayMsg('No triggeredSends found to refresh'));
2805
2896
  }
2806
2897
 
2898
+ // reload refreshed transactional journeys including their transactionalEmails
2899
+ if (transactionalJourneyKeys.length) {
2900
+ // reset transactionalEmail cache to trigger re-caching it.
2901
+ cache.clearCache(this.buObject.mid, 'transactionalEmail');
2902
+
2903
+ await this._reRetrieve(transactionalJourneyKeys, transactionalJourneyKeys.length, 0);
2904
+ }
2905
+
2807
2906
  Util.logger.info(
2808
2907
  `Refreshed ${refreshedKeyArr.length} of ${keyArr.length} ${this.definition.type}`
2809
2908
  );
@@ -69,21 +69,34 @@ class MetadataType {
69
69
  try {
70
70
  if (fileName.endsWith('.json')) {
71
71
  const fileContent = await File.readJSONFile(dir, fileName, false);
72
- const fileNameWithoutEnding = File.reverseFilterIllegalFilenames(
73
- fileName.split(/\.(\w|-)+-meta.json/)[0]
74
- );
75
- // We always store the filename using the External Key (CustomerKey or key) to avoid duplicate names.
76
- // to ensure any changes are done to both the filename and external key do a check here
77
72
  // ! convert numbers to string to allow numeric keys to be checked properly
78
73
  const key = Number.isInteger(fileContent[this.definition.keyField])
79
74
  ? fileContent[this.definition.keyField].toString()
80
75
  : fileContent[this.definition.keyField];
81
- if (key === fileNameWithoutEnding || listBadKeys) {
82
- fileName2FileContent[fileNameWithoutEnding] = fileContent;
76
+
77
+ // ensure filename includes extended metadata extension
78
+ const regex = new RegExp(/\.(\w|-)+-meta.json/);
79
+ const errorDir = dir.split('\\').join('/');
80
+ if (regex.test(fileName)) {
81
+ const fileNameWithoutEnding = File.reverseFilterIllegalFilenames(
82
+ fileName.split(regex)[0]
83
+ );
84
+ // We always store the filename using the External Key (CustomerKey or key) to avoid duplicate names.
85
+ // to ensure any changes are done to both the filename and external key do a check here
86
+ if (key === fileNameWithoutEnding || listBadKeys) {
87
+ fileName2FileContent[fileNameWithoutEnding] = fileContent;
88
+ } else {
89
+ Util.logger.error(
90
+ ` ☇ skipping ${this.definition.type} ${key}: Name of the metadata file and the JSON-key (${this.definition.keyField}) must match. Expected: ${key}.${this.definition.type}-meta.json. Actual: ` +
91
+ Util.getGrayMsg(`${errorDir}/`) +
92
+ fileName
93
+ );
94
+ }
83
95
  } else {
84
96
  Util.logger.error(
85
- ` ${this.definition.type} ${key}: Name of the metadata file and the JSON-key (${this.definition.keyField}) must match` +
86
- Util.getGrayMsg(` - ${dir}/${fileName}`)
97
+ ` ☇ skipping ${this.definition.type} ${key}: Name of the metadata file must end on the extended metadata suffix. Expected: ${key}.${this.definition.type}-meta.json. Actual: ` +
98
+ Util.getGrayMsg(`${errorDir}/`) +
99
+ fileName
87
100
  );
88
101
  }
89
102
  }
@@ -131,6 +131,26 @@ class SendClassification extends MetadataType {
131
131
  return metadata;
132
132
  }
133
133
 
134
+ /**
135
+ * Gets executed after deployment of metadata type
136
+ *
137
+ * @param {MetadataTypeMap} upsertResults metadata mapped by their keyField as returned by update/create
138
+ * @returns {Promise.<void>} -
139
+ */
140
+ static async postDeployTasks(upsertResults) {
141
+ // re-retrieve all upserted items to ensure we have all fields (createdDate and modifiedDate are otherwise not present)
142
+ Util.logger.debug(
143
+ `Caching all ${this.definition.type} post-deploy to ensure we have all fields`
144
+ );
145
+ const typeCache = await this.retrieveForCache();
146
+ // update values in upsertResults with retrieved values before saving to disk
147
+ for (const key of Object.keys(upsertResults)) {
148
+ if (typeCache.metadata[key]) {
149
+ upsertResults[key] = typeCache.metadata[key];
150
+ }
151
+ }
152
+ }
153
+
134
154
  /**
135
155
  * manages post retrieve steps
136
156
  *
@@ -162,6 +162,26 @@ class SenderProfile extends MetadataType {
162
162
  return metadata;
163
163
  }
164
164
 
165
+ /**
166
+ * Gets executed after deployment of metadata type
167
+ *
168
+ * @param {MetadataTypeMap} upsertResults metadata mapped by their keyField as returned by update/create
169
+ * @returns {Promise.<void>} -
170
+ */
171
+ static async postDeployTasks(upsertResults) {
172
+ // re-retrieve all upserted items to ensure we have all fields (createdDate and modifiedDate are otherwise not present)
173
+ Util.logger.debug(
174
+ `Caching all ${this.definition.type} post-deploy to ensure we have all fields`
175
+ );
176
+ const typeCache = await this.retrieveForCache();
177
+ // update values in upsertResults with retrieved values before saving to disk
178
+ for (const key of Object.keys(upsertResults)) {
179
+ if (typeCache.metadata[key]) {
180
+ upsertResults[key] = typeCache.metadata[key];
181
+ }
182
+ }
183
+ }
184
+
165
185
  /**
166
186
  *
167
187
  * @param {MetadataTypeItem} item single metadata item
@@ -78,6 +78,12 @@ export default {
78
78
  Scheduled: 6,
79
79
  Stopped: 5,
80
80
  },
81
+ fileNameOperatorMapping: {
82
+ Equals: 4,
83
+ Contains: 1,
84
+ 'Begins with': 2,
85
+ 'Ends with': 3,
86
+ },
81
87
  timeZoneMapping: {
82
88
  // bugs in SFMC timezones:
83
89
  // * Yerevan GMT+4 always is changing to 'Caucasus Standard Time' GMT+4, so no id 29
@@ -17,6 +17,20 @@ export default {
17
17
  'Used for export or import of files to/from Marketing Cloud. Previously this was labeled ftpLocation.',
18
18
  typeRetrieveByDefault: true,
19
19
  typeName: 'File Location',
20
+ locationTypeMapping: {
21
+ 'Enhanced FTP Site Import Directory': 0,
22
+ 'External FTP Site': 1,
23
+ 'External SFTP Site': 2,
24
+ 'External FTPS Site': 3,
25
+ 'Salesforce Objects and Reports': 4,
26
+ Safehouse: 5,
27
+ 'Enhanced FTP Site Export Directory': 6,
28
+ 'Legacy Import Directory': 8,
29
+ 'Relative location under FTP Site': 9,
30
+ 'Amazon Simple Storage Service': 13,
31
+ 'Azure Blob Storage': 15,
32
+ 'Google Cloud Storage': 16,
33
+ },
20
34
  fields: {
21
35
  id: {
22
36
  isCreateable: false,
@@ -34,13 +48,13 @@ export default {
34
48
  isCreateable: false,
35
49
  isUpdateable: false,
36
50
  retrieving: true,
37
- template: false,
51
+ template: true,
38
52
  },
39
53
  name: {
40
54
  isCreateable: false,
41
55
  isUpdateable: false,
42
56
  retrieving: true,
43
- template: false,
57
+ template: true,
44
58
  },
45
59
  relPath: {
46
60
  isCreateable: true,
@@ -48,5 +62,11 @@ export default {
48
62
  retrieving: true,
49
63
  template: true,
50
64
  },
65
+ c__locationType: {
66
+ isCreateable: false,
67
+ isUpdateable: false,
68
+ retrieving: true,
69
+ template: true,
70
+ },
51
71
  },
52
72
  };
package/lib/util/cache.js CHANGED
@@ -60,12 +60,17 @@ export default {
60
60
  /**
61
61
  * clean cache for one BU if mid provided, otherwise whole cache
62
62
  *
63
- * @param {number} [mid] of business unit
63
+ * @param {number} [mid] limit clearing to provided business unit MID
64
+ * @param {string} [type] optionally limit clearing to specific metadata type; only used if mid is provided
64
65
  * @returns {void}
65
66
  */
66
- clearCache: (mid) =>
67
+ clearCache: (mid, type) =>
67
68
  mid
68
- ? Object.keys(dataStore[mid]).forEach((key) => delete dataStore[mid][key])
69
+ ? Object.keys(dataStore[mid]).forEach((key) => {
70
+ if (!type || type === key) {
71
+ delete dataStore[mid][key];
72
+ }
73
+ })
69
74
  : Object.keys(dataStore).forEach((key) => delete dataStore[key]),
70
75
  /* eslint-enable unicorn/no-array-for-each */
71
76
 
@@ -127,6 +127,10 @@ const config = {
127
127
  `Your Accenture SFMC DevTools version ${Util.packageJsonMcdev.version} is lower than your project's config version ${properties.version}`
128
128
  );
129
129
  if (Util.skipInteraction) {
130
+ // print guidance for extension users
131
+ Util.logger.error(
132
+ `Run 'npm update -g mcdev@${properties.version}' now to fix this.`
133
+ );
130
134
  return false;
131
135
  }
132
136
 
@@ -277,6 +281,8 @@ const config = {
277
281
  ].join('\n- ')
278
282
  );
279
283
  if (Util.skipInteraction) {
284
+ // print guidance for extension users
285
+ Util.logger.error(`Run 'mcdev upgrade' now to fix this.`);
280
286
  return false;
281
287
  }
282
288
  const runUpgradeNow = await confirm({
package/lib/util/file.js CHANGED
@@ -270,6 +270,7 @@ const File = {
270
270
  * @returns {Promise.<string>} original string on error; formatted string on success
271
271
  */
272
272
  _beautify_prettier: async function (directory, filename, filetype, content) {
273
+ const properties = await config.getProperties();
273
274
  let formatted = '';
274
275
  try {
275
276
  if (!FileFs.prettierConfig) {
@@ -337,20 +338,22 @@ const File = {
337
338
 
338
339
  formatted = await prettier.format(content, FileFs.prettierConfig);
339
340
  } catch (ex) {
340
- // save prettier errror into log file
341
- // Note: we have to filter color codes from prettier's error message before saving it to file
342
- /* eslint-disable no-control-regex */
343
- this.writeToFile(
344
- directory,
345
- filename + '.error',
346
- 'log',
347
- `Error Log\nParser: ${FileFs.prettierConfig.parser}\n${ex.message.replaceAll(
348
- /[\u001B\u009B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
349
-
350
- ''
351
- )}`
352
- );
353
- /* eslint-enable no-control-regex */
341
+ if (properties.options.formatErrorLog) {
342
+ // save prettier errror into log file
343
+ // Note: we have to filter color codes from prettier's error message before saving it to file
344
+ /* eslint-disable no-control-regex */
345
+ this.writeToFile(
346
+ directory,
347
+ filename + '.error',
348
+ 'log',
349
+ `Error Log\nParser: ${FileFs.prettierConfig.parser}\n${ex.message.replaceAll(
350
+ /[\u001B\u009B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
351
+
352
+ ''
353
+ )}`
354
+ );
355
+ /* eslint-enable no-control-regex */
356
+ }
354
357
 
355
358
  formatted = content;
356
359
  }