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
@@ -14,6 +14,7 @@ import deepEqual from 'deep-equal';
14
14
  import pLimit from 'p-limit';
15
15
  import Mustache from 'mustache';
16
16
  import MetadataTypeInfo from '../MetadataTypeInfo.js';
17
+ import validationsRules from '../util/validations.js';
17
18
 
18
19
  /**
19
20
  * @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
@@ -127,23 +128,23 @@ class MetadataType {
127
128
  /**
128
129
  * Deploys metadata
129
130
  *
130
- * @param {MetadataTypeMap} metadata metadata mapped by their keyField
131
+ * @param {MetadataTypeMap} metadataMap metadata mapped by their keyField
131
132
  * @param {string} deployDir directory where deploy metadata are saved
132
133
  * @param {string} retrieveDir directory where metadata after deploy should be saved
133
134
  * @returns {Promise.<MetadataTypeMap>} Promise of keyField => metadata map
134
135
  */
135
- static async deploy(metadata, deployDir, retrieveDir) {
136
- const upsertResults = await this.upsert(metadata, deployDir);
137
- const savedMetadata = await this.saveResults(upsertResults, retrieveDir, null);
136
+ static async deploy(metadataMap, deployDir, retrieveDir) {
137
+ const upsertedMetadataMap = await this.upsert(metadataMap, deployDir);
138
+ const savedMetadataMap = await this.saveResults(upsertedMetadataMap, retrieveDir, null);
138
139
  if (
139
140
  this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type) &&
140
141
  !this.definition.documentInOneFile
141
142
  ) {
142
143
  // * do not await here as this might take a while and has no impact on the deploy
143
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
144
- this.document(savedMetadata, true);
145
+ this.document(savedMetadataMap, true);
145
146
  }
146
- return upsertResults;
147
+ return upsertedMetadataMap;
147
148
  }
148
149
 
149
150
  /**
@@ -710,12 +711,15 @@ class MetadataType {
710
711
  *
711
712
  * @param {MetadataTypeMap} metadataMap metadata mapped by their keyField
712
713
  * @param {string} deployDir directory where deploy metadata are saved
714
+ * @param {boolean} [runUpsertSequentially] when a type has self-dependencies creates need to run one at a time and created/changed keys need to be cached to ensure following creates/updates have thoses keys available
713
715
  * @returns {Promise.<MetadataTypeMap>} keyField => metadata map
714
716
  */
