mcdev 7.3.0 → 7.4.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 (48) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/.github/PULL_REQUEST_TEMPLATE/pr_template_release.md +3 -3
  3. package/.vscode/settings.json +1 -1
  4. package/@types/lib/index.d.ts.map +1 -1
  5. package/@types/lib/metadataTypes/Asset.d.ts +25 -0
  6. package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
  7. package/@types/lib/metadataTypes/DataExtension.d.ts +3 -2
  8. package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
  9. package/@types/lib/metadataTypes/DataExtensionField.d.ts.map +1 -1
  10. package/@types/lib/metadataTypes/Event.d.ts +33 -0
  11. package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
  12. package/@types/lib/metadataTypes/Journey.d.ts +4 -2
  13. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  14. package/@types/lib/metadataTypes/MetadataType.d.ts +22 -3
  15. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  16. package/@types/lib/metadataTypes/Role.d.ts +7 -0
  17. package/@types/lib/metadataTypes/Role.d.ts.map +1 -1
  18. package/@types/lib/util/init.config.d.ts.map +1 -1
  19. package/@types/lib/util/replaceContentBlockReference.d.ts +4 -5
  20. package/@types/lib/util/replaceContentBlockReference.d.ts.map +1 -1
  21. package/@types/lib/util/util.d.ts +11 -2
  22. package/@types/lib/util/util.d.ts.map +1 -1
  23. package/@types/lib/util/validations.d.ts +9 -0
  24. package/@types/lib/util/validations.d.ts.map +1 -0
  25. package/boilerplate/config.json +22 -0
  26. package/boilerplate/files/.gitattributes +1 -1
  27. package/boilerplate/files/.vscode/settings.json +3 -2
  28. package/boilerplate/files/README.md +1 -1
  29. package/boilerplate/forcedUpdates.json +8 -0
  30. package/lib/cli.js +28 -3
  31. package/lib/index.js +3 -2
  32. package/lib/metadataTypes/Asset.js +87 -7
  33. package/lib/metadataTypes/DataExtension.js +74 -4
  34. package/lib/metadataTypes/DataExtensionField.js +7 -1
  35. package/lib/metadataTypes/Event.js +254 -200
  36. package/lib/metadataTypes/Journey.js +242 -132
  37. package/lib/metadataTypes/MetadataType.js +182 -37
  38. package/lib/metadataTypes/Role.js +47 -35
  39. package/lib/util/init.config.js +10 -6
  40. package/lib/util/replaceContentBlockReference.js +15 -11
  41. package/lib/util/util.js +29 -9
  42. package/lib/util/validations.js +66 -0
  43. package/package.json +7 -2
  44. package/test/general.test.js +5 -2
  45. package/test/mockRoot/.mcdevrc.json +15 -1
  46. package/test/type.journey.test.js +11 -11
  47. package/test/utils.js +1 -0
  48. /package/test/resources/9999999/interaction/v1/interactions/publishAsync/{3c3f4112-9b43-43ca-8a89-aa0375b2c1a2 → 0175b971-71a3-4d8e-98ac-48121f3fbf4f}/post-response.json +0 -0
package/lib/cli.js CHANGED
@@ -163,7 +163,14 @@ yargs(hideBin(process.argv))
163
163
  group: 'Options for deploy:',
164
164
  describe:
165
165
  'Some metadata types allow updating resources despite a key mismatch by matching the name. That avoids clean-ups on all BUs but instead allows you to continously get higher environmetns into a better shape.',
166
+ })
167
+ .option('skipValidation', {
168
+ alias: 'sv',
169
+ group: 'Options for deploy:',
170
+ describe:
171
+ 'allows reducing validation rules from error to warn to handle edge cases',
166
172
  }),
173
+
167
174
  (argv) => {
168
175
  Mcdev.setOptions(argv);
169
176
 
@@ -395,6 +402,12 @@ yargs(hideBin(process.argv))
395
402
  group: 'Options for build:',
396
403
  describe:
397
404
  're-retrieves potentially relevant metadata before running buildTemplate (all if --dependencies is used)',
405
+ })
406
+ .option('skipValidation', {
407
+ alias: 'sv',
408
+ group: 'Options for build:',
409
+ describe:
410
+ 'allows reducing validation rules from error to warn to handle edge cases',
398
411
  }),
