UNPKG

mcdev

Version:

Accenture Salesforce Marketing Cloud DevTools

356 lines (337 loc) 14.3 kB
'use strict'; import { Util } from './util/util.js'; import File from './util/file.js'; import config from './util/config.js'; import Cli from './util/cli.js'; import auth from './util/auth.js'; import MetadataTypeInfo from './MetadataTypeInfo.js'; /** * @typedef {import('../types/mcdev.d.js').BuObject} BuObject * @typedef {import('../types/mcdev.d.js').CodeExtract} CodeExtract * @typedef {import('../types/mcdev.d.js').CodeExtractItem} CodeExtractItem * @typedef {import('../types/mcdev.d.js').Mcdevrc} Mcdevrc * @typedef {import('../types/mcdev.d.js').MetadataTypeItem} MetadataTypeItem * @typedef {import('../types/mcdev.d.js').MetadataTypeItemDiff} MetadataTypeItemDiff * @typedef {import('../types/mcdev.d.js').MetadataTypeItemObj} MetadataTypeItemObj * @typedef {import('../types/mcdev.d.js').MetadataTypeMap} MetadataTypeMap * @typedef {import('../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj * @typedef {import('../types/mcdev.d.js').MultiMetadataTypeList} MultiMetadataTypeList * @typedef {import('../types/mcdev.d.js').SoapRequestParams} SoapRequestParams * @typedef {import('../types/mcdev.d.js').TemplateMap} TemplateMap */ /** * Builds metadata from a template using market specific customisation */ class Builder { /** * Creates a Builder, uses v2 auth if v2AuthOptions are passed. * * @param {Mcdevrc} properties properties for auth saved * @param {BuObject} buObject properties for auth */ constructor(properties, buObject) { this.properties = properties; this.templateDir = properties.directories.template; this.retrieveDir = File.normalizePath([ properties.directories.retrieve, buObject.credential, buObject.businessUnit, ]); this.buObject = buObject; // allow multiple target directories const templateBuildsArr = Array.isArray(properties.directories.templateBuilds) ? properties.directories.templateBuilds : [properties.directories.templateBuilds]; this.targetDir = templateBuildsArr.map((directoriesTemplateBuilds) => File.normalizePath([ directoriesTemplateBuilds, buObject.credential, buObject.businessUnit, ]) ); /** * @type {MultiMetadataTypeList} */ this.metadata = {}; } /** * Builds a specific metadata file by name * * @param {string} metadataType metadata type to build * @param {string[]} nameArr name of metadata to build * @param {TemplateMap} templateVariables variables to be replaced in the metadata * @returns {Promise.<MultiMetadataTypeList>} Promise */ async _buildDefinition(metadataType, nameArr, templateVariables) { const type = metadataType; try { const result = ( await Promise.all( nameArr.map((name) => { // with npx and powershell spaces are not parsed correctly as part of a string // we hence require users to put %20 in their stead and have to convert that back name = name.split('%20').join(' '); MetadataTypeInfo[type].client = auth.getSDK(this.buObject); MetadataTypeInfo[type].properties = this.properties; MetadataTypeInfo[type].buObject = this.buObject; return MetadataTypeInfo[type].buildDefinition( this.templateDir, this.targetDir, name, templateVariables ); }) ) ).filter(Boolean); if (result && type === result[0]?.type) { // result elements can be undefined for each key that we did not find this.metadata[type] = result.map((element) => element.metadata); } } catch (ex) { Util.logger.errorStack(ex, 'mcdev.buildDefinition'); } return this.metadata; } /** * Build a template based on a list of metadata files in the retrieve folder. * * @param {string} businessUnit references credentials from properties.json * @param {string} selectedType supported metadata type * @param {string[]} keyArr customerkey of the metadata * @param {string[]} marketArr market localizations * @returns {Promise.<MultiMetadataTypeList>} - */ static async buildTemplate(businessUnit, selectedType, keyArr, marketArr) { const properties = await config.getProperties(); if (!properties) { return; } if (!Util._isValidType(selectedType)) { return; } if (selectedType.includes('-')) { Util.logger.error( `:: '${selectedType}' is not a valid metadata type. Please don't include subtypes.` ); return; } /** @type {TemplateMap} */ const templateVariables = {}; if (marketArr[0] !== '__clone__') { // if __clone__ is passed, we don't want to actually change anything but simply clone the metadata as-is for (const market of marketArr) { if (Util.checkMarket(market, properties)) { Object.assign(templateVariables, properties.markets[market]); } else { // do not execute the rest of this method if a market was invalid return; } } } const buObject = await Cli.getCredentialObject(properties, businessUnit); if (buObject !== null) { const builder = new Builder(properties, buObject); return builder._buildTemplate(selectedType, keyArr, templateVariables); } } /** * Build a template based on a list of metadata files in the retrieve folder. * * @param {string} metadataType metadata type to create a template of * @param {string[]} keyArr customerkey of metadata to create a template of * @param {TemplateMap} templateVariables variables to be replaced in the metadata * @returns {Promise.<MultiMetadataTypeList>} Promise */ async _buildTemplate(metadataType, keyArr, templateVariables) { const type = metadataType; try { const removeKeys = []; /** @type {MetadataTypeItemObj[]} */ const result = ( await Promise.all( keyArr.map(async (key) => { MetadataTypeInfo[type].properties = this.properties; MetadataTypeInfo[type].buObject = this.buObject; try { /** @type {MetadataTypeItemObj} */ const response = await MetadataTypeInfo[type].buildTemplate( this.retrieveDir, this.templateDir, key, templateVariables ); if (!response) { removeKeys.push(key); } return response; } catch (ex) { removeKeys.push(key); Util.logger.errorStack(ex, ` ☇ skipping template asset: ${key}`); } }) ) ).filter(Boolean); // remove keys that errored out keyArr.splice(0, keyArr.length, ...keyArr.filter((key) => !removeKeys.includes(key))); if (result && type === result[0]?.type) { // result elements can be undefined for each key that we did not find this.metadata[type] = result.map((element) => element.metadata); } } catch (ex) { Util.logger.errorStack(ex, 'mcdev.buildTemplate'); } return this.metadata; } /** * Build a specific metadata file based on a template. * * @param {string} businessUnit references credentials from properties.json * @param {string} selectedType supported metadata type * @param {string[]} nameArr name of the metadata * @param {string[]} marketArr market localizations * @returns {Promise.<MultiMetadataTypeList>} - */ static async buildDefinition(businessUnit, selectedType, nameArr, marketArr) { const properties = await config.getProperties(); if (!properties) { return; } if (!Util._isValidType(selectedType)) { return; } if (selectedType.includes('-')) { Util.logger.error( `:: '${selectedType}' is not a valid metadata type. Please don't include subtypes.` ); return; } /** @type {TemplateMap} */ const templateVariables = {}; if (marketArr[0] !== '__clone__') { // if __clone__ is passed, we don't want to actually change anything but simply clone the metadata as-is for (const market of marketArr) { if (Util.checkMarket(market, properties)) { Object.assign(templateVariables, properties.markets[market]); } else { // do not execute the rest of this method if a market was invalid return; } } } const buObject = await Cli.getCredentialObject(properties, businessUnit); if (buObject !== null) { const builder = new Builder(properties, buObject); return builder._buildDefinition(selectedType, nameArr, templateVariables); } } /** * Build a specific metadata file based on a template using a list of bu-market combos * * @param {string} listName name of list of BU-market combos * @param {string} type supported metadata type * @param {string[]} nameArr name of the metadata * @returns {Promise.<object>} - */ static async buildDefinitionBulk(listName, type, nameArr) { const properties = await config.getProperties(); if (!properties) { return; } try { Util.verifyMarketList(listName, properties); } catch (ex) { Util.logger.error(ex.message); return; } if (type && !MetadataTypeInfo[type]) { Util.logger.error(`:: '${type}' is not a valid metadata type`); return; } let i = 0; const responseObj = {}; for (const businessUnit in properties.marketList[listName]) { if (businessUnit === 'description') { // skip, it's just a metadata on this list and not a BU continue; } i++; /** @type {string | string[] | string[][]} */ const market = properties.marketList[listName][businessUnit]; const marketList = 'string' === typeof market ? [market] : market; for (const market of marketList) { // one can now send multiple markets to buildTemplate/buildDefinition and hence that also needs to work for marketLists const marketArr = 'string' === typeof market ? [market] : market; for (const market of marketArr) { if (!Util.checkMarket(market, properties)) { return; } } Util.logger.info(`Executing for '${businessUnit}': '${marketArr.join('-')}'`); // omitting "await" to speed up creation responseObj[businessUnit] ||= {}; responseObj[businessUnit][marketArr.join('-')] = await this.buildDefinition( businessUnit, type, nameArr, marketArr ); } } if (!i) { Util.logger.error('Please define properties.marketList in your config'); } return responseObj; } /** * helper for buildDefinitionBulk, createDeltaPkg * * @param {string} listName market list name * @returns {Promise.<void>} - */ static async purgeDeployFolderList(listName) { const properties = await config.getProperties(); if (!properties) { return; } for (const businessUnit in properties.marketList[listName]) { if (businessUnit === 'description') { // skip, it's just a metadata on this list and not a BU continue; } if (!Util.isValidBU(properties, businessUnit, true)) { throw new Error(`'${businessUnit}' in Market ${listName} is not defined.`); } await this.purgeDeployFolder(businessUnit); } } /** * helper for buildDefiniton, purgeDeployFolderList * * @param {string} businessUnit cred/bu combo * @returns {Promise.<void>} - */ static async purgeDeployFolder(businessUnit) { const properties = await config.getProperties(); if (!properties) { return; } if (!Util.isValidBU(properties, businessUnit, true)) { throw new Error(`'${businessUnit}' does not exist.`); } const deployDir = File.normalizePath([ properties.directories.deploy, ...businessUnit.split('/'), ]); // Clear output folder structure for selected sub-type // only run this if the standard deploy folder is a target of buildDefinition (which technically could be changed) Util.logger.info(` - 🚮 purging folder ${deployDir}`); try { await File.remove(deployDir); } catch { // sometimes the first attempt is not successful for some operating system reason. Trying again mostly solves this await File.remove(deployDir); } } } export default Builder;