715
- static async upsert(metadataMap, deployDir) {
717
+ static async upsert(metadataMap, deployDir, runUpsertSequentially = false) {
716
718
  const orignalMetadataMap = structuredClone(metadataMap);
717
719
  const metadataToUpdate = [];
718
720
  const metadataToCreate = [];
721
+ let createResults = [];
722
+ let updateResults = [];
719
723
  let filteredByPreDeploy = 0;
720
724
  for (const metadataKey in metadataMap) {
721
725
  let hasError = false;
@@ -727,6 +731,12 @@ class MetadataType {
727
731
  metadataMap[metadataKey],
728
732
  deployDir
729
733
  );
734
+
735
+ deployableMetadata = await this.validation(
736
+ 'deploy',
737
+ deployableMetadata,
738
+ deployDir
739
+ );
730
740
  } catch (ex) {
731
741
  // do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
732
742
  hasError = true;
@@ -744,13 +754,58 @@ class MetadataType {
744
754
  if (deployableMetadata) {
745
755
  metadataMap[metadataKey] = deployableMetadata;
746
756
  // create normalizedKey off of whats in the json rather than from "metadataKey" because preDeployTasks might have altered something (type asset)
747
- await this.createOrUpdate(
757
+ const action = await this.createOrUpdate(
748
758
  metadataMap,
749
759
  metadataKey,
750
760
  hasError,
751
761
  metadataToUpdate,
752
762
  metadataToCreate
753
763
  );
764
+ if (runUpsertSequentially) {
765
+ if (action === 'create') {
766
+ // handle creates sequentially here becasue we might have interdepencies
767
+ const result = await this.create(metadataMap[metadataKey], deployDir);
768
+ if (result) {
769
+ createResults.push(result);
770
+
771
+ // make this newly created item available in cache for other itmes that might reference it
772
+ const newObject = {};
773
+ newObject[metadataKey] = metadataMap[metadataKey];
774
+ cache.mergeMetadata(this.definition.type, newObject);
775
+ }
776
+ } else if (action === 'update' && !Util.OPTIONS.noUpdate) {
777
+ const metadataEntry = metadataToUpdate.find(
778
+ (el) =>
779
+ el !== null &&
780
+ el.after[this.definition.keyField] ===
781
+ metadataMap[metadataKey][this.definition.keyField]
782
+ );
783
+ if (!metadataEntry) {
784
+ Util.logger.error(
785
+ ` - ${this.definition.type} ${metadataKey} / ${metadataMap[metadataKey][this.definition.keyField]} not found in update list`
786
+ );
787
+ continue;
788
+ }
789
+ // handle updates sequentially here becasue we might have interdepencies
790
+ // this is especially important when we use features like --matchName which are updates but change the key
791
+ const result = await this.update(
792
+ metadataEntry.after,
793
+ metadataEntry.before
794
+ );
795
+ if (result) {
796
+ updateResults.push(result);
797
+
798
+ // make this newly created item available in cache for other itmes that might reference it
799
+ const newObject = {};
800
+ newObject[metadataKey] = structuredClone(metadataMap[metadataKey]);
801
+ if (result.objectID) {
802
+ // required for assets
803
+ newObject[metadataKey].objectID = result.objectID;
804
+ }
805
+ cache.mergeMetadata(this.definition.type, newObject);
806
+ }
807
+ }
808
+ }
754
809
  } else {
755
810
  filteredByPreDeploy++;
756
811
  }
@@ -758,37 +813,40 @@ class MetadataType {
758
813
  Util.logger.errorStack(ex, `Upserting ${this.definition.type} failed`);
759
814
  }
760
815
  }
761
- const createLimit = pLimit(10);
762
- const createResults = (
763
- await Promise.all(
764
- metadataToCreate
765
- .filter((r) => r !== undefined && r !== null)
766
- .map((metadataEntry) =>
767
- createLimit(() => this.create(metadataEntry, deployDir))
768
- )
769
- )
770
- ).filter((r) => r !== undefined && r !== null);
816
+ if (!runUpsertSequentially) {
817
+ // create
818
+ const createLimit = pLimit(10);
819
+ createResults = (
820
+ await Promise.all(
821
+ metadataToCreate
822
+ .filter((r) => r !== undefined && r !== null)
823
+ .map((metadataEntry) =>
824
+ createLimit(() => this.create(metadataEntry, deployDir))
825
+ )
826
+ )
827
+ ).filter((r) => r !== undefined && r !== null);
771
828
 
772
- if (Util.OPTIONS.noUpdate && metadataToUpdate.length > 0) {
773
- Util.logger.info(
774
- ` ☇ skipping update of ${metadataToUpdate.length} ${this.definition.type}${metadataToUpdate.length == 1 ? '' : 's'}: --noUpdate flag is set`
775
- );
829
+ // update
830
+ if (Util.OPTIONS.noUpdate && metadataToUpdate.length) {
831
+ Util.logger.info(
832
+ ` ☇ skipping update of ${metadataToUpdate.length} ${this.definition.type}${metadataToUpdate.length == 1 ? '' : 's'}: --noUpdate flag is set`
833
+ );
834
+ } else if (metadataToUpdate.length) {
835
+ const updateLimit = pLimit(10);
836
+ updateResults = (
837
+ await Promise.all(
838
+ metadataToUpdate
839
+ .filter((r) => r !== undefined && r !== null)
840
+ .map((metadataEntry) =>
841
+ updateLimit(() =>
842
+ this.update(metadataEntry.after, metadataEntry.before)
843
+ )
844
+ )
845
+ )
846
+ ).filter((r) => r !== undefined && r !== null);
847
+ }
776
848
  }
777
849
 
778
- const updateLimit = pLimit(10);
779
- const updateResults = Util.OPTIONS.noUpdate
780
- ? []
781
- : (
782
- await Promise.all(
783
- metadataToUpdate
784
- .filter((r) => r !== undefined && r !== null)
785
- .map((metadataEntry) =>
786
- updateLimit(() =>
787
- this.update(metadataEntry.after, metadataEntry.before)
788
- )
789
- )
790
- )
791
- ).filter((r) => r !== undefined && r !== null);
792
850
  // Logging
793
851
  Util.logger.info(
794
852
  `${this.definition.type} upsert: ${createResults.length} of ${metadataToCreate.length} created / ${updateResults.length} of ${metadataToUpdate.length} updated` +
@@ -886,6 +944,11 @@ class MetadataType {
886
944
  } else if (cacheMatchedByKey || cacheMatchedByName) {
887
945
  // normal way of processing update files
888
946
  const cachedVersion = cacheMatchedByKey || cacheMatchedByName;
947
+ if (!cacheMatchedByKey && cacheMatchedByName) {
948
+ Util.matchedByName[this.definition.type] ||= {};
949
+ Util.matchedByName[this.definition.type][metadataKey] =
950
+ cacheMatchedByName[this.definition.keyField];
951
+ }
889
952
  if (!this.hasChanged(cachedVersion, metadataMap[metadataKey])) {
890
953
  hasError = true;
891
954
  }
@@ -1903,6 +1966,11 @@ class MetadataType {
1903
1966
  filterCounter++;
1904
1967
  continue;
1905
1968
  }
1969
+ results[originalKey] = await this.validation(
1970
+ 'retrieve',
1971
+ results[originalKey],
1972
+ retrieveDir
1973
+ );
1906
1974
 
1907
1975
  savedResults[originalKey] = await this.saveToDisk(
1908
1976
  results,
@@ -2206,6 +2274,11 @@ class MetadataType {
2206
2274
  );
2207
2275
 
2208
2276
  try {
2277
+ await this.validation(
2278
+ 'buildDefinition',
2279
+ metadata,
2280
+ Array.isArray(targetDir) ? targetDir[0] : targetDir
2281
+ );
2209
2282
  // write to file
2210
2283
  const targetDirArr = Array.isArray(targetDir) ? targetDir : [targetDir];
2211
2284
  for (const targetDir of targetDirArr) {
@@ -2234,7 +2307,9 @@ class MetadataType {
2234
2307
 
2235
2308
  return { metadata: metadata, type: this.definition.type };
2236
2309
  } catch (ex) {
2237
- throw new Error(`${this.definition.type}:: ${ex.message}`);
2310
+ Util.logger.error(
2311
+ ` ☇ skipped ${this.definition.type} ${metadata[this.definition.keyField]}: ${ex.message}`
2312
+ );
2238
2313
  }
2239
2314
  }
2240
2315
 
@@ -2702,6 +2777,76 @@ class MetadataType {
2702
2777
 
2703
2778
  return newKey;
2704
2779
  }
2780
+
2781
+ /**
2782
+ * @typedef {'off'|'warn'|'error'} ValidationLevel
2783
+ */
2784
+ /**
2785
+ * @typedef {object} ValidationRules
2786
+ * @property {ValidationLevel} [noGuidKeys] flags metadata that did not get a proper key
2787
+ * @property {ValidationLevel} [noRootFolder] flags metadata that did not get a proper key
2788
+ * @property {{type:string[], options: ValidationRules}[]} [overrides] flags metadata that did not get a proper key
2789
+ */
2790
+
2791
+ /**
2792
+ * Gets executed before deploying metadata
2793
+ *
2794
+ * @param {'retrieve'|'buildDefinition'|'deploy'} method used to select the right config
2795
+ * @param {MetadataTypeItem | CodeExtractItem} item a single metadata item
2796
+ * @param {string} targetDir folder where files for deployment are stored
2797
+ * @returns {Promise.<MetadataTypeItem | CodeExtractItem>} Promise of a single metadata item
2798
+ */
2799
+ static async validation(method, item, targetDir) {
2800
+ if (!this.properties.options?.validation?.[method]) {
2801
+ return item;
2802
+ }
2803
+ /** @type {MetadataTypeItem} */
2804
+ const metadataItem = item.json && item.codeArr ? item.json : item;
2805
+ /** @type {ValidationRules} */
2806
+ const validationConfig = structuredClone(this.properties.options?.validation?.[method]);
2807
+
2808
+ // check if the config contains overrides for the current type
2809
+ const overrides = Array.isArray(validationConfig.overrides)
2810
+ ? validationConfig.overrides
2811
+ : [validationConfig.overrides];
2812
+ if (validationConfig.overrides) {
2813
+ delete validationConfig.overrides;
2814
+ for (const override of overrides) {
2815
+ if (
2816
+ override.type?.includes(this.definition.type) &&
2817
+ override.options &&
2818
+ Object.keys(override.options)
2819
+ ) {
2820
+ Object.assign(validationConfig, override.options);
2821
+ }
2822
+ }
2823
+ }
2824
+
2825
+ // get default and custom validation rules
2826
+ const definition = this.definition;
2827
+ const validationRules = await validationsRules(this.definition, metadataItem, targetDir);
2828
+
2829
+ // run validation rules
2830
+ for (const rule of Object.keys(validationRules)) {
2831
+ if (
2832
+ validationConfig[rule] &&
2833
+ validationConfig[rule] !== 'off' &&
2834
+ !this.definition.skipValidation?.[rule] &&
2835
+ !(await validationRules[rule].passed())
2836
+ ) {
2837
+ if (!Util.OPTIONS.skipValidation && validationConfig[rule] === 'error') {
2838
+ throw new Error(validationRules[rule].failedMsg);
2839
+ } else if (Util.OPTIONS.skipValidation || validationConfig[rule] === 'warn') {
2840
+ Util.logger.warn(
2841
+ ` - ${this.definition.type} ${metadataItem[this.definition.nameField]} (${
2842
+ metadataItem[this.definition.keyField]
2843
+ }): ${validationRules[rule].failedMsg}`
2844
+ );
2845
+ }
2846
+ }
2847
+ }
2848
+ return item;
2849
+ }
2705
2850
  }
2706
2851
 
2707
2852
  MetadataType.definition = {
@@ -73,41 +73,6 @@ class Role extends MetadataType {
73
73
  const results = await this.client.soap.retrieve('Role', fields, requestParams);
74
74
 
75
75
  const parsed = this.parseResponseBody(results);
76
- if (!retrieveDir) {
77
- // retrieve "Marketing Cloud%" roles not returned by SOAP API
78
- const { roles, timeZones } = await this.client.rest.get(
79
- '/platform/v1/setup/quickflow/data'
80
- );
81
- // this endpoint does not provide keys
82
- const roleNameKeyMap = {
83
- 'Marketing Cloud Administrator': 'SYS_DEF_IMHADMIN',
84
- 'Marketing Cloud Channel Manager': 'SYS_DEF_CHANNELMANAGER',
85
- 'Marketing Cloud Content Editor/Publisher': 'SYS_DEF_CONTENTEDIT',
86
- 'Marketing Cloud Security Administrator': 'SYS_DEF_SECURITYADMIN',
87
- 'Marketing Cloud Viewer': 'SYS_DEF_VIEWER',
88
- };
89
- for (const role of roles) {
90
- if (roleNameKeyMap[role.roleName]) {
91
- parsed[roleNameKeyMap[role.roleName]] = {
92
- CustomerKey: roleNameKeyMap[role.roleName],
93
- Name: role.roleName,
94
- ObjectID: role.roleID,
95
- Desscription: role.description,
96
- CreatedDate: '2012-02-21T02:09:19.983',
97
- IsSystemDefined: true,
98
- c__notAssignable: true,
99
- };
100
- }
101
- }
102
- // the languages object is incomplete. the actual list is much longer --> ignoring it here
103
- // convert timeZones to object
104
- const timeZonesObj = {};
105
- for (const timeZone of timeZones) {
106
- timeZonesObj[timeZone.id] = timeZone;
107
- }
108
- // cache timeZones to share it with other type-classes
109
- cache.setMetadata('_timezone', timeZonesObj);
110
- }
111
76
  if (retrieveDir) {
112
77
  const savedMetadata = await super.saveResults(parsed, retrieveDir, null);
113
78
  Util.logger.info(
@@ -117,9 +82,56 @@ class Role extends MetadataType {
117
82
 
118
83
  await this.runDocumentOnRetrieve(key, savedMetadata);
119
84
  }
85
+
86
+ await this.cacheDefaultRolesAndTimezones(parsed);
87
+
120
88
  return { metadata: parsed, type: this.definition.type };
121
89
  }
122
90
 
91
+ /**
92
+ * adds default roles to the list of retrieved roles for proper caching (but not storing)
93
+ * also caches available timezones for retrieve-user
94
+ *
95
+ * @param {MetadataTypeMap} parsed list or previously retrieved items as reference
96
+ */
97
+ static async cacheDefaultRolesAndTimezones(parsed) {
98
+ // retrieve "Marketing Cloud%" roles not returned by SOAP API for cache (required by retrieve-user)
99
+ // also cache available timezones for retrieve-user
100
+ Util.logger.info(Util.getGrayMsg(' - Caching default roles and timezones'));
101
+ const { roles, timeZones } = await this.client.rest.get(
102
+ '/platform/v1/setup/quickflow/data'
103
+ );
104
+ // this endpoint does not provide keys
105
+ const roleNameKeyMap = {
106
+ 'Marketing Cloud Administrator': 'SYS_DEF_IMHADMIN',
107
+ 'Marketing Cloud Channel Manager': 'SYS_DEF_CHANNELMANAGER',
108
+ 'Marketing Cloud Content Editor/Publisher': 'SYS_DEF_CONTENTEDIT',
109
+ 'Marketing Cloud Security Administrator': 'SYS_DEF_SECURITYADMIN',
110
+ 'Marketing Cloud Viewer': 'SYS_DEF_VIEWER',
111
+ };
112
+ for (const role of roles) {
113
+ if (roleNameKeyMap[role.roleName]) {
114
+ parsed[roleNameKeyMap[role.roleName]] = {
115
+ CustomerKey: roleNameKeyMap[role.roleName],
116
+ Name: role.roleName,
117
+ ObjectID: role.roleID,
118
+ Desscription: role.description,
119
+ CreatedDate: '2012-02-21T02:09:19.983',
120
+ IsSystemDefined: true,
121
+ c__notAssignable: true,
122
+ };
123
+ }
124
+ }
125
+ // the languages object is incomplete. the actual list is much longer --> ignoring it here
126
+ // convert timeZones to object
127
+ const timeZonesObj = {};
128
+ for (const timeZone of timeZones) {
129
+ timeZonesObj[timeZone.id] = timeZone;
130
+ }
131
+ // cache timeZones to share it with other type-classes
132
+ cache.setMetadata('_timezone', timeZonesObj);
133
+ }
134
+
123
135
  /**
124
136
  * Gets executed before deploying metadata
125
137
  *
@@ -374,7 +374,6 @@ const Init = {
374
374
  * @returns {Promise.<boolean>} install successful or error occured
375
375
  */
376
376
  async _createIdeConfigFile(fileNameArr, relevantForced, boilerplateFileContent) {
377
- let update = false;
378
377
  const fileName = fileNameArr.join('');
379
378
  const boilerplateFileName = path.resolve(
380
379
  __dirname,
@@ -409,17 +408,20 @@ const Init = {
409
408
  Util.logger.info(
410
409
  `- ✋ ${fileName} found with differences to the new standard version. We recommend updating it.`
411
410
  );
412
- if (!Util.skipInteraction) {
411
+ if (Util.skipInteraction) {
412
+ todo = 'update';
413
+ } else {
413
414
  const overrideFile = await confirm({
414
415
  message: 'Would you like to update (override) it?',
415
416
  default: true,
416
417
  });
417
- if (!overrideFile) {
418
+ if (overrideFile) {
419
+ todo = 'update';
420
+ } else {
418
421
  // skip override without error
419
422
  return true;
420
423
  }
421
424
  }
422
- update = true;
423
425
  }
424
426
 
425
427
  // ensure our update is not leading to data loss in case config files were not versioned correctly by the user
@@ -438,14 +440,16 @@ const Init = {
438
440
  if (saveStatus) {
439
441
  Util.logger.info(
440
442
  `- ✔️ ${fileName} ${
441
- update
443
+ todo === 'update'
442
444
  ? `updated (we created a backup of the old file under ${fileName + '.BAK'})`
443
445
  : 'created'
444
446
  }`
445
447
  );
446
448
  return true;
447
449
  } else {
448
- Util.logger.warn(`- ❌ ${fileName} ${update ? 'update' : 'creation'} failed`);
450
+ Util.logger.warn(
451
+ `- ❌ ${fileName} ${todo === 'update' ? 'update' : 'creation'} failed`
452
+ );
449
453
  return false;
450
454
  }
451
455
  } else if (todo === 'delete') {
@@ -208,50 +208,54 @@ export default class ReplaceContentBlockReference {
208
208
  * ensures we cache the right things from disk and if required from server
209
209
  *
210
210
  * @param {Mcdevrc} properties properties for auth
211
- saved
212
211
  * @param {BuObject} buObject properties for auth
213
212
  * @param {boolean} [retrieveSharedOnly] for --dependencies only, do not have to re-retrieve local assets
214
213
  * @returns {Promise.<void>} -
215
214
  */
216
- static async createCacheMap(properties, buObject, retrieveSharedOnly = false) {
215
+ static async createCache(properties, buObject, retrieveSharedOnly = false) {
217
216
  const { localAssets, sharedAssets } = await ReplaceContentBlockReference._retrieveCache(
218
217
  buObject,
219
218
  properties,
220
219
  retrieveSharedOnly
221
220
  );
222
221
 
223
- ReplaceContentBlockReference._createCacheMap(localAssets);
224
- ReplaceContentBlockReference._createCacheMap(sharedAssets);
222
+ ReplaceContentBlockReference.createCacheForMap(localAssets);
223
+ ReplaceContentBlockReference.createCacheForMap(sharedAssets);
225
224
  }
226
225
 
227
226
  /**
228
- * helper for {@link this.createCacheMap} that converts AssetMap into Asset
227
+ * helper for {@link ReplaceContentBlockReference.createCache} that converts AssetMap into AssetItemSimple entries in this.assetCacheMap
229
228
  *
230
229
  * @param {AssetMap} metadataMap list of local or shared assets
231
230
  */
232
- static _createCacheMap(metadataMap) {
231
+ static createCacheForMap(metadataMap) {
233
232
  for (const element of Object.values(metadataMap)) {
234
233
  // create actual cache map
235
234
  /** @type {AssetItemSimple} */
236
235
  const simpleAsset = {
237
236
  id: element.id,
238
237
  key: element.customerKey,
238
+ // ! note that ContentBlockByName expects backslashes between folders and file name, not forward slashes
239
239
  name: element.r__folder_Path
240
240
  ? element.r__folder_Path.replaceAll('/', '\\') + '\\' + element.name
241
241
  : null,
242
242
  };
243
- // ! note that ContentBlockByName expects backslashes between folders and file name, not forward slashes
244
- this.assetCacheMap.id[simpleAsset.id] = simpleAsset;
245
- this.assetCacheMap.key[simpleAsset.key] = simpleAsset;
243
+ // if this method was filled by Asset.upsert it might have been run before with more accurate (retrieved) data including the id that we do not want to override
244
+ this.assetCacheMap.key[simpleAsset.key] ||= simpleAsset;
245
+ if (simpleAsset.id) {
246
+ // if this method was filled by Asset.upsert it won't have ids
247
+ this.assetCacheMap.id[simpleAsset.id] = simpleAsset;
248
+ }
246
249
  if (simpleAsset.name) {
247
250
  // while asset without path could still be found via search, it would no longer referencable via ContentBlockByName
248
- this.assetCacheMap.name[simpleAsset.name] = simpleAsset;
251
+ // if this method was filled by Asset.upsert it might have been run before with more accurate (retrieved) data including the id that we do not want to override
252
+ this.assetCacheMap.name[simpleAsset.name] ||= simpleAsset;
249
253
  }
250
254
  }
251
255
  }
252
256
 
253
257
  /**
254
- * helper for {@link this.createCacheMap}
258
+ * helper for {@link ReplaceContentBlockReference.createCache}
255
259
  *
256
260
  * @param {BuObject} buObject references credentials
257
261
  * @param {Mcdevrc} properties central properties object
package/lib/util/util.js CHANGED
@@ -53,6 +53,7 @@ export const Util = {
53
53
  packageJsonMcdev: readJsonSync(path.join(__dirname, '../../package.json')),
54
54
  OPTIONS: {},
55
55
  changedKeysMap: {},
56
+ matchedByName: {},
56
57
 
57
58
  /**
58
59
  * helper that allows filtering an object by its keys
@@ -575,39 +576,38 @@ export const Util = {
575
576
  /**
576
577
  * Returns Order in which metadata needs to be retrieved/deployed
577
578
  *
578
- * @param {string[]} metadataTypes which should be retrieved/deployed
579
+ * @param {string[]} typeArr which should be retrieved/deployed
579
580
  * @returns {Object.<string, string[]>} retrieve/deploy order as array
580
581
  */
581
- getMetadataHierachy(metadataTypes) {
582
+ getMetadataHierachy(typeArr) {
582
583
  const dependencies = [];
583
584
  // loop through all metadata types which are being retrieved/deployed
584
585
  const subTypeDeps = {};
585
- for (const metadataType of metadataTypes) {
586
- const type = metadataType.split('-')[0];
586
+ for (const typeSubType of typeArr) {
587
+ const type = typeSubType.split('-')[0];
587
588
  // if they have dependencies then add a dependency pair for each type
588
589
  if (MetadataDefinitions[type].dependencies.length > 0) {
589
590
  dependencies.push(
590
591
  ...MetadataDefinitions[type].dependencies.map((dep) => {
591
592
  if (dep.includes('-')) {
592
593
  // log subtypes to be able to replace them if main type is also present
593
- subTypeDeps[dep.split('-')[0]] =
594
- subTypeDeps[dep.split('-')[0]] || new Set();
594
+ subTypeDeps[dep.split('-')[0]] ||= new Set();
595
595
  subTypeDeps[dep.split('-')[0]].add(dep);
596
596
  }
597
- return [dep, metadataType];
597
+ return [dep, typeSubType];
598
598
  })
599
599
  );
600
600
  }
601
601
  // if they have no dependencies then just add them with undefined.
602
602
  else {
603
- dependencies.push([undefined, metadataType]);
603
+ dependencies.push([undefined, typeSubType]);
604
604
  }
605
605
  }
606
606
  const allDeps = dependencies.map((dep) => dep[0]);
607
607
  // remove subtypes if main type is in the list
608
608
  for (const type of Object.keys(subTypeDeps)
609
609
  // only look at subtype deps that are also supposed to be retrieved or cached fully
610
- .filter((type) => metadataTypes.includes(type) || allDeps.includes(type))) {
610
+ .filter((type) => typeArr.includes(type) || allDeps.includes(type))) {
611
611
  // convert set into array to walk its elements
612
612
  for (const subType of subTypeDeps[type]) {
613
613
  for (const item of dependencies) {
@@ -1118,6 +1118,26 @@ export const Util = {
1118
1118
  ? []
1119
1119
  : [array.splice(0, chunk_size)].concat(this.chunk(array, chunk_size));
1120
1120
  },
1121
+ /**
1122
+ * recursively find all values of the given key in the object
1123
+ *
1124
+ * @param {any} object data to search in
1125
+ * @param {string} key attribute to find
1126
+ * @returns {Array} all values of the given key
1127
+ */
1128
+ findLeafVals(object, key) {
1129
+ const values = [];
1130
+ Object.keys(object).map((k) => {
1131
+ if (k === key) {
1132
+ values.push(object[k]);
1133
+ return true;
1134
+ }
1135
+ if (object[k] && typeof object[k] === 'object') {
1136
+ values.push(...this.findLeafVals(object[k], key));
1137
+ }
1138
+ });
1139
+ return [...new Set(values.sort())];
1140
+ },
1121
1141
  };
1122
1142
 
1123
1143
  Util.startLogger(false, true);