399
412
  (argv) => {
400
413
  Mcdev.setOptions(argv);
@@ -521,6 +534,12 @@ yargs(hideBin(process.argv))
521
534
  group: 'Options for buildDefinition:',
522
535
  describe: 'market used for building deployable definition',
523
536
  })
537
+ .option('skipValidation', {
538
+ alias: 'sv',
539
+ group: 'Options for buildDefinition:',
540
+ describe:
541
+ 'allows reducing validation rules from error to warn to handle edge cases',
542
+ })
524
543
  .check((argv) => {
525
544
  if (!argv.MARKET && !argv.market) {
526
545
  throw new Error(
@@ -572,6 +591,12 @@ yargs(hideBin(process.argv))
572
591
  alias: 'm',
573
592
  group: 'Options for buildDefinitionBulk:',
574
593
  describe: 'type:templateName combos to build template for',
594
+ })
595
+ .option('skipValidation', {
596
+ alias: 'sv',
597
+ group: 'Options for deploy:',
598
+ describe:
599
+ 'allows reducing validation rules from error to warn to handle edge cases',
575
600
  }),
576
601
  (argv) => {
577
602
  Mcdev.setOptions(argv);
@@ -679,8 +704,8 @@ yargs(hideBin(process.argv))
679
704
  }
680
705
  )
681
706
  .command(
682
- ['execute <BU> [TYPE] [KEY]', 'exec', 'start'],
683
- 'executes the entity (query/journey/automation etc.)',
707
+ ['execute <BU> [TYPE] [KEY]', 'exec', 'start', 'resume'],
708
+ 'executes the entity',
684
709
  (yargs) =>
685
710
  yargs
686
711
  .positional('BU', {
@@ -733,7 +758,7 @@ yargs(hideBin(process.argv))
733
758
  }
734
759
  )
735
760
  .command(
736
- ['publish <BU> [TYPE] [KEY]'],
761
+ ['publish <BU> [TYPE] [KEY]', 'activate'],
737
762
  'publishes the entity',
738
763
  (yargs) =>
739
764
  yargs
package/lib/index.js CHANGED
@@ -113,6 +113,7 @@ class Mcdev {
113
113
  'skipInteraction',
114
114
  'skipRetrieve',
115
115
  'skipStatusCheck',
116
+ 'skipValidation',
116
117
  '_runningTest',
117
118
  '_welcomeMessageShown',
118
119
  ];
@@ -939,7 +940,7 @@ class Mcdev {
939
940
  'Searching for additional dependencies that were linked via ContentBlockByKey, ContentBlockByName and ContentBlockById'
940
941
  );
941
942
 
942
- await ReplaceContentBlockReference.createCacheMap(properties, buObject, true);
943
+ await ReplaceContentBlockReference.createCache(properties, buObject, true);
943
944
 
944
945
  // because we re-use the replaceReference logic here we need to manually set this value
945
946
  /** @type {ContentBlockConversionTypes[]} */
@@ -2073,7 +2074,7 @@ class Mcdev {
2073
2074
  buObject.businessUnit,
2074
2075
  ]);
2075
2076
 
2076
- await ReplaceContentBlockReference.createCacheMap(properties, buObject);
2077
+ await ReplaceContentBlockReference.createCache(properties, buObject);
2077
2078
 
2078
2079
  try {
2079
2080
  let metadataMap;
@@ -9,6 +9,7 @@ import cache from '../util/cache.js';
9
9
  import TriggeredSend from './TriggeredSend.js';
10
10
  import Folder from './Folder.js';
11
11
  import ReplaceCbReference from '../util/replaceContentBlockReference.js';
12
+ import toposort from 'toposort';
12
13
 
13
14
  /**
14
15
  * @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
@@ -27,6 +28,7 @@ import ReplaceCbReference from '../util/replaceContentBlockReference.js';
27
28
  * @typedef {import('../../types/mcdev.d.js').AssetMap} AssetMap
28
29
  * @typedef {import('../../types/mcdev.d.js').AssetItem} AssetItem
29
30
  * @typedef {import('../../types/mcdev.d.js').AssetRequestParams} AssetRequestParams
31
+ * @typedef {import('../../types/mcdev.d.js').ContentBlockConversionTypes} ContentBlockConversionTypes
30
32
  */
31
33
 
32
34
  /**
@@ -185,6 +187,81 @@ class Asset extends MetadataType {
185
187
  /* eslint-enable unicorn/prefer-ternary */
186
188
  }
187
189
 
190
+ /**
191
+ * Returns Order in which metadata needs to be retrieved/deployed
192
+ *
193
+ * @param {AssetMap} metadataMap metadata mapped by their keyField
194
+ * @param {string} deployDir directory where deploy metadata are saved
195
+ * @returns {Promise.<AssetMap>} keyField => metadata map but sorted to ensure dependencies are deployed in correct order
196
+ */
197
+ static async _getUpsertOrder(metadataMap, deployDir) {
198
+ /**
199
+ * one entry for each dependency with the first item being the key thats required by the second item
200
+ *
201
+ * @type {string[][]}
202
+ */
203
+ const dependencies = [];
204
+ /** @type {ContentBlockConversionTypes[]} */
205
+ Util.OPTIONS.referenceFrom = ['key', 'name', 'id'];
206
+ /** @type {ContentBlockConversionTypes} */
207
+ Util.OPTIONS.referenceTo = 'key';
208
+ // loop through all metadata types which are being retrieved/deployed
209
+ for (const key in metadataMap) {
210
+ const findAssetKeys = new Set();
211
+ try {
212
+ // find asset references in code
213
+ await this.replaceCbReference(metadataMap[key], deployDir, findAssetKeys);
214
+ // find asset references in metadata
215
+ const findAssetKeyMeta = Util.findLeafVals(metadataMap[key], 'r__asset_key');
216
+ for (const metaKey of findAssetKeyMeta) {
217
+ findAssetKeys.add(metaKey);
218
+ }
219
+
220
+ const dependentKeys = [...findAssetKeys];
221
+ if (dependentKeys.length > 0) {
222
+ dependencies.push(...dependentKeys.map((depKey) => [depKey, key]));
223
+ } else {
224
+ Util.logger.debug('Asset._getUpsertOrder: this case should not happen');
225
+ dependencies.push([undefined, key]);
226
+ }
227
+ } catch (ex) {
228
+ if (ex.code !== 200) {
229
+ Util.logger.errorStack(ex, 'Cannot find related code blocks for ' + key);
230
+ }
231
+ // no dependent keys found
232
+ dependencies.push([undefined, key]);
233
+ }
234
+ }
235
+
236
+ // sort list & remove the undefined dependencies
237
+ const flatList = toposort(dependencies).filter((a) => !!a);
238
+
239
+ /** @type {AssetMap} */
240
+ const metadataTypeMapSorted = {};
241
+ // group subtypes per type
242
+ for (const key of flatList) {
243
+ metadataTypeMapSorted[key] = metadataMap[key];
244
+ }
245
+ return metadataTypeMapSorted;
246
+ }
247
+
248
+ /**
249
+ * MetadataType upsert, after retrieving from target and comparing to check if create or update operation is needed.
250
+ *
251
+ * @param {AssetMap} metadataMap metadata mapped by their keyField
252
+ * @param {string} deployDir directory where deploy metadata are saved
253
+ * @returns {Promise.<AssetMap>} keyField => metadata map
254
+ */
255
+ static async upsert(metadataMap, deployDir) {
256
+ if (Object.keys(metadataMap).length > 1) {
257
+ // fill the cache map with our deployment package to ensure we can find
258
+ ReplaceCbReference.createCacheForMap(metadataMap);
259
+ // assets can link to other assets (via template, content block reference and SSJS/AMPscript) and deployment would fail if we did not sort this here
260
+ metadataMap = await this._getUpsertOrder(metadataMap, deployDir);
261
+ }
262
+ return super.upsert(metadataMap, deployDir, true);
263
+ }
264
+
188
265
  /**
189
266
  * Creates a single asset
190
267
  *
@@ -1037,8 +1114,9 @@ class Asset extends MetadataType {
1037
1114
  * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
1038
1115
  *
1039
1116
  * @param {MetadataTypeItem} metadata a single script activity definition
1117
+ * @param {boolean} [hideWarning] when checking content blocks we do want to set the folder path but if we cant, lets not cludder the log with warnings about it
1040
1118
  */
1041
- static setFolderPath(metadata) {
1119
+ static setFolderPath(metadata, hideWarning = false) {
1042
1120
  try {
1043
1121
  metadata.r__folder_Path = cache.searchForField(
1044
1122
  'folder',
@@ -1048,11 +1126,13 @@ class Asset extends MetadataType {
1048
1126
  );
1049
1127
  delete metadata.category;
1050
1128
  } catch (ex) {
1051
- Util.logger.warn(
1052
- ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
1053
- metadata[this.definition.keyField]
1054
- }): Could not find folder (${ex.message})`
1055
- );
1129
+ if (!hideWarning) {
1130
+ Util.logger.warn(
1131
+ ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
1132
+ metadata[this.definition.keyField]
1133
+ }): Could not find folder (${ex.message})`
1134
+ );
1135
+ }
1056
1136
  }
1057
1137
  }
1058
1138
 
@@ -1827,7 +1907,7 @@ class Asset extends MetadataType {
1827
1907
  }
1828
1908
  }
