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.
- package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
- package/.github/PULL_REQUEST_TEMPLATE/pr_template_release.md +3 -3
- package/.vscode/settings.json +1 -1
- package/@types/lib/index.d.ts.map +1 -1
- package/@types/lib/metadataTypes/Asset.d.ts +25 -0
- package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
- package/@types/lib/metadataTypes/DataExtension.d.ts +3 -2
- package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
- package/@types/lib/metadataTypes/DataExtensionField.d.ts.map +1 -1
- package/@types/lib/metadataTypes/Event.d.ts +33 -0
- package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
- package/@types/lib/metadataTypes/Journey.d.ts +4 -2
- package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
- package/@types/lib/metadataTypes/MetadataType.d.ts +22 -3
- package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
- package/@types/lib/metadataTypes/Role.d.ts +7 -0
- package/@types/lib/metadataTypes/Role.d.ts.map +1 -1
- package/@types/lib/util/init.config.d.ts.map +1 -1
- package/@types/lib/util/replaceContentBlockReference.d.ts +4 -5
- package/@types/lib/util/replaceContentBlockReference.d.ts.map +1 -1
- package/@types/lib/util/util.d.ts +11 -2
- package/@types/lib/util/util.d.ts.map +1 -1
- package/@types/lib/util/validations.d.ts +9 -0
- package/@types/lib/util/validations.d.ts.map +1 -0
- package/boilerplate/config.json +22 -0
- package/boilerplate/files/.gitattributes +1 -1
- package/boilerplate/files/.vscode/settings.json +3 -2
- package/boilerplate/files/README.md +1 -1
- package/boilerplate/forcedUpdates.json +8 -0
- package/lib/cli.js +28 -3
- package/lib/index.js +3 -2
- package/lib/metadataTypes/Asset.js +87 -7
- package/lib/metadataTypes/DataExtension.js +74 -4
- package/lib/metadataTypes/DataExtensionField.js +7 -1
- package/lib/metadataTypes/Event.js +254 -200
- package/lib/metadataTypes/Journey.js +242 -132
- package/lib/metadataTypes/MetadataType.js +182 -37
- package/lib/metadataTypes/Role.js +47 -35
- package/lib/util/init.config.js +10 -6
- package/lib/util/replaceContentBlockReference.js +15 -11
- package/lib/util/util.js +29 -9
- package/lib/util/validations.js +66 -0
- package/package.json +7 -2
- package/test/general.test.js +5 -2
- package/test/mockRoot/.mcdevrc.json +15 -1
- package/test/type.journey.test.js +11 -11
- package/test/utils.js +1 -0
- /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}
|
|
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(
|
|
136
|
-
const
|
|
137
|
-
const
|
|
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(
|
|
145
|
+
this.document(savedMetadataMap, true);
|
|
145
146
|
}
|
|
146
|
-
return
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
773
|
-
Util.
|
|
774
|
-
|
|
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
|
-
|
|
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
|
*
|
package/lib/util/init.config.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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(
|
|
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
|
|
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.
|
|
224
|
-
ReplaceContentBlockReference.
|
|
222
|
+
ReplaceContentBlockReference.createCacheForMap(localAssets);
|
|
223
|
+
ReplaceContentBlockReference.createCacheForMap(sharedAssets);
|
|
225
224
|
}
|
|
226
225
|
|
|
227
226
|
/**
|
|
228
|
-
* helper for {@link
|
|
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
|
|
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
|
-
//
|
|
244
|
-
this.assetCacheMap.
|
|
245
|
-
|
|
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.
|
|
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
|
|
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[]}
|
|
579
|
+
* @param {string[]} typeArr which should be retrieved/deployed
|
|
579
580
|
* @returns {Object.<string, string[]>} retrieve/deploy order as array
|
|
580
581
|
*/
|
|
581
|
-
getMetadataHierachy(
|
|
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
|
|
586
|
-
const type =
|
|
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,
|
|
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,
|
|
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) =>
|
|
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);
|