mcdev 7.4.0 → 7.4.2

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 (57) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/@types/lib/Deployer.d.ts.map +1 -1
  3. package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
  4. package/@types/lib/metadataTypes/DataExtensionField.d.ts.map +1 -1
  5. package/@types/lib/metadataTypes/Event.d.ts +13 -7
  6. package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
  7. package/@types/lib/metadataTypes/Folder.d.ts +1 -2
  8. package/@types/lib/metadataTypes/Folder.d.ts.map +1 -1
  9. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  10. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  11. package/@types/lib/util/util.d.ts +7 -0
  12. package/@types/lib/util/util.d.ts.map +1 -1
  13. package/@types/lib/util/validations.d.ts +2 -0
  14. package/@types/lib/util/validations.d.ts.map +1 -1
  15. package/@types/types/mcdev.d.d.ts +212 -0
  16. package/@types/types/mcdev.d.d.ts.map +1 -1
  17. package/boilerplate/files/.vscode/settings.json +0 -3
  18. package/boilerplate/forcedUpdates.json +8 -0
  19. package/boilerplate/gitignore-template +1 -0
  20. package/lib/Deployer.js +11 -3
  21. package/lib/cli.js +7 -1
  22. package/lib/metadataTypes/DataExtension.js +0 -13
  23. package/lib/metadataTypes/DataExtensionField.js +4 -2
  24. package/lib/metadataTypes/Event.js +53 -19
  25. package/lib/metadataTypes/Folder.js +8 -5
  26. package/lib/metadataTypes/Journey.js +43 -12
  27. package/lib/metadataTypes/MetadataType.js +11 -8
  28. package/lib/metadataTypes/definitions/TriggeredSend.definition.js +4 -2
  29. package/lib/util/config.js +4 -4
  30. package/lib/util/util.js +14 -0
  31. package/lib/util/validations.js +9 -2
  32. package/package.json +11 -11
  33. package/test/general.test.js +4 -4
  34. package/test/mockRoot/.mcdevrc.json +1 -1
  35. package/test/mockRoot/deploy/testInstance/testBU/triggeredSend/testExisting_triggeredSend.triggeredSend-meta.json +0 -1
  36. package/test/mockRoot/deploy/testInstance/testBU/triggeredSend/testNew_triggeredSend.triggeredSend-meta.json +0 -1
  37. package/test/resources/9999999/journey/build-expected.json +4 -2
  38. package/test/resources/9999999/journey/get-multistep-expected.json +63 -61
  39. package/test/resources/9999999/journey/get-quicksend-expected.json +4 -2
  40. package/test/resources/9999999/journey/get-quicksend-rcb-id-expected.json +2 -2
  41. package/test/resources/9999999/journey/get-quicksend-rcb-key-expected.json +2 -2
  42. package/test/resources/9999999/journey/get-quicksend-rcb-name-expected.json +6 -2
  43. package/test/resources/9999999/journey/get-transactionalEmail-expected.json +2 -2
  44. package/test/resources/9999999/journey/template-expected.json +4 -2
  45. package/test/resources/9999999/triggeredSend/build-expected.json +0 -1
  46. package/test/resources/9999999/triggeredSend/get-expected.json +0 -1
  47. package/test/resources/9999999/triggeredSend/get-rcb-id-expected.json +0 -1
  48. package/test/resources/9999999/triggeredSend/get-rcb-key-expected.json +0 -1
  49. package/test/resources/9999999/triggeredSend/get-rcb-name-expected.json +0 -1
  50. package/test/resources/9999999/triggeredSend/patch-expected.json +0 -1
  51. package/test/resources/9999999/triggeredSend/post-expected.json +0 -1
  52. package/test/resources/9999999/triggeredSend/template-expected.json +0 -1
  53. package/test/resources/9999999/triggeredSendDefinition/create-response.xml +0 -1
  54. package/test/resources/9999999/triggeredSendDefinition/retrieve-TriggeredSendStatusINNew,Active,Inactive,Moved,Canceled-response.xml +0 -2
  55. package/test/resources/9999999/triggeredSendDefinition/update-response.xml +0 -1
  56. package/test/type.dataExtension.test.js +3 -3
  57. package/types/mcdev.d.js +79 -0