1829
1909
  if (asset.category?.id) {
1830
- this.setFolderPath(asset);
1910
+ this.setFolderPath(asset, true);
1831
1911
  }
1832
1912
 
1833
1913
  if (asset.slots) {
@@ -42,9 +42,10 @@ class DataExtension extends MetadataType {
42
42
  * if create or update operation is needed.
43
43
  *
44
44
  * @param {DataExtensionMap} metadataMap dataExtensions mapped by their customerKey
45
- * @returns {Promise} Promise
45
+ * @param {string} deployDir directory where deploy metadata are saved
46
+ * @returns {Promise.<MetadataTypeMap>} keyField => metadata map
46
47
  */
47
- static async upsert(metadataMap) {
48
+ static async upsert(metadataMap, deployDir) {
48
49
  // get existing DE-fields for DE-keys in deployment package to properly handle add/update/delete of fields
49
50
  // we need to use IN here because it would fail otherwise if we try to deploy too many DEs at the same time
50
51
  /** @type {SoapRequestParams} */
@@ -66,6 +67,11 @@ class DataExtension extends MetadataType {
66
67
  for (const metadataKey in metadataMap) {
67
68
  try {
68
69
  metadataMap[metadataKey] = await this.preDeployTasks(metadataMap[metadataKey]);
70
+ metadataMap[metadataKey] = await this.validation(
71
+ 'deploy',
72
+ metadataMap[metadataKey],
73
+ deployDir
74
+ );
69
75
  } catch (ex) {
70
76
  // output error & remove from deploy list
71
77
  Util.logger.error(
@@ -187,7 +193,7 @@ class DataExtension extends MetadataType {
187
193
  DataExtension.oldFields[metadataMap[metadataKey][this.definition.keyField]] =
188
194
  await DataExtensionField.prepareDeployColumnsOnUpdate(
189
195
  metadataMap[metadataKey].Fields,
190
- metadataKey
196
+ Util.matchedByName?.[this.definition.type]?.[metadataKey] || metadataKey
191
197
  );
192
198
 
193
199
  if (
@@ -893,7 +899,7 @@ class DataExtension extends MetadataType {
893
899
  if (metadata[field?.DataExtension?.CustomerKey]) {
894
900
  metadata[field.DataExtension.CustomerKey].Fields.push(field);
895
901
  } else {
896
- Util.logger.warn(` - Issue retrieving data extension fields. key='${key}'`);
902
+ // field was retrieved for which we do not have the right dataExtension. This might be due to us having to resort to not using a DE filter to avoid the "String or binary data would be truncated." error
897
903
  }
898
904
  }
899
905
 
@@ -1632,6 +1638,70 @@ class DataExtension extends MetadataType {
1632
1638
  return super.getFilesToCommit(keyArr);
1633
1639
  }
1634
1640
  }
1641
+ /**
1642
+ * helper for {@link MetadataType.createOrUpdate}
1643
+ *
1644
+ * @param {MetadataTypeItem} metadataItem to be deployed item
1645
+ * @returns {MetadataTypeItem} cached item or undefined
1646
+ */
1647
+ static getCacheMatchedByName(metadataItem) {
1648
+ let cacheMatchedByName;
1649
+
1650
+ if (Util.OPTIONS.matchName) {
1651
+ // make sure to run the search ONLY if OPTIONS.matchName is true and definition.allowMatchingByName signals support
1652
+ const typeCache = cache.getCache()?.[this.definition.type];
1653
+ const potentials = [];
1654
+ for (const key in typeCache) {
1655
+ const cachedItem = typeCache[key];
1656
+ if (
1657
+ cachedItem[this.definition.nameField] ===
1658
+ metadataItem[this.definition.nameField]
1659
+ ) {
1660
+ potentials.push(cachedItem);
1661
+ }
1662
+ }
1663
+ if (potentials.length > 1) {
1664
+ throw new Error(
1665
+ `found multiple name matches in cache for ${this.definition.type} ${metadataItem[this.definition.keyField]} / ${metadataItem[this.definition.nameField]}. Check their keys for more details: ${potentials.map((p) => p[this.definition.keyField]).join(', ')}`
1666
+ );
1667
+ } else if (potentials.length === 1) {
1668
+ // only one item found, confirm that it's in the same folder
1669
+ const deployFolderPath = cache.searchForField(
1670
+ 'folder',
1671
+ metadataItem[this.definition.folderIdField],
1672
+ 'ID',
1673
+ 'Path'
1674
+ );
1675
+ if (
1676
+ potentials[0][this.definition.folderIdField] ===
1677
+ metadataItem[this.definition.folderIdField]
1678
+ ) {
1679
+ cacheMatchedByName = potentials[0];
1680
+
1681
+ Util.logger.info(
1682
+ Util.getGrayMsg(
1683
+ ` - found ${this.definition.type} ${metadataItem[this.definition.keyField]} in cache by name "${metadataItem[this.definition.nameField]}" and folder "${deployFolderPath}": ${cacheMatchedByName[this.definition.keyField]}`
1684
+ )
1685
+ );
1686
+ } else {
1687
+ const cacheFolderPath = cache.searchForField(
1688
+ 'folder',
1689
+ potentials[0][this.definition.folderIdField],
1690
+ 'ID',
1691
+ 'Path'
1692
+ );
1693
+ throw new Error(
1694
+ `found ${this.definition.type} ${metadataItem[this.definition.keyField]} in cache by name "${metadataItem[this.definition.nameField]}" but in different folders: "${deployFolderPath}" vs "${cacheFolderPath}": ${potentials[0][this.definition.keyField]}`
1695
+ );
1696
+ }
1697
+ } else {
1698
+ Util.logger.debug(
1699
+ ` - no name-match found for ${this.definition.type} ${metadataItem[this.definition.keyField]}. Creating new ${this.definition.type} instead.`
1700
+ );
1701
+ }
1702
+ }
1703
+ return cacheMatchedByName;
1704
+ }
1635
1705
  }
1636
1706
 
1637
1707
  // Assign definition to static attributes
@@ -48,7 +48,13 @@ class DataExtensionField extends MetadataType {
48
48
  * @returns {Promise.<{metadata: DataExtensionFieldMap, type: string}>} Promise of items
49
49
  */
50
50
  static async retrieveForCacheDE(requestParams, additionalFields) {
51
- return super.retrieveSOAP(null, requestParams, null, additionalFields);
51
+ let response;
52
+ response = await super.retrieveSOAP(null, requestParams, null, additionalFields);
53
+ if (!response) {
54
+ // try again but without filters as a workaround for the "String or binary data would be truncated." issue
55
+ response = await super.retrieveSOAP(null, {}, null, additionalFields);
56
+ }
57
+ return response;
52
58
  }
53
59
 
54
60
  /**