diff --git a/docs/dist/documentation.md b/docs/dist/documentation.md index d0f6ea335..941cc81cb 100644 --- a/docs/dist/documentation.md +++ b/docs/dist/documentation.md @@ -132,6 +132,9 @@ Provides default functionality that can be overwritten by child metadata type cl
Cli

CLI helper class

+
config
+

Central class for loading and validating properties from config and auth

+
DevOps

DevOps helper class

@@ -466,8 +469,8 @@ main class * [.upgrade([skipInteraction])](#Mcdev.upgrade) ⇒ Promise.<boolean> * [.retrieve(businessUnit, [selectedTypesArr], [keys], [changelogOnly])](#Mcdev.retrieve) ⇒ Promise.<object> * [.deploy(businessUnit, [selectedTypesArr], [keyArr], [fromRetrieve])](#Mcdev.deploy) ⇒ Promise.<void> - * [.initProject([credentialsName], [skipInteraction])](#Mcdev.initProject) ⇒ Promise.<void> - * [.findBUs(credentialsName)](#Mcdev.findBUs) ⇒ Promise.<void> + * [.initProject([credentialName], [skipInteraction])](#Mcdev.initProject) ⇒ Promise.<void> + * [.findBUs(credentialName)](#Mcdev.findBUs) ⇒ Promise.<void> * [.document(businessUnit, type)](#Mcdev.document) ⇒ Promise.<void> * [.deleteByKey(businessUnit, type, customerKey)](#Mcdev.deleteByKey) ⇒ Promise.<void> * [.badKeys(businessUnit)](#Mcdev.badKeys) ⇒ Promise.<void> @@ -568,7 +571,7 @@ Deploys all metadata located in the 'deploy' directory to the specified business -### Mcdev.initProject([credentialsName], [skipInteraction]) ⇒ Promise.<void> +### Mcdev.initProject([credentialName], [skipInteraction]) ⇒ Promise.<void> Creates template file for properties.json **Kind**: static method of [Mcdev](#Mcdev) @@ -576,12 +579,12 @@ Creates template file for properties.json | Param | Type | Description | | --- | --- | --- | -| [credentialsName] | string | identifying name of the installed package / project | +| [credentialName] | string | identifying name of the installed package / project | | [skipInteraction] | boolean \| TYPE.skipInteraction | signals what to insert automatically for things usually asked via wizard | -### Mcdev.findBUs(credentialsName) ⇒ Promise.<void> +### Mcdev.findBUs(credentialName) ⇒ Promise.<void> Refreshes BU names and ID's from MC instance **Kind**: static method of [Mcdev](#Mcdev) @@ -589,7 +592,7 @@ Refreshes BU names and ID's from MC instance | Param | Type | Description | | --- | --- | --- | -| credentialsName | string | identifying name of the installed package / project | +| credentialName | string | identifying name of the installed package / project | @@ -4395,9 +4398,7 @@ CLI entry for SFMC DevTools * [.isTrue(attrValue)](#Util.isTrue) ⇒ boolean * [.isFalse(attrValue)](#Util.isFalse) ⇒ boolean * [._isValidType(selectedType)](#Util._isValidType) ⇒ boolean - * [.getDefaultProperties()](#Util.getDefaultProperties) ⇒ TYPE.Mcdevrc - * [.getRetrieveTypeChoices()](#Util.getRetrieveTypeChoices) ⇒ Array.<string> - * [.checkProperties(properties, [silent])](#Util.checkProperties) ⇒ Promise.<(boolean\|Array.<string>)> + * [.getRetrieveTypeChoices()](#Util.getRetrieveTypeChoices) ⇒ Array.<TYPE.SupportedMetadataTypes> * [.metadataLogger(level, type, method, payload, [source])](#Util.metadataLogger) ⇒ void * [.replaceByObject(str, obj)](#Util.replaceByObject) ⇒ string \| object * [.inverseGet(objs, val)](#Util.inverseGet) ⇒ string @@ -4522,36 +4523,15 @@ helper for retrieve, retrieveAsTemplate and deploy | Param | Type | Description | | --- | --- | --- | -| selectedType | string | type or type-subtype | - - - -### Util.getDefaultProperties() ⇒ TYPE.Mcdevrc -defines how the properties.json should look like -used for creating a template and for checking if variables are set +| selectedType | TYPE.SupportedMetadataTypes | type or type-subtype | -**Kind**: static method of [Util](#Util) -**Returns**: TYPE.Mcdevrc - default properties -### Util.getRetrieveTypeChoices() ⇒ Array.<string> +### Util.getRetrieveTypeChoices() ⇒ Array.<TYPE.SupportedMetadataTypes> helper for getDefaultProperties() **Kind**: static method of [Util](#Util) -**Returns**: Array.<string> - type choices - - -### Util.checkProperties(properties, [silent]) ⇒ Promise.<(boolean\|Array.<string>)> -check if the config file is correctly formatted and has values - -**Kind**: static method of [Util](#Util) -**Returns**: Promise.<(boolean\|Array.<string>)> - file structure ok OR list of fields to be fixed - -| Param | Type | Description | -| --- | --- | --- | -| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | -| [silent] | boolean | set to true for internal use w/o cli output | - +**Returns**: Array.<TYPE.SupportedMetadataTypes> - type choices ### Util.metadataLogger(level, type, method, payload, [source]) ⇒ void @@ -4697,7 +4677,7 @@ Helper that handles retrieval of BU info **Kind**: global constant -### BusinessUnit.refreshBUProperties(properties, credentialsName) ⇒ Promise.<boolean> +### BusinessUnit.refreshBUProperties(properties, credentialName) ⇒ Promise.<boolean> Refreshes BU names and ID's from MC instance **Kind**: static method of [BusinessUnit](#BusinessUnit) @@ -4706,7 +4686,7 @@ Refreshes BU names and ID's from MC instance | Param | Type | Description | | --- | --- | --- | | properties | TYPE.Mcdevrc | current properties that have to be refreshed | -| credentialsName | string | identifying name of the installed package / project | +| credentialName | string | identifying name of the installed package / project | @@ -4862,6 +4842,64 @@ this keeps the config automatically upgradable when we add new subtypes or chang shows metadata type descriptions **Kind**: static method of [Cli](#Cli) + + +## config +Central class for loading and validating properties from config and auth + +**Kind**: global constant + +* [config](#config) + * [.getProperties([skipChecks])](#config.getProperties) ⇒ object + * [.checkProperties(properties, [skipCredentialValidation])](#config.checkProperties) ⇒ Promise.<(boolean\|Array.<string>)> + * [.getDefaultProperties()](#config.getDefaultProperties) ⇒ TYPE.Mcdevrc + * [.getProblems(properties, [defaultProps], [skipCredentialValidation])](#config.getProblems) ⇒ Promise.<{missingFields: Array.<string>, errorMsgs: Array.<string>, solutionSet: Set.<string>}> + + + +### config.getProperties([skipChecks]) ⇒ object +loads central properties from config file + +**Kind**: static method of [config](#config) +**Returns**: object - central properties object + +| Param | Type | Description | +| --- | --- | --- | +| [skipChecks] | boolean | omit throwing errors and print messages | + + + +### config.checkProperties(properties, [skipCredentialValidation]) ⇒ Promise.<(boolean\|Array.<string>)> +check if the config file is correctly formatted and has values + +**Kind**: static method of [config](#config) +**Returns**: Promise.<(boolean\|Array.<string>)> - file structure ok OR list of fields to be fixed + +| Param | Type | Description | +| --- | --- | --- | +| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | +| [skipCredentialValidation] | boolean | set to true for internal use w/o cli output | + + + +### config.getDefaultProperties() ⇒ TYPE.Mcdevrc +defines how the properties.json should look like +used for creating a template and for checking if variables are set + +**Kind**: static method of [config](#config) +**Returns**: TYPE.Mcdevrc - default properties + + +### config.getProblems(properties, [defaultProps], [skipCredentialValidation]) ⇒ Promise.<{missingFields: Array.<string>, errorMsgs: Array.<string>, solutionSet: Set.<string>}> +**Kind**: static method of [config](#config) +**Returns**: Promise.<{missingFields: Array.<string>, errorMsgs: Array.<string>, solutionSet: Set.<string>}> - - + +| Param | Type | Description | +| --- | --- | --- | +| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | +| [defaultProps] | TYPE.Mcdevrc | default properties | +| [skipCredentialValidation] | boolean | used by init.config.js>fixMcdevConfig() to auto-fix the config file | + ## DevOps @@ -4968,7 +5006,6 @@ File extends fs-extra. It adds logger and util methods for file handling * [.readFilteredFilename(directory, filename, filetype, [encoding])](#File.readFilteredFilename) ⇒ Promise.<string> * [.readDirectories(directory, depth, [includeStem], [_stemLength])](#File.readDirectories) ⇒ Promise.<Array.<string>> * [.readDirectoriesSync(directory, [depth], [includeStem], [_stemLength])](#File.readDirectoriesSync) ⇒ Array.<string> - * [.loadConfigFile([silent])](#File.loadConfigFile) ⇒ TYPE.Mcdevrc * [.saveConfigFile(properties)](#File.saveConfigFile) ⇒ Promise.<void> * [.initPrettier([filetype])](#File.initPrettier) ⇒ Promise.<boolean> @@ -5167,18 +5204,6 @@ TODO - merge with readDirectories. so far the logic is really different ```js ['deploy/mcdev/bu1'] ``` - - -### File.loadConfigFile([silent]) ⇒ TYPE.Mcdevrc -loads central properties from config file - -**Kind**: static method of [File](#File) -**Returns**: TYPE.Mcdevrc - central properties object - -| Param | Type | Description | -| --- | --- | --- | -| [silent] | boolean | omit throwing errors and print messages; assuming not silent if not set | - ### File.saveConfigFile(properties) ⇒ Promise.<void> @@ -5225,7 +5250,7 @@ CLI helper class * [.initProject(properties, credentialName, [skipInteraction])](#Init.initProject) ⇒ Promise.<void> * [._downloadAllBUs(bu, gitStatus, [skipInteraction])](#Init._downloadAllBUs) ⇒ Promise.<void> * [.upgradeProject(properties, [initial], [repoName])](#Init.upgradeProject) ⇒ Promise.<boolean> - * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Array.<string> + * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Promise.<Array.<string>> * [.installDependencies([repoName])](#Init.installDependencies) ⇒ Promise.<boolean> * [._getDefaultPackageJson([currentContent])](#Init._getDefaultPackageJson) ⇒ Promise.<{script: object, author: string, license: string}> @@ -5396,15 +5421,15 @@ wrapper around npm dependency & configuration file setup -### Init.\_getMissingCredentials(properties) ⇒ Array.<string> +### Init.\_getMissingCredentials(properties) ⇒ Promise.<Array.<string>> finds credentials that are set up in config but not in auth file **Kind**: static method of [Init](#Init) -**Returns**: Array.<string> - list of credential names +**Returns**: Promise.<Array.<string>> - list of credential names | Param | Type | Description | | --- | --- | --- | -| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | +| properties | object | javascript object in .mcdevrc.json | @@ -5454,7 +5479,7 @@ CLI helper class * [.initProject(properties, credentialName, [skipInteraction])](#Init.initProject) ⇒ Promise.<void> * [._downloadAllBUs(bu, gitStatus, [skipInteraction])](#Init._downloadAllBUs) ⇒ Promise.<void> * [.upgradeProject(properties, [initial], [repoName])](#Init.upgradeProject) ⇒ Promise.<boolean> - * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Array.<string> + * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Promise.<Array.<string>> * [.installDependencies([repoName])](#Init.installDependencies) ⇒ Promise.<boolean> * [._getDefaultPackageJson([currentContent])](#Init._getDefaultPackageJson) ⇒ Promise.<{script: object, author: string, license: string}> @@ -5625,15 +5650,15 @@ wrapper around npm dependency & configuration file setup -### Init.\_getMissingCredentials(properties) ⇒ Array.<string> +### Init.\_getMissingCredentials(properties) ⇒ Promise.<Array.<string>> finds credentials that are set up in config but not in auth file **Kind**: static method of [Init](#Init) -**Returns**: Array.<string> - list of credential names +**Returns**: Promise.<Array.<string>> - list of credential names | Param | Type | Description | | --- | --- | --- | -| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | +| properties | object | javascript object in .mcdevrc.json | @@ -5683,7 +5708,7 @@ CLI helper class * [.initProject(properties, credentialName, [skipInteraction])](#Init.initProject) ⇒ Promise.<void> * [._downloadAllBUs(bu, gitStatus, [skipInteraction])](#Init._downloadAllBUs) ⇒ Promise.<void> * [.upgradeProject(properties, [initial], [repoName])](#Init.upgradeProject) ⇒ Promise.<boolean> - * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Array.<string> + * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Promise.<Array.<string>> * [.installDependencies([repoName])](#Init.installDependencies) ⇒ Promise.<boolean> * [._getDefaultPackageJson([currentContent])](#Init._getDefaultPackageJson) ⇒ Promise.<{script: object, author: string, license: string}> @@ -5854,15 +5879,15 @@ wrapper around npm dependency & configuration file setup -### Init.\_getMissingCredentials(properties) ⇒ Array.<string> +### Init.\_getMissingCredentials(properties) ⇒ Promise.<Array.<string>> finds credentials that are set up in config but not in auth file **Kind**: static method of [Init](#Init) -**Returns**: Array.<string> - list of credential names +**Returns**: Promise.<Array.<string>> - list of credential names | Param | Type | Description | | --- | --- | --- | -| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | +| properties | object | javascript object in .mcdevrc.json | @@ -5912,7 +5937,7 @@ CLI helper class * [.initProject(properties, credentialName, [skipInteraction])](#Init.initProject) ⇒ Promise.<void> * [._downloadAllBUs(bu, gitStatus, [skipInteraction])](#Init._downloadAllBUs) ⇒ Promise.<void> * [.upgradeProject(properties, [initial], [repoName])](#Init.upgradeProject) ⇒ Promise.<boolean> - * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Array.<string> + * [._getMissingCredentials(properties)](#Init._getMissingCredentials) ⇒ Promise.<Array.<string>> * [.installDependencies([repoName])](#Init.installDependencies) ⇒ Promise.<boolean> * [._getDefaultPackageJson([currentContent])](#Init._getDefaultPackageJson) ⇒ Promise.<{script: object, author: string, license: string}> @@ -6083,15 +6108,15 @@ wrapper around npm dependency & configuration file setup -### Init.\_getMissingCredentials(properties) ⇒ Array.<string> +### Init.\_getMissingCredentials(properties) ⇒ Promise.<Array.<string>> finds credentials that are set up in config but not in auth file **Kind**: static method of [Init](#Init) -**Returns**: Array.<string> - list of credential names +**Returns**: Promise.<Array.<string>> - list of credential names | Param | Type | Description | | --- | --- | --- | -| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | +| properties | object | javascript object in .mcdevrc.json | @@ -6138,9 +6163,7 @@ Util that contains logger and simple util methods * [.isTrue(attrValue)](#Util.isTrue) ⇒ boolean * [.isFalse(attrValue)](#Util.isFalse) ⇒ boolean * [._isValidType(selectedType)](#Util._isValidType) ⇒ boolean - * [.getDefaultProperties()](#Util.getDefaultProperties) ⇒ TYPE.Mcdevrc - * [.getRetrieveTypeChoices()](#Util.getRetrieveTypeChoices) ⇒ Array.<string> - * [.checkProperties(properties, [silent])](#Util.checkProperties) ⇒ Promise.<(boolean\|Array.<string>)> + * [.getRetrieveTypeChoices()](#Util.getRetrieveTypeChoices) ⇒ Array.<TYPE.SupportedMetadataTypes> * [.metadataLogger(level, type, method, payload, [source])](#Util.metadataLogger) ⇒ void * [.replaceByObject(str, obj)](#Util.replaceByObject) ⇒ string \| object * [.inverseGet(objs, val)](#Util.inverseGet) ⇒ string @@ -6265,36 +6288,15 @@ helper for retrieve, retrieveAsTemplate and deploy | Param | Type | Description | | --- | --- | --- | -| selectedType | string | type or type-subtype | - - - -### Util.getDefaultProperties() ⇒ TYPE.Mcdevrc -defines how the properties.json should look like -used for creating a template and for checking if variables are set +| selectedType | TYPE.SupportedMetadataTypes | type or type-subtype | -**Kind**: static method of [Util](#Util) -**Returns**: TYPE.Mcdevrc - default properties -### Util.getRetrieveTypeChoices() ⇒ Array.<string> +### Util.getRetrieveTypeChoices() ⇒ Array.<TYPE.SupportedMetadataTypes> helper for getDefaultProperties() **Kind**: static method of [Util](#Util) -**Returns**: Array.<string> - type choices - - -### Util.checkProperties(properties, [silent]) ⇒ Promise.<(boolean\|Array.<string>)> -check if the config file is correctly formatted and has values - -**Kind**: static method of [Util](#Util) -**Returns**: Promise.<(boolean\|Array.<string>)> - file structure ok OR list of fields to be fixed - -| Param | Type | Description | -| --- | --- | --- | -| properties | TYPE.Mcdevrc | javascript object in .mcdevrc.json | -| [silent] | boolean | set to true for internal use w/o cli output | - +**Returns**: Array.<TYPE.SupportedMetadataTypes> - type choices ### Util.metadataLogger(level, type, method, payload, [source]) ⇒ void diff --git a/lib/Builder.js b/lib/Builder.js index 4a418604c..1e636ae44 100644 --- a/lib/Builder.js +++ b/lib/Builder.js @@ -4,6 +4,7 @@ const TYPE = require('../types/mcdev.d'); const Util = require('./util/util'); const File = require('./util/file'); const Cli = require('./util/cli'); +const config = require('./util/config'); const auth = require('./util/auth'); const MetadataTypeInfo = require('./MetadataTypeInfo'); // @ts-ignore @@ -102,8 +103,7 @@ saved * @returns {Promise.} - */ static async buildTemplate(businessUnit, selectedType, keyArr, market) { - Util.logger.info('mcdev:: Build Definition from Template'); - const properties = File.loadConfigFile(); + const properties = await config.getProperties(); if (!Util._isValidType(selectedType)) { return; } @@ -170,7 +170,7 @@ saved * @returns {Promise.} - */ static async buildDefinition(businessUnit, selectedType, name, market) { - const properties = File.loadConfigFile(); + const properties = await config.getProperties(); if (!Util._isValidType(selectedType)) { return; } @@ -197,7 +197,7 @@ saved * @returns {Promise.} - */ static async buildDefinitionBulk(listName, type, name) { - const properties = File.loadConfigFile(); + const properties = await config.getProperties(); Util.verifyMarketList(listName, properties); if (type && !MetadataTypeInfo[type]) { Util.logger.error(`:: '${type}' is not a valid metadata type`); diff --git a/lib/Deployer.js b/lib/Deployer.js index 2bb289b9a..8d8d66d85 100644 --- a/lib/Deployer.js +++ b/lib/Deployer.js @@ -8,6 +8,7 @@ const Util = require('./util/util'); const File = require('./util/file'); const cache = require('./util/cache'); const auth = require('./util/auth'); +const config = require('./util/config'); /** * Reads metadata from local directory and deploys it to specified target business unit. @@ -51,7 +52,8 @@ class Deployer { */ static async deploy(businessUnit, selectedTypesArr, keyArr, fromRetrieve) { Util.logger.info('mcdev:: Deploy'); - const properties = File.loadConfigFile(); + + const properties = await config.getProperties(); if (fromRetrieve) { properties.directories.deploy = properties.directories.retrieve; } diff --git a/lib/cli.js b/lib/cli.js index aefc90336..a5695ce9c 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -33,10 +33,10 @@ yargs describe: 'metadata keys that shall be exclusively downloaded', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.retrieve(argv.BU, csvToArray(argv.TYPE), csvToArray(argv.KEY)); + await Mcdev.retrieve(argv.BU, csvToArray(argv.TYPE), csvToArray(argv.KEY)); }, }) .command({ @@ -63,41 +63,41 @@ yargs describe: 'optionally deploy from retrieve folder', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); Mcdev.deploy(argv.BU, csvToArray(argv.TYPE), csvToArray(argv.KEY), argv.fromRetrieve); }, }) .command({ - command: 'init [credentialsName]', + command: 'init [credentialName]', desc: `creates '${Util.configFileName}' in your root or adds additional credentials to the existing one`, builder: (yargs) => { - yargs.positional('credentialsName', { + yargs.positional('credentialName', { type: 'string', describe: 'name of your installed package', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.initProject(argv.credentialsName, argv.skipInteraction); + await Mcdev.initProject(argv.credentialsName, argv.skipInteraction); }, }) .command({ - command: 'reloadBUs [credentialsName]', + command: 'reloadBUs [credentialName]', aliases: ['rb'], desc: 'loads the list of available BUs from the server and saves it to your config', builder: (yargs) => { - yargs.positional('credentialsName', { + yargs.positional('credentialName', { type: 'string', describe: 'name of your installed package', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.findBUs(argv.credentialsName); + await Mcdev.findBUs(argv.credentialsName); }, }) .command({ @@ -109,10 +109,10 @@ yargs describe: 'the business unit to deploy to', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.badKeys(argv.BU); + await Mcdev.badKeys(argv.BU); }, }) .command({ @@ -132,10 +132,10 @@ yargs 'metadata type to generate docs for; currently supported: dataExtension, role', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.document(argv.BU, argv.TYPE); + await Mcdev.document(argv.BU, argv.TYPE); }, }) .command({ @@ -158,10 +158,10 @@ yargs describe: 'the key to delete', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.deleteByKey(argv.BU, argv.TYPE, argv.EXTERNALKEY); + await Mcdev.deleteByKey(argv.BU, argv.TYPE, argv.EXTERNALKEY); }, }) .command({ @@ -188,10 +188,10 @@ yargs describe: 'market used for reverse building template', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.retrieveAsTemplate(argv.BU, argv.TYPE, csvToArray(argv.NAME), argv.MARKET); + await Mcdev.retrieveAsTemplate(argv.BU, argv.TYPE, csvToArray(argv.NAME), argv.MARKET); }, }) .command({ @@ -218,12 +218,12 @@ yargs describe: 'market used for reverse building template', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); const keyArr = csvToArray(argv.KEY); - Mcdev.buildTemplate(argv.BU, argv.TYPE, keyArr, argv.MARKET); + await Mcdev.buildTemplate(argv.BU, argv.TYPE, keyArr, argv.MARKET); }, }) .command({ @@ -249,10 +249,10 @@ yargs describe: 'the business unit to deploy to', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.buildDefinition(argv.BU, argv.TYPE, argv.NAME, argv.MARKET); + await Mcdev.buildDefinition(argv.BU, argv.TYPE, argv.NAME, argv.MARKET); }, }) .command({ @@ -274,30 +274,30 @@ yargs describe: 'name of the metadata component', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.buildDefinitionBulk(argv.LISTNAME, argv.TYPE, argv.NAME); + await Mcdev.buildDefinitionBulk(argv.LISTNAME, argv.TYPE, argv.NAME); }, }) .command({ command: 'selectTypes', aliases: ['st'], desc: 'lets you choose what metadata types to retrieve', - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.selectTypes(); + await Mcdev.selectTypes(); }, }) .command({ command: 'explainTypes', aliases: ['et'], desc: 'explains metadata types that can be retrieved', - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.explainTypes(); + await Mcdev.explainTypes(); }, }) .command({ @@ -316,10 +316,10 @@ yargs 'Disable templating & instead filter by the specified file path (comma separated)', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.createDeltaPkg(argv); + await Mcdev.createDeltaPkg(argv); }, }) .command({ @@ -342,22 +342,22 @@ yargs describe: 'key(s) of the metadata component(s)', }); }, - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); const keyArr = csvToArray(argv.KEY); - Mcdev.getFilesToCommit(argv.BU, argv.TYPE, keyArr); + await Mcdev.getFilesToCommit(argv.BU, argv.TYPE, keyArr); }, }) .command({ command: 'upgrade', aliases: ['up'], desc: 'Add NPM dependencies and IDE configuration files to your project', - handler: (argv) => { + handler: async (argv) => { Mcdev.setSkipInteraction(argv.skipInteraction); Mcdev.setLoggingLevel(argv); - Mcdev.upgrade(argv.skipInteraction); + await Mcdev.upgrade(argv.skipInteraction); }, }) .option('verbose', { @@ -378,6 +378,15 @@ yargs }) .demandCommand(1, 'Please enter a valid command') .strict() + .fail((msg, ex, yargs) => { + if (ex) { + Util.logger.error(ex.message); + } else if (msg) { + Util.logger.info(msg); + console.error(yargs.help()); + process.exit(1); + } + }) .recommendCommands() .wrap(yargs.terminalWidth()) .epilog( diff --git a/lib/index.js b/lib/index.js index 98b63bcc9..612194eff 100644 --- a/lib/index.js +++ b/lib/index.js @@ -16,7 +16,7 @@ const MetadataTypeInfo = require('./MetadataTypeInfo'); const MetadataTypeDefinitions = require('./MetadataTypeDefinitions'); const Retriever = require('./Retriever'); const cache = require('./util/cache'); -let properties; +const config = require('./util/config'); /** * main class @@ -54,11 +54,9 @@ class Mcdev { * @returns {Promise.} list of changed items */ static async createDeltaPkg(argv) { + Util.logger.info('Create Delta Package ::'); Mcdev.setSkipInteraction(argv.skipInteraction); - properties = properties || File.loadConfigFile(); - if (!(await Util.checkProperties(properties))) { - return null; - } + const properties = await config.getProperties(); // get source market and source BU from config if (argv.filter) { return DevOps.getDeltaList(properties, argv.range, true, argv.filter); @@ -72,10 +70,7 @@ class Mcdev { * @returns {Promise} . */ static async selectTypes() { - properties = properties || File.loadConfigFile(); - if (!(await Util.checkProperties(properties))) { - return null; - } + const properties = await config.getProperties(); await Cli.selectTypes(properties); } /** @@ -90,16 +85,15 @@ class Mcdev { */ static async upgrade(skipInteraction) { Mcdev.setSkipInteraction(skipInteraction); - properties = properties || File.loadConfigFile(); - if (!properties) { - Util.logger.error('No config found. Please run mcdev init'); - return false; - } + const properties = await config.getProperties(true); if ((await InitGit.initGitRepo(skipInteraction)).status === 'error') { return false; } - - return Init.upgradeProject(properties, false); + try { + return Init.upgradeProject(properties, false); + } catch (ex) { + Util.logger.error(`Upgrade project failed: ${ex.message}`); + } } /** @@ -113,11 +107,8 @@ class Mcdev { */ static async retrieve(businessUnit, selectedTypesArr, keys, changelogOnly) { Util.logger.info('mcdev:: Retrieve'); - properties = properties || File.loadConfigFile(); - if (!(await Util.checkProperties(properties))) { - // return null here to avoid seeing 2 error messages for the same issue - return null; - } + + const properties = await config.getProperties(); // assume a list was passed in and check each entry's validity if (selectedTypesArr) { @@ -202,7 +193,7 @@ class Mcdev { * @returns {Promise.} ensure that BUs are worked on sequentially */ static async _retrieveBU(cred, bu, selectedTypesArr, keys, changelogOnly) { - properties = properties || File.loadConfigFile(); + const properties = await config.getProperties(); const buObject = await Cli.getCredentialObject( properties, cred !== null ? cred + '/' + bu : null, @@ -282,27 +273,27 @@ class Mcdev { /** * Creates template file for properties.json * - * @param {string} [credentialsName] identifying name of the installed package / project + * @param {string} [credentialName] identifying name of the installed package / project * @param {boolean | TYPE.skipInteraction} [skipInteraction] signals what to insert automatically for things usually asked via wizard * @returns {Promise.} - */ - static async initProject(credentialsName, skipInteraction) { + static async initProject(credentialName, skipInteraction) { Util.logger.info('mcdev:: Setting up project'); Mcdev.setSkipInteraction(skipInteraction); - properties = properties || File.loadConfigFile(!!credentialsName); - await Init.initProject(properties, credentialsName, skipInteraction); + const properties = await config.getProperties(!!credentialName); + await Init.initProject(properties, credentialName, skipInteraction); } /** * Refreshes BU names and ID's from MC instance * - * @param {string} credentialsName identifying name of the installed package / project + * @param {string} credentialName identifying name of the installed package / project * @returns {Promise.} - */ - static async findBUs(credentialsName) { + static async findBUs(credentialName) { Util.logger.info('mcdev:: Load BUs'); - properties = properties || File.loadConfigFile(); - const buObject = await Cli.getCredentialObject(properties, credentialsName, true); + const properties = await config.getProperties(); + const buObject = await Cli.getCredentialObject(properties, credentialName, true); if (buObject !== null) { BuHelper.refreshBUProperties(properties, buObject.credential); } @@ -317,7 +308,8 @@ class Mcdev { */ static async document(businessUnit, type) { Util.logger.info('mcdev:: Document'); - properties = properties || File.loadConfigFile(); + + const properties = await config.getProperties(); if (type && !MetadataTypeInfo[type]) { Util.logger.error(`:: '${type}' is not a valid metadata type`); return; @@ -352,7 +344,7 @@ class Mcdev { */ static async deleteByKey(businessUnit, type, customerKey) { Util.logger.info('mcdev:: delete'); - properties = properties || File.loadConfigFile(); + const properties = await config.getProperties(); const buObject = await Cli.getCredentialObject(properties, businessUnit); if (buObject !== null) { if ('string' !== typeof type) { @@ -381,7 +373,7 @@ class Mcdev { * @returns {Promise.} - */ static async badKeys(businessUnit) { - properties = properties || File.loadConfigFile(); + const properties = await config.getProperties(); const buObject = await Cli.getCredentialObject(properties, businessUnit); if (buObject !== null) { Util.logger.info('Gathering list of Name<>External Key mismatches (bad keys)'); @@ -452,7 +444,8 @@ class Mcdev { */ static async retrieveAsTemplate(businessUnit, selectedType, name, market) { Util.logger.info('mcdev:: Retrieve as Template'); - properties = properties || File.loadConfigFile(); + + const properties = await config.getProperties(); if (!Util._isValidType(selectedType)) { return; } @@ -489,6 +482,7 @@ class Mcdev { * @returns {Promise.} - */ static async buildTemplate(businessUnit, selectedType, keyArr, market) { + Util.logger.info('mcdev:: Build Definition from Template'); return Builder.buildTemplate(businessUnit, selectedType, keyArr, market); } /** @@ -526,7 +520,7 @@ class Mcdev { */ static async getFilesToCommit(businessUnit, selectedType, keyArr) { Util.logger.info('mcdev:: getFilesToCommit'); - const properties = File.loadConfigFile(); + const properties = await config.getProperties(); if (!Util._isValidType(selectedType)) { return; } diff --git a/lib/util/auth.js b/lib/util/auth.js index 46e61dd8d..7185fd5b5 100644 --- a/lib/util/auth.js +++ b/lib/util/auth.js @@ -8,7 +8,40 @@ const credentialStore = new Conf({ configName: 'sessions', clearInvalidConfig: t const initializedSDKs = {}; let authfile; -module.exports = { +/** + * Returns an SDK instance to be used for API calls + * + * @param {string} sessionKey key for specific BU + * @param {TYPE.AuthObject} authObject credentials for specific BU + * @returns {SDK} auth object + */ +function setupSDK(sessionKey, authObject) { + return new SDK(authObject, { + eventHandlers: { + onLoop: (type, req) => { + Util.logger.info( + `- Requesting next batch (currently ${req.Results.length} records)` + ); + }, + onRefresh: (authObject) => { + authObject.scope = authObject.scope.split(' '); // Scope is usually not an array, but we enforce conversion here for simplicity + credentialStore.set(sessionKey, authObject); + }, + onConnectionError: (ex, remainingAttempts) => { + Util.logger.warn( + `- Connection problem (Code: ${ex.code}). Retrying ${remainingAttempts} time${ + remainingAttempts > 1 ? 's' : '' + }` + ); + Util.logger.errorStack(ex); + }, + }, + requestAttempts: 4, + retryOnConnectionError: true, + }); +} + +const Auth = { /** * For each business unit, set up base credentials to be used. * @@ -81,35 +114,4 @@ module.exports = { Util.logger.info(`Auth sessions cleared`); }, }; -/** - * Returns an SDK instance to be used for API calls - * - * @param {string} sessionKey key for specific BU - * @param {TYPE.AuthObject} authObject credentials for specific BU - * @returns {SDK} auth object - */ - -const setupSDK = (sessionKey, authObject) => - new SDK(authObject, { - eventHandlers: { - onLoop: (type, req) => { - Util.logger.info( - `- Requesting next batch (currently ${req.Results.length} records)` - ); - }, - onRefresh: (authObject) => { - authObject.scope = authObject.scope.split(' '); // Scope is usually not an array, but we enforce conversion here for simplicity - credentialStore.set(sessionKey, authObject); - }, - onConnectionError: (ex, remainingAttempts) => { - Util.logger.warn( - `- Connection problem (Code: ${ex.code}). Retrying ${remainingAttempts} time${ - remainingAttempts > 1 ? 's' : '' - }` - ); - Util.logger.errorStack(ex); - }, - }, - requestAttempts: 4, - retryOnConnectionError: true, - }); +module.exports = Auth; diff --git a/lib/util/businessUnit.js b/lib/util/businessUnit.js index b6e1300a9..690d51044 100644 --- a/lib/util/businessUnit.js +++ b/lib/util/businessUnit.js @@ -1,5 +1,4 @@ 'use strict'; - const TYPE = require('../../types/mcdev.d'); const Util = require('./util'); const File = require('./file'); @@ -13,17 +12,17 @@ const BusinessUnit = { * Refreshes BU names and ID's from MC instance * * @param {TYPE.Mcdevrc} properties current properties that have to be refreshed - * @param {string} credentialsName identifying name of the installed package / project + * @param {string} credentialName identifying name of the installed package / project * @returns {Promise.} success of refresh */ - refreshBUProperties: async function (properties, credentialsName) { - const currentCredentials = properties.credentials[credentialsName]; + refreshBUProperties: async function (properties, credentialName) { + const currentCredentials = properties.credentials[credentialName]; Util.logger.info(`Loading BUs`); try { const client = auth.getSDK({ mid: currentCredentials.eid, eid: currentCredentials.eid, - credential: credentialsName, + credential: credentialName, businessUnit: '_ParentBU_', }); const buResult = await client.soap.retrieve( @@ -93,7 +92,7 @@ const BusinessUnit = { // store BU list for repo await File.writeJSONToFile( properties.directories.businessUnits, - File.filterIllegalFilenames(credentialsName + '.businessUnits'), + File.filterIllegalFilenames(credentialName + '.businessUnits'), buResult.Results ); // update config diff --git a/lib/util/cli.js b/lib/util/cli.js index 466b75da4..e63a16fa9 100644 --- a/lib/util/cli.js +++ b/lib/util/cli.js @@ -7,6 +7,7 @@ const inquirer = require('inquirer'); const MetadataDefinitions = require('./../MetadataTypeDefinitions'); const Util = require('./util'); const auth = require('./auth'); +const config = require('./config'); /** * CLI helper class @@ -23,7 +24,7 @@ const Cli = { async initMcdevConfig(skipInteraction) { Util.logger.info('-- Initialising server connection --'); Util.logger.info('Please enter a name for your "Installed Package" credentials:'); - const propertiesTemplate = Util.getDefaultProperties(); + const propertiesTemplate = config.getDefaultProperties(); delete propertiesTemplate.credentials.default; // wait for the interaction to finish or else an outer await will run before this is done @@ -37,24 +38,22 @@ const Cli = { * @returns {Promise.} - */ async addExtraCredential(properties, skipInteraction) { - if (!(await Util.checkProperties(properties))) { - // return null here to avoid seeing 2 error messages for the same issue - return null; - } else { - Util.logger.info('Found the following credentials in your config file:'); - for (const cred in properties.credentials) { - if (Object.prototype.hasOwnProperty.call(properties.credentials, cred)) { - Util.logger.info(` - ${cred}`); - } - } - Util.logger.info('\nPlease enter your new credentials'); - if (skipInteraction && properties.credentials[skipInteraction.credentialName]) { - Util.logger.error( - `Credential '${skipInteraction.credentialName}' already existing. If you tried updating please provide run 'mcdev init ${skipInteraction.credentialName}'` - ); + // check existing properties before adding additional // todo CONFIRM if needed + await config.checkProperties(properties); + + Util.logger.info('Found the following credentials in your config file:'); + for (const cred in properties.credentials) { + if (Object.prototype.hasOwnProperty.call(properties.credentials, cred)) { + Util.logger.info(` - ${cred}`); } - return this._setCredential(properties, null, skipInteraction); } + Util.logger.info('\nPlease enter your new credentials'); + if (skipInteraction && properties.credentials[skipInteraction.credentialName]) { + Util.logger.error( + `Credential '${skipInteraction.credentialName}' already existing. If you tried updating please provide run 'mcdev init ${skipInteraction.credentialName}'` + ); + } + return this._setCredential(properties, null, skipInteraction); }, /** * Extends template file for properties.json @@ -84,10 +83,6 @@ const Cli = { */ async getCredentialObject(properties, target, isCredentialOnly, allowAll) { try { - if (!(await Util.checkProperties(properties))) { - // return null here to avoid seeing 2 error messages for the same issue - return null; - } let [credential, businessUnit] = target ? target.split('/') : [null, null]; if ( credential && diff --git a/lib/util/config.js b/lib/util/config.js new file mode 100644 index 000000000..2a70fd079 --- /dev/null +++ b/lib/util/config.js @@ -0,0 +1,287 @@ +const TYPE = require('../../types/mcdev.d'); +const Util = require('./util'); +const File = require('./file'); +const inquirer = require('inquirer'); +const semver = require('semver'); +const path = require('path'); +const packageJsonMcdev = require('../../package.json'); +/** + * Central class for loading and validating properties from config and auth + */ +const config = { + properties: null, + /** + * loads central properties from config file + * + * @param {boolean} [skipChecks] omit throwing errors and print messages + * @returns {object} central properties object + */ + async getProperties(skipChecks = false) { + // already loaded, return existing + if (config.properties) { + return config.properties; + } + if (await File.pathExists(Util.configFileName)) { + config.properties = await File.readJson(Util.configFileName); + + if (await File.pathExists(Util.authFileName)) { + const auth = await File.readJson(Util.authFileName); + for (const cred in config.properties.credentials) { + if (auth[cred]) { + if ( + config.properties.credentials[cred].eid !== auth[cred].account_id && + !skipChecks + ) { + // ! this is the only we want to run even if skipChecks is true + throw new Error( + `'${cred}' found in ${Util.configFileName} and ${Util.authFileName} have a Enterprise ID mismatch. Please check.` + ); + } + // TODO add auth checks #294 + } else if (!skipChecks) { + throw new Error( + `'${cred}' found in ${Util.configFileName} but not in ${Util.authFileName}. Please run 'mcdev init' to provide the missing credential details.` + ); + } + } + } else if (!skipChecks) { + throw new Error( + `${Util.authFileName} not found. Please run 'mcdev init' to provide the missing credential details.` + ); + } + } + if (!skipChecks) { + await this.checkProperties(config.properties); + } + return config.properties; + }, + /** + * check if the config file is correctly formatted and has values + * + * @param {TYPE.Mcdevrc} properties javascript object in .mcdevrc.json + * @param {boolean} [skipCredentialValidation] set to true for internal use w/o cli output + * @returns {Promise.} file structure ok OR list of fields to be fixed + */ + async checkProperties(properties, skipCredentialValidation) { + if (!(await File.pathExists(Util.configFileName)) || !properties) { + throw new Error( + `Could not find ${ + Util.configFileName + } in ${process.cwd()}.Run 'mcdev init' to initialize your project.\n` + ); + } + if (!(await File.pathExists(Util.authFileName)) || !properties) { + throw new Error( + `Could not find ${ + Util.authFileName + } in ${process.cwd()}.Run 'mcdev init' to initialize your project.\n` + ); + } + + // check if user is running older (ignores patches) mcdev version than whats saved to the config + if (properties.version && semver.gt(properties.version, packageJsonMcdev.version)) { + const errorMsg = `Your Accenture SFMC DevTools version ${packageJsonMcdev.version} is lower than your project's config version ${properties.version}`; + if (Util.skipInteraction) { + throw new Error(errorMsg); + } + const responses = await inquirer.prompt([ + { + type: 'confirm', + name: 'runUpgradeNow', + message: `Do you want to run 'npm update -g mcdev@${properties.version}' now? This may take a few minutes.`, + default: true, + }, + ]); + if (responses.runUpgradeNow) { + // use _execSync here to avoid a circular dependency + Util.execSync('npm', ['update', '-g', `mcdev@${properties.version}`]); + } + throw new Error(errorMsg); + } + + // check config properties + const { errorMsgs, solutionSet } = await this.getProblems( + properties, + null, + skipCredentialValidation + ); + if (errorMsgs.length) { + const errorMsgOutput = [ + `Found problems in your ./${Util.configFileName} that you need to fix first:`, + ]; + for (const msg of errorMsgs) { + errorMsgOutput.push(' - ' + msg); + } + const errorMsgText = errorMsgOutput.join('\n'); + if (Util.skipInteraction) { + throw new Error(errorMsgText); + } + Util.logger.error(errorMsgText); + Util.logger.info( + ['Here is what you can do to fix these issues:', ...Array.from(solutionSet)].join( + '\n- ' + ) + ); + const responses = await inquirer.prompt([ + { + type: 'confirm', + name: 'runUpgradeNow', + message: `Do you want to run 'mcdev upgrade' now?`, + default: true, + }, + ]); + if (responses.runUpgradeNow) { + // use _execSync here to avoid a circular dependency + Util.execSync('mcdev', ['upgrade']); + } + throw new Error(errorMsgOutput); + } + }, + /** + * defines how the properties.json should look like + * used for creating a template and for checking if variables are set + * + * @returns {TYPE.Mcdevrc} default properties + */ + async getDefaultProperties() { + const configFileName = path.resolve(__dirname, Util.boilerplateDirectory, 'config.json'); + if (!(await File.pathExists(configFileName))) { + Util.logger.debug(`Default config file not found in ${configFileName}`); + return false; + } + const defaultProperties = await File.readJson(configFileName); + // set default name for parent BU + defaultProperties.credentials.default.businessUnits[Util.parentBuName] = 0; + // set default retrieve values + defaultProperties.metaDataTypes.retrieve = Util.getRetrieveTypeChoices(); + + return defaultProperties; + }, + /** + * + * @param {TYPE.Mcdevrc} properties javascript object in .mcdevrc.json + * @param {TYPE.Mcdevrc} [defaultProps] default properties + * @param {boolean} [skipCredentialValidation] used by init.config.js>fixMcdevConfig() to auto-fix the config file + * @returns {Promise.<{missingFields: string[], errorMsgs: string[], solutionSet: Set}>} - + */ + async getProblems(properties, defaultProps, skipCredentialValidation) { + defaultProps = defaultProps || (await this.getDefaultProperties()); + const errorMsgs = []; + const solutionSet = new Set(); + const missingFields = []; + for (const key in defaultProps) { + if (Object.prototype.hasOwnProperty.call(defaultProps, key)) { + if (!Object.prototype.hasOwnProperty.call(properties, key)) { + errorMsgs.push(`${key}{} missing`); + solutionSet.add( + `Run 'mcdev upgrade' to fix missing or changed configuration options` + ); + missingFields.push(key); + } else { + if (!skipCredentialValidation && key === 'credentials') { + if (!Object.keys(properties.credentials)) { + errorMsgs.push(`no Credential defined`); + } else { + for (const cred in properties.credentials) { + if (cred.includes('/') || cred.includes('\\')) { + errorMsgs.push( + `Credential names may not includes slashes: ${cred}` + ); + solutionSet.add('Apply manual fix in your config.'); + } + if ( + !properties.credentials[cred].eid || + properties.credentials[cred].eid === 0 + ) { + errorMsgs.push(`invalid account_id (EID) on ${cred}`); + solutionSet.add(`Run 'mcdev init ${cred}'`); + } + let i = 0; + for (const buName in properties.credentials[cred].businessUnits) { + if (buName.includes('/') || buName.includes('\\')) { + errorMsgs.push( + `Business Unit names may not includes slashes: ${cred}: ${buName}` + ); + solutionSet.add(`Run 'mcdev reloadBUs ${cred}'`); + } + if ( + Object.prototype.hasOwnProperty.call( + properties.credentials[cred].businessUnits, + buName + ) && + properties.credentials[cred].businessUnits[buName] !== 0 + ) { + i++; + } + } + if (!i) { + errorMsgs.push(`no Business Units defined`); + solutionSet.add(`Run 'mcdev reloadBUs ${cred}'`); + } + } + } + } else if (['directories', 'metaDataTypes', 'options'].includes(key)) { + for (const subkey in defaultProps[key]) { + if ( + Object.prototype.hasOwnProperty.call(defaultProps[key], subkey) && + !Object.prototype.hasOwnProperty.call(properties[key], subkey) + ) { + errorMsgs.push( + `${key}.${subkey} missing. Default value (${ + Array.isArray(defaultProps[key][subkey]) + ? 'Array' + : typeof defaultProps[key][subkey] + }): ${defaultProps[key][subkey]}` + ); + solutionSet.add( + `Run 'mcdev upgrade' to fix missing or changed configuration options` + ); + missingFields.push(`${key}.${subkey}`); + } else if (subkey === 'deployment') { + for (const dkey in defaultProps[key][subkey]) { + if ( + Object.prototype.hasOwnProperty.call( + defaultProps[key][subkey], + dkey + ) && + !Object.prototype.hasOwnProperty.call( + properties[key][subkey], + dkey + ) + ) { + errorMsgs.push( + `${key}.${subkey} missing. Default value (${ + Array.isArray(defaultProps[key][subkey][dkey]) + ? 'Array' + : typeof defaultProps[key][subkey][dkey] + }): ${defaultProps[key][subkey][dkey]}` + ); + solutionSet.add( + `Run 'mcdev upgrade' to fix missing or changed configuration options` + ); + missingFields.push(`${key}.${subkey}.${dkey}`); + } + } + } + } + } + } + } + } + // check if project config version is outdated compared to user's mcdev version + if ( + !properties.version || + (![null, 'patch'].includes(semver.diff(packageJsonMcdev.version, properties.version)) && + semver.gt(packageJsonMcdev.version, properties.version)) + ) { + errorMsgs.push( + `Your project's config version ${properties.version} is lower than your Accenture SFMC DevTools version ${packageJsonMcdev.version}` + ); + solutionSet.add(`Run 'mcdev upgrade' to ensure optimal performance`); + missingFields.push('version'); + } + return { missingFields, errorMsgs, solutionSet }; + }, +}; + +module.exports = config; diff --git a/lib/util/devops.js b/lib/util/devops.js index 8a8a9ab3b..80da447f6 100644 --- a/lib/util/devops.js +++ b/lib/util/devops.js @@ -24,7 +24,6 @@ const DevOps = { */ async getDeltaList(properties, range, saveToDeployDir, filterPaths) { const rangeUserInput = range; - Util.logger.info('Create Delta Package ::'); if (filterPaths) { filterPaths = filterPaths.split(',').map((filePath) => path diff --git a/lib/util/file.js b/lib/util/file.js index b6b361e75..52f45d972 100644 --- a/lib/util/file.js +++ b/lib/util/file.js @@ -145,7 +145,11 @@ const File = { filename = this.filterIllegalFilenames(filename); await fs.ensureDir(directory); try { - return fs.writeJSON(path.join(directory, filename + '.json'), content, { spaces: 4 }); + return fs.writeJSON( + path.join(directory, filename.endsWith('.json') ? filename : filename + '.json'), + content, + { spaces: 4 } + ); } catch (ex) { Util.logger.error('File.writeJSONToFile:: error | ' + ex.message); } @@ -440,63 +444,6 @@ const File = { Util.logger.debug(ex.stack); } }, - /** - * loads central properties from config file - * - * @param {boolean} [silent] omit throwing errors and print messages; assuming not silent if not set - * @returns {TYPE.Mcdevrc} central properties object - */ - loadConfigFile(silent) { - let properties; - if (fs.pathExistsSync(Util.configFileName)) { - try { - properties = fs.readJsonSync(Util.configFileName); - } catch (ex) { - Util.logger.error(`${ex.code}: ${ex.message}`); - return; - } - if (fs.pathExistsSync(Util.authFileName)) { - let auth; - try { - auth = fs.readJsonSync(Util.authFileName); - } catch (ex) { - Util.logger.error(`${ex.code}: ${ex.message}`); - return; - } - - if (!auth) { - const err = `${Util.authFileName} is not set up correctly.`; - Util.logger.error(err); - throw new Error(err); - } - for (const cred in properties.credentials) { - if (auth[cred]) { - if (properties.credentials[cred].eid != auth[cred].account_id && !silent) { - const err = `'${cred}' found in ${ - Util.configFileName - } (${typeof properties.credentials[cred].eid} ${ - properties.credentials[cred].eid - }) and ${Util.authFileName} (${typeof auth[cred].account_id} ${ - auth[cred].account_id - }) have a Enterprise ID mismatch. Please check.`; - Util.logger.error(err); - throw new Error(err); - } - // TODO add auth checks #294 - } else if (!silent) { - Util.logger.warn( - `'${cred}' found in ${Util.configFileName} but not in ${Util.authFileName}. Please run 'mcdev init' to provide the missing credential details.` - ); - } - } - } else if (!silent) { - Util.logger.warn( - `${Util.authFileName} not found. Please run 'mcdev init' to provide the missing credential details.` - ); - } - } - return properties; - }, /** * helper that splits the config back into auth & config parts to save them separately * diff --git a/lib/util/init.config.js b/lib/util/init.config.js index 7981e9294..b24b90665 100644 --- a/lib/util/init.config.js +++ b/lib/util/init.config.js @@ -7,6 +7,7 @@ const Util = require('./util'); const inquirer = require('inquirer'); const path = require('path'); const semver = require('semver'); +const config = require('./config'); /** * CLI helper class @@ -28,97 +29,96 @@ const Init = { let updateConfigNeeded = false; const upgradeMsgs = [`Upgrading existing ${Util.configFileName}:`]; + const defaultProps = await config.getDefaultProperties(); + const { missingFields } = await config.getProblems(properties, defaultProps, true); + if (missingFields?.length) { + updateConfigNeeded = true; + } + missingFields.forEach((fieldName) => { + switch (fieldName) { + case 'marketList': + if (properties.marketBulk) { + upgradeMsgs.push(`- ✔️ converted 'marketBulk' to '${fieldName}'`); + properties[fieldName] = properties.marketBulk; + delete properties.marketBulk; + } else { + upgradeMsgs.push(`- ✔️ added '${fieldName}'`); + this._updateLeaf(properties, defaultProps, fieldName); + } + break; + case 'directories.docs': + if (properties.directories.badKeys) { + delete properties.directories.badKeys; + upgradeMsgs.push(`- ✋ removed 'directories.badKeys'`); + } + if (properties.directories.dataExtension) { + File.removeSync(properties.directories.dataExtension); + delete properties.directories.dataExtension; + upgradeMsgs.push(`- ✋ removed 'directories.dataExtension'`); + } + if (properties.directories.deltaPackage) { + delete properties.directories.deltaPackage; + upgradeMsgs.push(`- ✋ removed 'directories.deltaPackage'`); + } + if (properties.directories.dataextension) { + delete properties.directories.dataextension; + upgradeMsgs.push(`- ✋ removed 'directories.dataextension'`); + } + if (properties.directories.roles) { + delete properties.directories.roles; + upgradeMsgs.push(`- ✋ removed 'directories.roles'`); + } + if (properties.directories.users) { + delete properties.directories.users; + upgradeMsgs.push(`- ✋ removed 'directories.users'`); + } - const missingFields = await Util.checkProperties(properties, true); - const defaultProps = Util.getDefaultProperties(); - if (missingFields.length) { - missingFields.forEach((fieldName) => { - switch (fieldName) { - case 'marketList': - if (properties.marketBulk) { - upgradeMsgs.push(`- ✔️ converted 'marketBulk' to '${fieldName}'`); - properties[fieldName] = properties.marketBulk; - delete properties.marketBulk; - } else { - upgradeMsgs.push(`- ✔️ added '${fieldName}'`); - this._updateLeaf(properties, defaultProps, fieldName); - } - break; - case 'directories.docs': - if (properties.directories.badKeys) { - delete properties.directories.badKeys; - upgradeMsgs.push(`- ✋ removed 'directories.badKeys'`); - } - if (properties.directories.dataExtension) { - File.removeSync(properties.directories.dataExtension); - delete properties.directories.dataExtension; - upgradeMsgs.push(`- ✋ removed 'directories.dataExtension'`); - } - if (properties.directories.deltaPackage) { - delete properties.directories.deltaPackage; - upgradeMsgs.push(`- ✋ removed 'directories.deltaPackage'`); - } - if (properties.directories.dataextension) { - delete properties.directories.dataextension; - upgradeMsgs.push(`- ✋ removed 'directories.dataextension'`); - } - if (properties.directories.roles) { - delete properties.directories.roles; - upgradeMsgs.push(`- ✋ removed 'directories.roles'`); - } - if (properties.directories.users) { - delete properties.directories.users; - upgradeMsgs.push(`- ✋ removed 'directories.users'`); - } - + this._updateLeaf(properties, defaultProps, fieldName); + upgradeMsgs.push(`- ✔️ added '${fieldName}'`); + break; + case 'metaDataTypes.documentOnRetrieve': + if (!properties.options.documentOnRetrieve) { + properties.metaDataTypes.documentOnRetrieve = []; + } else { this._updateLeaf(properties, defaultProps, fieldName); - upgradeMsgs.push(`- ✔️ added '${fieldName}'`); - break; - case 'metaDataTypes.documentOnRetrieve': - if (!properties.options.documentOnRetrieve) { - properties.metaDataTypes.documentOnRetrieve = []; - } else { - this._updateLeaf(properties, defaultProps, fieldName); - } - delete properties.options.documentOnRetrieve; + } + delete properties.options.documentOnRetrieve; + upgradeMsgs.push( + `- ✔️ converted 'options.documentOnRetrieve' to '${fieldName}'` + ); + break; + case 'options.deployment.commitHistory': + if (properties.options.commitHistory) { upgradeMsgs.push( - `- ✔️ converted 'options.documentOnRetrieve' to '${fieldName}'` + `- ✔️ converted 'options.commitHistory' to '${fieldName}'` ); - break; - case 'options.deployment.commitHistory': - if (properties.options.commitHistory) { - upgradeMsgs.push( - `- ✔️ converted 'options.commitHistory' to '${fieldName}'` - ); - properties.options.deployment.commitHistory = - properties.options.commitHistory; - delete properties.options.commitHistory; - } else { - upgradeMsgs.push(`- ✔️ added '${fieldName}'`); - this._updateLeaf(properties, defaultProps, fieldName); - } - break; - case 'options.exclude': - if (properties.options.filter) { - upgradeMsgs.push(`- ✔️ converted 'options.filter' to '${fieldName}'`); - properties.options.exclude = properties.options.filter; - delete properties.options.filter; - } else { - upgradeMsgs.push(`- ✔️ added '${fieldName}'`); - this._updateLeaf(properties, defaultProps, fieldName); - } - break; - case 'version': - // do nothing other than ensure we re-save the config (with the new version) - upgradeMsgs.push(`- ✔️ version updated`); - break; - default: + properties.options.deployment.commitHistory = + properties.options.commitHistory; + delete properties.options.commitHistory; + } else { + upgradeMsgs.push(`- ✔️ added '${fieldName}'`); this._updateLeaf(properties, defaultProps, fieldName); + } + break; + case 'options.exclude': + if (properties.options.filter) { + upgradeMsgs.push(`- ✔️ converted 'options.filter' to '${fieldName}'`); + properties.options.exclude = properties.options.filter; + delete properties.options.filter; + } else { upgradeMsgs.push(`- ✔️ added '${fieldName}'`); - } - }); - updateConfigNeeded = true; - } + this._updateLeaf(properties, defaultProps, fieldName); + } + break; + case 'version': + // do nothing other than ensure we re-save the config (with the new version) + upgradeMsgs.push(`- ✔️ version updated`); + break; + default: + this._updateLeaf(properties, defaultProps, fieldName); + upgradeMsgs.push(`- ✔️ added '${fieldName}'`); + } + }); // ensure we document dataExtensions and automations on retrieve as they should now be in the retrieve folder this._updateLeaf(properties, defaultProps, 'metaDataTypes.documentOnRetrieve'); diff --git a/lib/util/init.js b/lib/util/init.js index d6e90461e..679f10fd2 100644 --- a/lib/util/init.js +++ b/lib/util/init.js @@ -7,6 +7,7 @@ const InitNpm = require('./init.npm'); const InitConfig = require('./init.config'); const inquirer = require('inquirer'); const Util = require('./util'); +const config = require('./config'); /** * CLI helper class @@ -22,12 +23,13 @@ const Init = { * @returns {Promise.} - */ async initProject(properties, credentialName, skipInteraction) { - const missingCredentials = this._getMissingCredentials(properties); + const missingCredentials = await this._getMissingCredentials(properties); if ((await File.pathExists(Util.configFileName)) && properties) { // config exists if (credentialName) { Util.logger.info(`Updating credential '${credentialName}'`); // update-credential mode + console.log('CHECK', properties); if (!properties.credentials[credentialName]) { Util.logger.error(`Could not find credential '${credentialName}'`); const response = await Cli._selectBU(properties, null, true); @@ -52,7 +54,7 @@ const Init = { } } while (error && !skipInteraction); Util.logger.debug('reloading config'); - properties = File.loadConfigFile(true); + properties = await config.getProperties(true); } else if (missingCredentials.length) { // forced update-credential mode - user likely cloned repo and is missing mcdev-auth.json Util.logger.warn( @@ -82,7 +84,7 @@ const Init = { } } while (error); Util.logger.debug('reloading config'); - properties = File.loadConfigFile(true); + properties = await config.getProperties(true); } Util.logger.info('✔️ All credentials updated.'); // assume node dependencies are not installed @@ -258,18 +260,22 @@ const Init = { /** * finds credentials that are set up in config but not in auth file * - * @param {TYPE.Mcdevrc} properties javascript object in .mcdevrc.json - * @returns {string[]} list of credential names + * @param {object} properties javascript object in .mcdevrc.json + * @returns {Promise.} list of credential names */ - _getMissingCredentials(properties) { + async _getMissingCredentials(properties) { let missingCredentials; + const authCredentials = await File.readJson(Util.authFileName); + console.log('GET', authCredentials); if (properties && properties.credentials) { missingCredentials = Object.keys(properties.credentials).filter( (cred) => - !properties.credentials[cred].client_id || - !properties.credentials[cred].client_secret || - !properties.credentials[cred].auth_url || - !properties.credentials[cred].account_id + !authCredentials || + !authCredentials[cred] || + !authCredentials[cred].client_id || + !authCredentials[cred].client_secret || + !authCredentials[cred].auth_url || + !authCredentials[cred].account_id ); } return missingCredentials || []; diff --git a/lib/util/util.js b/lib/util/util.js index 490bf345a..ec7621ad4 100644 --- a/lib/util/util.js +++ b/lib/util/util.js @@ -1,16 +1,12 @@ 'use strict'; const TYPE = require('../../types/mcdev.d'); -const fs = require('fs-extra'); // ! do not switch to util/file.js to avoid circular dependency const MetadataDefinitions = require('./../MetadataTypeDefinitions'); -const packageJsonMcdev = require('../../package.json'); -const path = require('path'); const process = require('process'); const toposort = require('toposort'); const winston = require('winston'); -const inquirer = require('inquirer'); const child_process = require('child_process'); -const semver = require('semver'); +const packageJsonMcdev = require('../../package.json'); /** * Util that contains logger and simple util methods @@ -156,7 +152,7 @@ const Util = { /** * helper for retrieve, retrieveAsTemplate and deploy * - * @param {string} selectedType type or type-subtype + * @param {TYPE.SupportedMetadataTypes} selectedType type or type-subtype * @returns {boolean} type ok or not */ _isValidType(selectedType) { @@ -175,30 +171,10 @@ const Util = { return true; }, - /** - * defines how the properties.json should look like - * used for creating a template and for checking if variables are set - * - * @returns {TYPE.Mcdevrc} default properties - */ - getDefaultProperties() { - const configFileName = path.resolve(__dirname, this.boilerplateDirectory, 'config.json'); - if (!fs.pathExistsSync(configFileName)) { - this.logger.debug(`Default config file not found in ${configFileName}`); - return false; - } - const defaultProperties = fs.readJsonSync(configFileName); - // set default name for parent BU - defaultProperties.credentials.default.businessUnits[this.parentBuName] = 0; - // set default retrieve values - defaultProperties.metaDataTypes.retrieve = this.getRetrieveTypeChoices(); - - return defaultProperties; - }, /** * helper for getDefaultProperties() * - * @returns {string[]} type choices + * @returns {TYPE.SupportedMetadataTypes[]} type choices */ getRetrieveTypeChoices() { const typeChoices = []; @@ -226,203 +202,6 @@ const Util = { return typeChoices; }, - /** - * check if the config file is correctly formatted and has values - * - * @param {TYPE.Mcdevrc} properties javascript object in .mcdevrc.json - * @param {boolean} [silent] set to true for internal use w/o cli output - * @returns {Promise.} file structure ok OR list of fields to be fixed - */ - checkProperties: async function (properties, silent) { - if (!(await fs.pathExists(Util.configFileName)) || !properties) { - Util.logger.error(`\nCould not find ${Util.configFileName} in ${process.cwd()}.`); - Util.logger.error(`Run 'mcdev init' to initialize your project.\n`); - return false; - } - if (!(await fs.pathExists(Util.authFileName)) || !properties) { - Util.logger.error(`\nCould not find ${Util.authFileName} in ${process.cwd()}.`); - Util.logger.error(`Run 'mcdev init' to initialize your project.\n`); - return false; - } - - // check if user is running older (ignores patches) mcdev version than whats saved to the config - if (properties.version && semver.gt(properties.version, packageJsonMcdev.version)) { - Util.logger.error( - `Your Accenture SFMC DevTools version ${packageJsonMcdev.version} is lower than your project's config version ${properties.version}` - ); - if (Util.skipInteraction) { - return false; - } - - const responses = await inquirer.prompt([ - { - type: 'confirm', - name: 'runUpgradeNow', - message: `Do you want to run 'npm update -g mcdev@${properties.version}' now? This may take a few minutes.`, - default: true, - }, - ]); - if (responses.runUpgradeNow) { - // use _execSync here to avoid a circular dependency - this.execSync('npm', ['update', '-g', `mcdev@${properties.version}`]); - } - return false; - } - - // check config properties - const defaultProps = this.getDefaultProperties(); - const errorMsgs = []; - const solutionSet = new Set(); - const missingFields = []; - for (const key in defaultProps) { - if (Object.prototype.hasOwnProperty.call(defaultProps, key)) { - if (!Object.prototype.hasOwnProperty.call(properties, key)) { - errorMsgs.push(`${key}{} missing`); - solutionSet.add( - `Run 'mcdev upgrade' to fix missing or changed configuration options` - ); - missingFields.push(key); - } else { - if (!silent && key === 'credentials') { - if (!Object.keys(properties.credentials)) { - errorMsgs.push(`no Credential defined`); - } else { - for (const cred in properties.credentials) { - if (cred.includes('/') || cred.includes('\\')) { - errorMsgs.push( - `Credential names may not includes slashes: ${cred}` - ); - solutionSet.add('Apply manual fix in your config.'); - } - if ( - !properties.credentials[cred].eid || - properties.credentials[cred].eid === 0 - ) { - errorMsgs.push(`invalid account_id (EID) on ${cred}`); - solutionSet.add(`Run 'mcdev init ${cred}'`); - } - let i = 0; - for (const buName in properties.credentials[cred].businessUnits) { - if (buName.includes('/') || buName.includes('\\')) { - errorMsgs.push( - `Business Unit names may not includes slashes: ${cred}: ${buName}` - ); - solutionSet.add(`Run 'mcdev reloadBUs ${cred}'`); - } - if ( - Object.prototype.hasOwnProperty.call( - properties.credentials[cred].businessUnits, - buName - ) && - properties.credentials[cred].businessUnits[buName] !== 0 - ) { - i++; - } - } - if (!i) { - errorMsgs.push(`no Business Units defined`); - solutionSet.add(`Run 'mcdev reloadBUs ${cred}'`); - } - } - } - } else if (['directories', 'metaDataTypes', 'options'].includes(key)) { - for (const subkey in defaultProps[key]) { - if ( - Object.prototype.hasOwnProperty.call(defaultProps[key], subkey) && - !Object.prototype.hasOwnProperty.call(properties[key], subkey) - ) { - errorMsgs.push( - `${key}.${subkey} missing. Default value (${ - Array.isArray(defaultProps[key][subkey]) - ? 'Array' - : typeof defaultProps[key][subkey] - }): ${defaultProps[key][subkey]}` - ); - solutionSet.add( - `Run 'mcdev upgrade' to fix missing or changed configuration options` - ); - missingFields.push(`${key}.${subkey}`); - } else if (subkey === 'deployment') { - for (const dkey in defaultProps[key][subkey]) { - if ( - Object.prototype.hasOwnProperty.call( - defaultProps[key][subkey], - dkey - ) && - !Object.prototype.hasOwnProperty.call( - properties[key][subkey], - dkey - ) - ) { - errorMsgs.push( - `${key}.${subkey} missing. Default value (${ - Array.isArray(defaultProps[key][subkey][dkey]) - ? 'Array' - : typeof defaultProps[key][subkey][dkey] - }): ${defaultProps[key][subkey][dkey]}` - ); - solutionSet.add( - `Run 'mcdev upgrade' to fix missing or changed configuration options` - ); - missingFields.push(`${key}.${subkey}.${dkey}`); - } - } - } - } - } - } - } - } - // check if project config version is outdated compared to user's mcdev version - if ( - !properties.version || - (![null, 'patch'].includes(semver.diff(packageJsonMcdev.version, properties.version)) && - semver.gt(packageJsonMcdev.version, properties.version)) - ) { - errorMsgs.push( - `Your project's config version ${properties.version} is lower than your Accenture SFMC DevTools version ${packageJsonMcdev.version}` - ); - solutionSet.add(`Run 'mcdev upgrade' to ensure optimal performance`); - missingFields.push('version'); - } - if (silent) { - return missingFields; - } else { - if (errorMsgs.length) { - const errorMsgOutput = [ - `Found problems in your ./${Util.configFileName} that you need to fix first:`, - ]; - for (const msg of errorMsgs) { - errorMsgOutput.push(' - ' + msg); - } - Util.logger.error(errorMsgOutput.join('\n')); - if (Util.skipInteraction) { - return false; - } - Util.logger.info( - [ - 'Here is what you can do to fix these issues:', - ...Array.from(solutionSet), - ].join('\n- ') - ); - const responses = await inquirer.prompt([ - { - type: 'confirm', - name: 'runUpgradeNow', - message: `Do you want to run 'mcdev upgrade' now?`, - default: true, - }, - ]); - if (responses.runUpgradeNow) { - // use _execSync here to avoid a circular dependency - this.execSync('mcdev', ['upgrade']); - } - return false; - } else { - return true; - } - } - }, loggerTransports: null, /** * Logger that creates timestamped log file in 'logs/' directory