@@ -149,11 +149,13 @@ class DataExtensionField extends MetadataType {
149
149
  const existingFieldByName = {};
150
150
 
151
151
  for (const key of Object.keys(fieldsObj)) {
152
- existingFieldByName[fieldsObj[key].Name] = fieldsObj[key];
152
+ // make sure we stringify the name in case it looked numeric and then lowercase it for easy comparison as the server is comparing field names case-insensitive
153
+ existingFieldByName[(fieldsObj[key].Name + '').toLowerCase()] = fieldsObj[key];
153
154
  }
154
155
  for (let i = deployColumns.length - 1; i >= 0; i--) {
155
156
  const item = deployColumns[i];
156
- const itemOld = existingFieldByName[item.Name];
157
+ // make sure we stringify the name in case it looked numeric and then lowercase it for easy comparison as the server is comparing field names case-insensitive
158
+ const itemOld = existingFieldByName[(item.Name + '').toLowerCase()];
157
159
  if (itemOld) {
158
160
  // field is getting updated ---
159
161
 
@@ -21,6 +21,8 @@ import pLimit from 'p-limit';
21
21
  *
22
22
  * @typedef {import('../../types/mcdev.d.js').ReferenceObject} ReferenceObject
23
23
  * @typedef {import('../../types/mcdev.d.js').SfObjectField} SfObjectField
24
+ * @typedef {import('../../types/mcdev.d.js').configurationArguments} configurationArguments
25
+ * @typedef {import('../../types/mcdev.d.js').Conditions} Conditions
24
26
  */
25
27
 
26
28
  /**
@@ -42,10 +44,10 @@ class Event extends MetadataType {
42
44
  * @param {string} [key] customer key of single item to retrieve
43
45
  * @returns {Promise.<MetadataTypeMapObj>} Promise of metadata
44
46
  */
45
- static retrieve(retrieveDir, _, __, key) {
47
+ static async retrieve(retrieveDir, _, __, key) {
46
48
  Util.logBeta(this.definition.type);
47
49
  try {
48
- return super.retrieveREST(
50
+ return await super.retrieveREST(
49
51
  retrieveDir,
50
52
  `/interaction/v1/eventDefinitions${
51
53
  key ? '/key:' + encodeURIComponent(key) : ''
@@ -447,7 +449,8 @@ class Event extends MetadataType {
447
449
  await this.postRetrieveTasks_SalesforceEntryEvents(
448
450
  metadata.type,
449
451
  metadata.configurationArguments,
450
- metadata.eventDefinitionKey
452
+ metadata.eventDefinitionKey,
453
+ metadata.publishedInteractionCount >= 1
451
454
  );
452
455
  } catch (ex) {
453
456
  Util.logger.warn(
@@ -644,8 +647,9 @@ class Event extends MetadataType {
644
647
  this.sfObjects.objectFields[objectAPIName][field.name] = field;
645
648
  }
646
649
  } else {
647
- Util.logger.debug(`Could not find any fields for Salesforce Object ${objectAPIName}`);
648
- throw new Error(`Could not find any fields for Salesforce Object ${objectAPIName}`);
650
+ Util.logger.warn(
651
+ `Could not cache fields for Salesforce Object '${objectAPIName}'. This is likely caused by insufficient access of your MC-Connect integration user. Please check assigned permission sets / the profile.`
652
+ );
649
653
  }
650
654
  return;
651
655
  }
@@ -666,26 +670,41 @@ class Event extends MetadataType {
666
670
 
667
671
  /**
668
672
  *
669
- * @param {any} ca trigger[0].configurationArguments
673
+ * @param {configurationArguments} ca trigger[0].configurationArguments
674
+ * @param {boolean} isPublished if the current item is published it means we do not need to do contact vs common checks
670
675
  */
671
- static checkSalesforceEntryEvents(ca) {
676
+ static checkSalesforceEntryEvents(ca, isPublished) {
672
677
  // 1 check eventDataConfig
673
- const edcObjects = ca.eventDataConfig.objects;
678
+ const edcObjects = ca.eventDataConfig.objects.sort((a, b) =>
679
+ a.dePrefix.localeCompare(b.dePrefix)
680
+ );
674
681
  const errors = [];
675
682
  const dePrefixFields = {};
676
683
  const dePrefixRelationshipMap = {};
677
684
  const dePrefixReferenceObjectMap = {};
678
- const checkCommon = ca.whoToInject === 'Contact ID/Lead ID (Contacts and Leads)';
685
+ // SFMC only uses "Common" to aggreagate Contacts and Leads if that was actively selected in the entry event. Also, already published journeys/events continue to work even if fields would later be changed, leading to a shift from or to the "common" fake-object.
686
+ const checkCommon =
687
+ ca.whoToInject === 'Contact ID/Lead ID (Contacts and Leads)' && !isPublished;
679
688
  for (const object of edcObjects) {
680
689
  // create secondary object to quickly check eventDataSummary against
681
690
  dePrefixFields[object.dePrefix] = object.fields;
682
- dePrefixRelationshipMap[object.dePrefix] = object.relationshipName;
683
- dePrefixReferenceObjectMap[object.dePrefix] = object.referenceObject;
691
+
692
+ // if the current object is the entry object then relationshipName and referenceObject are set to empty strings because it's not "referencing" a "relationship" but just listing its own fields
693
+ dePrefixRelationshipMap[object.dePrefix] =
694
+ object.relationshipName === ''
695
+ ? object.dePrefix.split(':')[0]
696
+ : object.relationshipName;
697
+ dePrefixReferenceObjectMap[object.dePrefix] =
698
+ object.referenceObject === ''
699
+ ? object.dePrefix.split(':')[0]
700
+ : object.referenceObject;
684
701
 
685
702
  // 1.1 check if fields in eventDataConfig exist in Salesforce
686
703
  // if it has no value this is the entry-source object itself
687
704
  const referencedObject =
688
705
  object.referenceObject === '' ? ca.objectAPIName : object.referenceObject;
706
+ // sort list of fields alphabetically
707
+ object.fields.sort();
689
708
  // check if object was cached earlier
690
709
  if (!this.sfObjects.workflowObjects.includes(referencedObject)) {
691
710
  errors.push(`Salesforce object ${referencedObject} not found on connected org.`);
@@ -695,7 +714,8 @@ class Event extends MetadataType {
695
714
  ) {
696
715
  // check if we found fields for the object
697
716
  errors.push(
698
- `No fields found for Salesforce object ${referencedObject} on connected org.`
717
+ `Fields for Salesforce object ${referencedObject} could not be checked. Fields selected in entry event: ` +
718
+ object.fields.join(', ')
699
719
  );
700
720
  } else {
701
721
  // check if the fields selected in the eventDefinition are actually available
@@ -892,19 +912,23 @@ class Event extends MetadataType {
892
912
  /**
893
913
  *
894
914
  * @param {string} triggerType e.g. SalesforceObjectTriggerV2, APIEvent, ...
895
- * @param {any} ca trigger[0].configurationArguments
915
+ * @param {configurationArguments} ca trigger[0].configurationArguments
896
916
  * @param {string} key of event / journey
897
- * @param {string} [type] optionally provide type for error on missing configurationArguments attributes
917
+ * @param {boolean} isPublished if the current item is published it means we do not need to do contact vs common checks
918
+ * @param {string} [type] optionally provide metadatype for error on missing configurationArguments attributes
898
919
  * @returns {Promise.<void>} -
899
920
  */
900
- static async postRetrieveTasks_SalesforceEntryEvents(triggerType, ca, key, type) {
921
+ static async postRetrieveTasks_SalesforceEntryEvents(triggerType, ca, key, isPublished, type) {
901
922
  if (triggerType !== 'SalesforceObjectTriggerV2' || !ca) {
902
923
  return;
903
924
  }
904
925
  // normalize payload because these fields are sometimes set as strings and sometimes as objects
926
+ // @ts-expect-error journeys SOMETIMES spell it "Api" and this script aims to auto-correct that
905
927
  if (ca.objectApiName) {
906
928
  // on event only the uppercase version is used. lets make sure we normalize that here.
929
+ // @ts-expect-error journeys SOMETIMES spell it "Api" and this script aims to auto-correct that
907
930
  ca.objectAPIName = ca.objectApiName;
931
+ // @ts-expect-error journeys SOMETIMES spell it "Api" and this script aims to auto-correct that
908
932
  delete ca.objectApiName;
909
933
  }
910
934
 
@@ -926,7 +950,8 @@ class Event extends MetadataType {
926
950
  : ca.eventDataConfig;
927
951
  ca.eventDataSummary =
928
952
  'string' === typeof ca.eventDataSummary
929
- ? ca.eventDataSummary.split('; ').filter(Boolean)
953
+ ? // @ts-expect-error transforming this from API-string-format to from mcdev-format
954
+ ca.eventDataSummary.split('; ').filter(Boolean).sort()
930
955
  : ca.eventDataSummary;
931
956
  ca.passThroughArgument =
932
957
  'string' === typeof ca.passThroughArgument
@@ -946,13 +971,13 @@ class Event extends MetadataType {
946
971
  await this.getSalesforceObjects(ca.objectAPIName);
947
972
 
948
973
  // check if whats on the journey matches what SF Core offers
949
- this.checkSalesforceEntryEvents(ca);
974
+ this.checkSalesforceEntryEvents(ca, isPublished);
950
975
  }
951
976
 
952
977
  /**
953
978
  *
954
979
  * @param {string} triggerType e.g. SalesforceObjectTriggerV2, APIEvent, ...
955
- * @param {any} ca trigger[0].configurationArguments
980
+ * @param {configurationArguments} ca trigger[0].configurationArguments
956
981
  * @returns {Promise.<void>} -
957
982
  */
958
983
  static async preDeployTasks_SalesforceEntryEvents(triggerType, ca) {
@@ -980,34 +1005,43 @@ class Event extends MetadataType {
980
1005
  await this.getSalesforceObjects(ca.objectAPIName);
981
1006
 
982
1007
  // check if whats on the journey matches what SF Core offers
983
- this.checkSalesforceEntryEvents(ca);
1008
+ this.checkSalesforceEntryEvents(ca, false);
984
1009
 
985
1010
  // normalize payload because these fields are sometimes set as strings and sometimes as objects
1011
+ // @ts-expect-error reverting this back from mcdev-format to API format
986
1012
  ca.contactKey =
987
1013
  'object' === typeof ca.contactKey ? JSON.stringify(ca.contactKey) : ca.contactKey;
1014
+ // @ts-expect-error reverting this back from mcdev-format to API format
988
1015
  ca.eventDataConfig =
989
1016
  'object' === typeof ca.eventDataConfig
990
1017
  ? JSON.stringify(ca.eventDataConfig)
991
1018
  : ca.eventDataConfig;
1019
+ // @ts-expect-error reverting this back from mcdev-format to API format
992
1020
  ca.eventDataSummary = Array.isArray(ca.eventDataSummary)
993
1021
  ? ca.eventDataSummary.join('; ') + '; '
994
1022
  : ca.eventDataSummary;
1023
+ // @ts-expect-error reverting this back from mcdev-format to API format
995
1024
  ca.passThroughArgument =
996
1025
  'object' === typeof ca.passThroughArgument
997
1026
  ? JSON.stringify(ca.passThroughArgument)
998
1027
  : ca.passThroughArgument;
1028
+ // @ts-expect-error reverting this back from mcdev-format to API format
999
1029
  ca.primaryObjectFilterCriteria =
1000
1030
  'object' === typeof ca.primaryObjectFilterCriteria
1001
1031
  ? JSON.stringify(ca.primaryObjectFilterCriteria)
1002
1032
  : ca.primaryObjectFilterCriteria;
1033
+ // @ts-expect-error reverting this back from mcdev-format to API format
1003
1034
  ca.relatedObjectFilterCriteria =
1004
1035
  'object' === typeof ca.relatedObjectFilterCriteria
1005
1036
  ? JSON.stringify(ca.relatedObjectFilterCriteria)
1006
1037
  : ca.relatedObjectFilterCriteria;
1007
1038
 
1039
+ // @ts-expect-error journeys SOMETIMES spell it "Api" and this script aims to auto-correct that
1008
1040
  if (ca.objectApiName) {
1009
1041
  // on event only the uppercase version is used. lets make sure we normalize that here.
1042
+ // @ts-expect-error journeys SOMETIMES spell it "Api" and this script aims to auto-correct that
1010
1043
  ca.objectAPIName = ca.objectApiName;
1044
+ // @ts-expect-error journeys SOMETIMES spell it "Api" and this script aims to auto-correct that
1011
1045
  delete ca.objectApiName;
1012
1046
  }
1013
1047
  }
@@ -164,7 +164,7 @@ class Folder extends MetadataType {
164
164
  }
165
165
  }
166
166
  if (retrieveDir) {
167
- const savedMetadata = await this.saveResults(metadata, retrieveDir, this.buObject.mid);
167
+ const savedMetadata = await this.saveResults(metadata, retrieveDir);
168
168
  Util.logger.info(
169
169
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
170
170
  );
@@ -644,17 +644,20 @@ class Folder extends MetadataType {
644
644
  *
645
645
  * @param {ListMap} results metadata results from deploy
646
646
  * @param {string} retrieveDir directory where metadata should be stored after deploy/retrieve
647
- * @param {number | string} mid unused parameter
648
647
  * @returns {Promise.<ListMap>} Promise of saved metadata
649
648
  */
650
- static async saveResults(results, retrieveDir, mid) {
649
+ static async saveResults(results, retrieveDir) {
651
650
  /** @type {ListMap} */
652
651
  const savedResults = {};
653
652
  for (const metadataEntry in results) {
654
653
  try {
655
654
  // skip saving shared folders as they technically live in parent.
656
- // ! Warning: our result set does not have Client.ID in it - bad check?
657
- if (results[metadataEntry].Client && mid != results[metadataEntry].Client.ID) {
655
+ if (
656
+ results[metadataEntry].Client &&
657
+ this.buObject.mid != results[metadataEntry].Client.ID
658
+ ) {
659
+ // deploy: folders auto-generated by deploy do not have .Client set and hence this check will be skipped
660
+ // retrieve: Client.ID is set to the MID of the BU that the folder belongs to; we only want folders of the current BU saved here
658
661
  continue;
659
662
  } else if (
660
663
  results[metadataEntry] &&
@@ -185,7 +185,7 @@ class Journey extends MetadataType {
185
185
  // if the interaction does not exist, the API returns an error code which would otherwise bring execution to a hold
186
186
  if (
187
187
  [
188
- 'Journey matching key not found.',
188
+ 'Interaction matching key not found.',
189
189
  'Must provide a valid ID or Key parameter',
190
190
  ].includes(ex.message)
191
191
  ) {
@@ -419,6 +419,7 @@ class Journey extends MetadataType {
419
419
  metadata.triggers[0].type,
420
420
  metadata.triggers[0].configurationArguments,
421
421
  metadata.key,
422
+ metadata.status === 'Published',
422
423
  this.definition.type
423
424
  );
424
425
  } catch (ex) {
@@ -562,7 +563,6 @@ class Journey extends MetadataType {
562
563
  switch (activity.type) {
563
564
  case 'EMAILV2': {
564
565
  // triggeredSend + email+asset
565
- // TODO email / asset
566
566
  const configurationArguments = activity.configurationArguments;
567
567
  if (configurationArguments) {
568
568
  try {
@@ -634,12 +634,15 @@ class Journey extends MetadataType {
634
634
  triggeredSend.emailId = linkedTS.Email?.ID;
635
635
  triggeredSend.dynamicEmailSubject = linkedTS.DynamicEmailSubject;
636
636
  triggeredSend.emailSubject = linkedTS.EmailSubject;
637
- triggeredSend.bccEmail = linkedTS.BccEmail;
637
+ // only the bccEmail field can be retrieved for triggeredSends, not the ccEmail field; for some reason BccEmail can be retrieved but does not return a value even if stored correctly in the journey.
638
+ // triggeredSend.bccEmail = linkedTS.BccEmail;
638
639
  triggeredSend.isMultipart = linkedTS.IsMultipart;
639
640
  triggeredSend.autoAddSubscribers = linkedTS.AutoAddSubscribers;
640
641
  triggeredSend.autoUpdateSubscribers =
641
642
  linkedTS.AutoUpdateSubscribers;
642
643
  triggeredSend.isTrackingClicks = !linkedTS.SuppressTracking;
644
+ triggeredSend.suppressTracking = linkedTS.SuppressTracking;
645
+ triggeredSend.triggeredSendStatus = linkedTS.TriggeredSendStatus;
643
646
  // from name & email are set in the senderProfile, not in the triggeredSend
644
647
  // triggeredSend.fromName = linkedTS.FromName;
645
648
  // triggeredSend.fromAddress = linkedTS.FromAddress;
@@ -735,6 +738,13 @@ class Journey extends MetadataType {
735
738
  delete triggeredSend.key;
736
739
  }
737
740
 
741
+ triggeredSend.ccEmail = triggeredSend.ccEmail
742
+ .split(';')
743
+ .filter((el) => el !== '');
744
+ triggeredSend.bccEmail = triggeredSend.bccEmail
745
+ .split(';')
746
+ .filter((el) => el !== '');
747
+
738
748
  // List (optional)
739
749
  triggeredSend.r__list_PathName ||= {};
740
750
  if (triggeredSend.publicationListId) {
@@ -891,6 +901,10 @@ class Journey extends MetadataType {
891
901
  }
892
902
  }
893
903
  }
904
+
905
+ // sort attributes of triggeredSend alphabetically to allow for easier pull request reviews
906
+ configurationArguments.triggeredSend =
907
+ Util.sortObjectAttributes(triggeredSend);
894
908
  }
895
909
  break;
896
910
  }
@@ -1009,12 +1023,10 @@ class Journey extends MetadataType {
1009
1023
  break;
1010
1024
  }
1011
1025
  }
1012
- // TODO: Filters / activities[].type === 'MULTICRITERIADECISION'
1013
- // - activities[].arguments.filterResult
1014
- // - activities[].arguments.configurationArguments.criteria
1015
-
1016
- // TODO: wait activity / activities[].type === 'WAIT'
1017
1026
  }
1027
+
1028
+ // apply sorting by activity key to work around the API shuffling activities around
1029
+ metadata.activities = metadata.activities.sort((a, b) => a.key.localeCompare(b.key));
1018
1030
  }
1019
1031
 
1020
1032
  /**
@@ -1221,6 +1233,15 @@ class Journey extends MetadataType {
1221
1233
  delete triggeredSend.r__triggeredSend_key;
1222
1234
  }
1223
1235
 
1236
+ triggeredSend.ccEmail =
1237
+ typeof triggeredSend.ccEmail === 'string'
1238
+ ? triggeredSend.ccEmail
1239
+ : triggeredSend.ccEmail.join(';');
1240
+ triggeredSend.bccEmail =
1241
+ typeof triggeredSend.bccEmail === 'string'
1242
+ ? triggeredSend.bccEmail
1243
+ : triggeredSend.bccEmail.join(';');
1244
+
1224
1245
  // List (optional)
1225
1246
  if (triggeredSend.r__list_PathName) {
1226
1247
  if (triggeredSend.r__list_PathName.publicationList) {
@@ -1475,11 +1496,16 @@ class Journey extends MetadataType {
1475
1496
  if (triggeredSend) {
1476
1497
  // the following is very similar but not equal to the variables in TriggeredSend.js
1477
1498
  try {
1478
- triggeredSend.bccEmail = ReplaceCbReference.replaceReference(
1479
- triggeredSend.bccEmail,
1499
+ let bccEmail =
1500
+ typeof triggeredSend.bccEmail === 'string'
1501
+ ? triggeredSend.bccEmail
1502
+ : triggeredSend.bccEmail.join(';');
1503
+ bccEmail = ReplaceCbReference.replaceReference(
1504
+ bccEmail,
1480
1505
  parentName,
1481
1506
  findAssetKeys
1482
1507
  );
1508
+ triggeredSend.bccEmail = bccEmail.split(';').filter((el) => el !== '');
1483
1509
  changes = true;
1484
1510
  } catch (ex) {
1485
1511
  if (ex.code !== 200) {
@@ -1487,11 +1513,16 @@ class Journey extends MetadataType {
1487
1513
  }
1488
1514
  }
1489
1515
  try {
1490
- triggeredSend.ccEmail = ReplaceCbReference.replaceReference(
1491
- triggeredSend.ccEmail,
1516
+ let ccEmail =
1517
+ typeof triggeredSend.ccEmail === 'string'
1518
+ ? triggeredSend.ccEmail
1519
+ : triggeredSend.ccEmail.join(';');
1520
+ ccEmail = ReplaceCbReference.replaceReference(
1521
+ ccEmail,
1492
1522
  parentName,
1493
1523
  findAssetKeys
1494
1524
  );
1525
+ triggeredSend.ccEmail = ccEmail.split(';').filter((el) => el !== '');
1495
1526
  changes = true;
1496
1527
  } catch (ex) {
1497
1528
  if (ex.code !== 200) {
@@ -135,14 +135,17 @@ class MetadataType {
135
135
  */
136
136
  static async deploy(metadataMap, deployDir, retrieveDir) {
137
137
  const upsertedMetadataMap = await this.upsert(metadataMap, deployDir);
138
- const savedMetadataMap = await this.saveResults(upsertedMetadataMap, retrieveDir, null);
139
- if (
140
- this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
141
- !this.definition.documentInOneFile
142
- ) {
143
- // * do not await here as this might take a while and has no impact on the deploy
144
- // * this should only be run if documentation is on a per metadata record level. Types that document an overview into a single file will need a full retrieve to work instead
145
- this.document(savedMetadataMap, true);
138
+ if (retrieveDir) {
139
+ // deploy can be run with retrieveDir set to null for deploying auto-created foldes - these should not be saved to the retrieve-folder while everything else should
140
+ const savedMetadataMap = await this.saveResults(upsertedMetadataMap, retrieveDir, null);
141
+ if (
142
+ this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
143
+ !this.definition.documentInOneFile
144
+ ) {
145
+ // * do not await here as this might take a while and has no impact on the deploy
146
+ // * this should only be run if documentation is on a per metadata record level. Types that document an overview into a single file will need a full retrieve to work instead
147
+ this.document(savedMetadataMap, true);
148
+ }
146
149
  }
147
150
  return upsertedMetadataMap;
148
151
  }
@@ -67,10 +67,11 @@ export default {
67
67
  templating: true,
68
68
  },
69
69
  BccEmail: {
70
+ // while this can be retrieved, it seems to be always returned empty
70
71
  isCreateable: true,
71
72
  isUpdateable: true,
72
- retrieving: true,
73
- templating: true,
73
+ retrieving: false,
74
+ templating: false,
74
75
  },
75
76
  CategoryID: {
76
77
  isCreateable: true,
@@ -79,6 +80,7 @@ export default {
79
80
  templating: true,
80
81
  },
81
82
  CCEmail: {
83
+ // this field is updatable but not retrievable for some reason
82
84
  isCreateable: true,
83
85
  isUpdateable: true,
84
86
  retrieving: false,
@@ -202,7 +202,7 @@ const config = {
202
202
  Array.isArray(defaultProps[key][subkey])
203
203
  ? 'Array'
204
204
  : typeof defaultProps[key][subkey]
205
- }): ${defaultProps[key][subkey]}`
205
+ }): ${typeof defaultProps[key][subkey] === 'object' ? JSON.stringify(defaultProps[key][subkey]) : defaultProps[key][subkey]}`
206
206
  );
207
207
  solutionSet.add(
208
208
  `Run 'mcdev upgrade' to fix missing or changed configuration options`
@@ -270,15 +270,15 @@ const config = {
270
270
  errorMsgOutput.push(' - ' + msg);
271
271
  }
272
272
  Util.logger.error(errorMsgOutput.join('\n'));
273
- if (Util.skipInteraction) {
274
- return false;
275
- }
276
273
  Util.logger.info(
277
274
  [
278
275
  'Here is what you can do to fix these issues:',
279
276
  ...Array.from(solutionSet),
280
277
  ].join('\n- ')
281
278
  );
279
+ if (Util.skipInteraction) {
280
+ return false;
281
+ }
282
282
  const runUpgradeNow = await confirm({
283
283
  message: `Do you want to run 'mcdev upgrade' now?`,
284
284
  default: true,
package/lib/util/util.js CHANGED
@@ -1138,6 +1138,20 @@ export const Util = {
1138
1138
  });
1139
1139
  return [...new Set(values.sort())];
1140
1140
  },
1141
+ /**
1142
+ * helper that returns a new object with sorted attributes of the given object
1143
+ *
1144
+ * @param {object} obj object with unsorted attributes
1145
+ * @returns {object} obj but with sorted attributes
1146
+ */
1147
+ sortObjectAttributes(obj) {
1148
+ return Object.keys(obj)
1149
+ .sort()
1150
+ .reduce((acc, key) => {
1151
+ acc[key] = obj[key];
1152
+ return acc;
1153
+ }, {});
1154
+ },
1141
1155
  };
1142
1156
 
1143
1157
  Util.startLogger(false, true);
@@ -2,6 +2,12 @@
2
2
  import path from 'node:path';
3
3
  import { Util } from './util.js';
4
4
 
5
+ /**
6
+ * @typedef {import('../../types/mcdev.d.js').validationRuleList} validationRuleList
7
+ * @typedef {import('../../types/mcdev.d.js').validationRuleTest} validationRuleTest
8
+ */
9
+
10
+ /** @type {validationRuleList} */
5
11
  let customRules = {};
6
12
  let customRuleImport;
7
13
  try {
@@ -28,11 +34,12 @@ export default async function validation(definition, item, targetDir) {
28
34
  'Could not load custom validation rules from .mcdev-validations.js: ' + ex.message
29
35
  );
30
36
  }
37
+ /** @type {validationRuleList} */
31
38
  const defaultRules = {
32
39
  noGuidKeys: {
33
40
  failedMsg: 'Please update the key to a readable value. Currently still in GUID format.',
34
41
  /**
35
- * @returns {boolean} true=test passed
42
+ * @type {validationRuleTest}
36
43
  */
37
44
  passed: function () {
38
45
  const key = item[definition.keyField];
@@ -48,7 +55,7 @@ export default async function validation(definition, item, targetDir) {
48
55
  noRootFolder: {
49
56
  failedMsg: 'Root folder not allowed. Current folder: ' + item.r__folder_Path,
50
57
  /**
51
- * @returns {boolean} true=test passed
58
+ * @type {validationRuleTest}
52
59
  */
53
60
  passed: function () {
54
61
  /** @type {string} */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcdev",
3
- "version": "7.4.0",
3
+ "version": "7.4.2",
4
4
  "description": "Accenture Salesforce Marketing Cloud DevTools",
5
5
  "author": "Accenture: joern.berkefeld, douglas.midgley, robert.zimmermann, maciej.barnas",
6
6
  "license": "MIT",
@@ -75,7 +75,7 @@
75
75
  "console.table": "0.10.0",
76
76
  "deep-equal": "2.2.3",
77
77
  "fs-extra": "11.2.0",
78
- "inquirer": "10.1.8",
78
+ "inquirer": "11.0.2",
79
79
  "json-to-table": "4.2.1",
80
80
  "mustache": "4.2.0",
81
81
  "p-limit": "6.1.0",
@@ -85,39 +85,39 @@
85
85
  "sfmc-sdk": "2.1.2",
86
86
  "simple-git": "3.25.0",
87
87
  "toposort": "2.0.2",
88
- "update-notifier": "7.2.0",
88
+ "update-notifier": "7.3.1",
89
89
  "winston": "3.14.2",
90
90
  "yargs": "17.7.2",
91
91
  "yocto-spinner": "0.1.0"
92
92
  },
93
93
  "devDependencies": {
94
- "@eslint/js": "9.9.0",
94
+ "@eslint/js": "9.10.0",
95
95
  "@types/fs-extra": "11.0.4",
96
96
  "@types/inquirer": "9.0.7",
97
- "@types/mocha": "10.0.7",
98
- "@types/node": "22.4.2",
97
+ "@types/mocha": "10.0.8",
98
+ "@types/node": "22.5.5",
99
99
  "@types/yargs": "17.0.33",
100
100
  "assert": "2.1.0",
101
101
  "axios-mock-adapter": "2.0.0",
102
102
  "c8": "10.0.0",
103
103
  "chai": "5.1.1",
104
104
  "chai-files": "1.4.0",
105
- "eslint": "9.9.0",
105
+ "eslint": "9.10.0",
106
106
  "eslint-config-prettier": "9.1.0",
107
107
  "eslint-config-ssjs": "2.0.0",
108
- "eslint-plugin-jsdoc": "50.2.2",
108
+ "eslint-plugin-jsdoc": "50.2.4",
109
109
  "eslint-plugin-mocha": "10.5.0",
110
110
  "eslint-plugin-prettier": "5.2.1",
111
111
  "eslint-plugin-unicorn": "55.0.0",
112
112
  "fast-xml-parser": "4.4.1",
113
113
  "globals": "15.9.0",
114
- "husky": "9.1.5",
115
- "lint-staged": "15.2.9",
114
+ "husky": "9.1.6",
115
+ "lint-staged": "15.2.10",
116
116
  "mocha": "10.7.3",
117
117
  "mock-fs": "5.2.0",
118
118
  "npm-run-all": "4.1.5",
119
119
  "prettier-eslint": "16.3.0",
120
- "typescript": "5.5.4"
120
+ "typescript": "5.6.2"
121
121
  },
122
122
  "optionalDependencies": {
123
123
  "fsevents": "*"
@@ -569,7 +569,7 @@ describe('GENERAL', () => {
569
569
  );
570
570
  assert.equal(
571
571
  testUtils.getAPIHistoryLength(),
572
- 14,
572
+ 13,
573
573
  'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
574
574
  );
575
575
  });
@@ -632,7 +632,7 @@ describe('GENERAL', () => {
632
632
  );
633
633
  assert.equal(
634
634
  testUtils.getAPIHistoryLength(),
635
- 15,
635
+ 14,
636
636
  'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
637
637
  );
638
638
  });
@@ -697,7 +697,7 @@ describe('GENERAL', () => {
697
697
  );
698
698
  assert.equal(
699
699
  testUtils.getAPIHistoryLength(),
700
- 12,
700
+ 11,
701
701
  'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
702
702
  );
703
703
  });
@@ -2251,7 +2251,7 @@ describe('GENERAL', () => {
2251
2251
  );
2252
2252
  assert.equal(
2253
2253
  testUtils.getAPIHistoryLength(),
2254
- 14,
2254
+ 13,
2255
2255
  'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
2256
2256
  );
2257
2257
  });
@@ -114,5 +114,5 @@
114
114
  "verification"
115
115
  ]
116
116
  },
117
- "version": "7.4.0"
117
+ "version": "7.4.2"
118
118
  }
@@ -3,7 +3,6 @@
3
3
  "AutoAddSubscribers": false,
4
4
  "AutoUpdateSubscribers": false,
5
5
  "BatchInterval": 0,
6
- "BccEmail": "",
7
6
  "CreatedDate": "2018-06-25T05:58:00",
8
7
  "CustomerKey": "testExisting_triggeredSend",
9
8
  "Description": "updated on deploy",
@@ -3,7 +3,6 @@
3
3
  "AutoAddSubscribers": false,
4
4
  "AutoUpdateSubscribers": false,
5
5
  "BatchInterval": 0,
6
- "BccEmail": "",
7
6
  "CreatedDate": "2018-06-25T05:58:00",
8
7
  "CustomerKey": "testNew_triggeredSend",
9
8
  "Description": "created on deploy",