mcdev 7.3.1 → 7.4.1
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/@types/lib/Deployer.d.ts.map +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 +39 -7
- package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
- package/@types/lib/metadataTypes/Journey.d.ts +4 -3
- 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/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 +18 -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/@types/types/mcdev.d.d.ts +195 -0
- package/@types/types/mcdev.d.d.ts.map +1 -1
- package/boilerplate/config.json +22 -0
- package/boilerplate/files/.gitattributes +1 -1
- package/boilerplate/files/README.md +1 -1
- package/boilerplate/forcedUpdates.json +8 -0
- package/boilerplate/gitignore-template +1 -0
- package/lib/Deployer.js +5 -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 -17
- package/lib/metadataTypes/DataExtensionField.js +11 -3
- package/lib/metadataTypes/Event.js +171 -105
- package/lib/metadataTypes/Journey.js +207 -89
- package/lib/metadataTypes/MetadataType.js +182 -37
- package/lib/metadataTypes/definitions/TriggeredSend.definition.js +4 -2
- package/lib/util/config.js +4 -4
- package/lib/util/init.config.js +10 -6
- package/lib/util/replaceContentBlockReference.js +15 -11
- package/lib/util/util.js +43 -9
- package/lib/util/validations.js +66 -0
- package/package.json +8 -8
- package/test/general.test.js +4 -4
- package/test/mockRoot/.mcdevrc.json +15 -1
- package/test/mockRoot/deploy/testInstance/testBU/triggeredSend/testExisting_triggeredSend.triggeredSend-meta.json +0 -1
- package/test/mockRoot/deploy/testInstance/testBU/triggeredSend/testNew_triggeredSend.triggeredSend-meta.json +0 -1
- package/test/resources/9999999/journey/build-expected.json +4 -2
- package/test/resources/9999999/journey/get-multistep-expected.json +61 -61
- package/test/resources/9999999/journey/get-quicksend-expected.json +4 -2
- package/test/resources/9999999/journey/get-quicksend-rcb-id-expected.json +2 -2
- package/test/resources/9999999/journey/get-quicksend-rcb-key-expected.json +2 -2
- package/test/resources/9999999/journey/get-quicksend-rcb-name-expected.json +6 -2
- package/test/resources/9999999/journey/get-transactionalEmail-expected.json +2 -2
- package/test/resources/9999999/journey/template-expected.json +4 -2
- package/test/resources/9999999/triggeredSend/build-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/get-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/get-rcb-id-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/get-rcb-key-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/get-rcb-name-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/patch-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/post-expected.json +0 -1
- package/test/resources/9999999/triggeredSend/template-expected.json +0 -1
- package/test/resources/9999999/triggeredSendDefinition/create-response.xml +0 -1
- package/test/resources/9999999/triggeredSendDefinition/retrieve-TriggeredSendStatusINNew,Active,Inactive,Moved,Canceled-response.xml +0 -2
- package/test/resources/9999999/triggeredSendDefinition/update-response.xml +0 -1
- package/test/type.dataExtension.test.js +3 -3
- package/test/type.journey.test.js +2 -2
- package/test/utils.js +1 -0
- package/types/mcdev.d.js +66 -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 = {
|
|
@@ -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:
|
|
73
|
-
templating:
|
|
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,
|
package/lib/util/config.js
CHANGED
|
@@ -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/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,40 @@ 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
|
+
},
|
|
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
|
+
},
|
|
1121
1155
|
};
|
|
1122
1156
|
|
|
1123
1157
|
Util.startLogger(false, true);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Util } from './util.js';
|
|
4
|
+
|
|
5
|
+
let customRules = {};
|
|
6
|
+
let customRuleImport;
|
|
7
|
+
try {
|
|
8
|
+
customRuleImport = await import('file://' + path.join(process.cwd(), '.mcdev-validations.js'));
|
|
9
|
+
} catch {
|
|
10
|
+
Util.logger.debug('.mcdev-validations.js not found');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {any} definition type definition
|
|
15
|
+
* @param {any} item MetadataItem
|
|
16
|
+
* @returns {Promise.<any>} MetadataItem
|
|
17
|
+
* @param {string} targetDir folder in which the MetadataItem is deployed from (deploy/cred/bu)
|
|
18
|
+
*/
|
|
19
|
+
export default async function validation(definition, item, targetDir) {
|
|
20
|
+
try {
|
|
21
|
+
if (customRuleImport) {
|
|
22
|
+
customRules = customRuleImport
|
|
23
|
+
? await customRuleImport.validation(definition, item, targetDir, Util)
|
|
24
|
+
: {};
|
|
25
|
+
}
|
|
26
|
+
} catch (ex) {
|
|
27
|
+
Util.logger.error(
|
|
28
|
+
'Could not load custom validation rules from .mcdev-validations.js: ' + ex.message
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const defaultRules = {
|
|
32
|
+
noGuidKeys: {
|
|
33
|
+
failedMsg: 'Please update the key to a readable value. Currently still in GUID format.',
|
|
34
|
+
/**
|
|
35
|
+
* @returns {boolean} true=test passed
|
|
36
|
+
*/
|
|
37
|
+
passed: function () {
|
|
38
|
+
const key = item[definition.keyField];
|
|
39
|
+
if (key) {
|
|
40
|
+
const regex = /^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/i;
|
|
41
|
+
return !regex.test(String(key).toLowerCase());
|
|
42
|
+
} else {
|
|
43
|
+
Util.logger.debug('validation-noGuidKeys: key not found');
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
noRootFolder: {
|
|
49
|
+
failedMsg: 'Root folder not allowed. Current folder: ' + item.r__folder_Path,
|
|
50
|
+
/**
|
|
51
|
+
* @returns {boolean} true=test passed
|
|
52
|
+
*/
|
|
53
|
+
passed: function () {
|
|
54
|
+
/** @type {string} */
|
|
55
|
+
const folderPath = item.r__folder_Path;
|
|
56
|
+
if (!folderPath) {
|
|
57
|
+
// some types do not support folders
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return folderPath.includes('/');
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
return Object.assign({}, defaultRules, customRules);
|
|
66
|
+
}
|