UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

342 lines (340 loc) 16 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ // Node const os = require("os"); const util = require("util"); const path = require("path"); const fse = require("fs-extra"); // Thirdparty const command_1 = require("@salesforce/command"); const core_1 = require("@salesforce/core"); const lodash_1 = require("lodash"); const ts_types_1 = require("@salesforce/ts-types"); const kit_1 = require("@salesforce/kit"); const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve"); const configApi_1 = require("../../lib/core/configApi"); const jsonXmlTools_1 = require("../core/jsonXmlTools"); const OrgPrefRegistry = require("./orgPrefRegistry"); const js2xmlparser = require('js2xmlparser'); core_1.Messages.importMessagesDirectory(__dirname); const orgSettingsMessages = core_1.Messages.loadMessages('salesforce-alm', 'org_settings'); /** * Helper class for dealing with the settings that are defined in a scratch definition file. This class knows how to extract the * settings from the definition, how to expand them into a MD directory and how to generate a package.xml. */ class SettingsGenerator { constructor() { this.currentApiVersion = new configApi_1.Config().getApiVersion(); this.logger = core_1.Logger.childFromRoot('SettingsGenerator'); } /** extract the settings from the scratch def file, if they are present. */ async extract(scratchDef, apiVersion) { this.logger.debug('extracting settings from scratch definition file'); if (util.isNullOrUndefined(apiVersion)) { apiVersion = this.currentApiVersion; } if (apiVersion >= 47.0 && this.orgPreferenceSettingsMigrationRequired(scratchDef)) { await this.extractAndMigrateSettings(scratchDef); } else { this.settingData = ts_types_1.getObject(scratchDef, 'settings'); this.objectSettingsData = ts_types_1.getObject(scratchDef, 'objectSettings'); } this.logger.debug('settings are', this.settingData); // TODO, this is where we will validate the settings. // See W-5068155 // if (this.hasSettings()) { } } /** True if we are currently tracking setting or object setting data. */ hasSettings() { return !(kit_1.isEmpty(this.settingData) && kit_1.isEmpty(this.objectSettingsData)); } /** Check to see if the scratchDef contains orgPreferenceSettings * orgPreferenceSettings are no longer supported after api version 46.0 */ orgPreferenceSettingsMigrationRequired(scratchDef) { return !(util.isNullOrUndefined(scratchDef) || util.isNullOrUndefined(scratchDef.settings) || util.isNullOrUndefined(scratchDef.settings.orgPreferenceSettings)); } /** This will copy all of the settings in the scratchOrgInfo orgPreferences mapping into the settings structure. * It will also spit out a warning about the pending deprecation og the orgPreferences structure. * This returns a failure message in the promise upon critical error for api versions after 46.0. * For api versions less than 47.0 it will return a warning. */ async migrate(scratchDef, apiVersion) { // Make sure we have old style preferences if (!scratchDef.orgPreferences) { return; } if (util.isNullOrUndefined(apiVersion)) { apiVersion = this.currentApiVersion; } // First, let's map the old style tooling preferences into MD-API preferences this.settingData = {}; const ux = await command_1.UX.create(); function lhccmdt(mdt) { // lowercase head camel case metadata type return util.isNullOrUndefined(mdt) ? mdt : mdt.substring(0, 1).toLowerCase() + mdt.substring(1); } function storePrefs(data, pref, prefVal) { const orgPrefApi = lhccmdt(OrgPrefRegistry.whichApi(pref, apiVersion)); if (util.isNullOrUndefined(orgPrefApi)) { ux.warn(`Unsupported org preference: ${pref}, ignored`); return; } const mdApiName = lhccmdt(OrgPrefRegistry.forMdApi(pref, apiVersion)); if (!lodash_1.has(data, orgPrefApi)) { kit_1.set(data, orgPrefApi, {}); } const apiOrgPrefs = ts_types_1.getObject(data, orgPrefApi); kit_1.set(apiOrgPrefs, mdApiName, prefVal); } if (scratchDef.orgPreferences.enabled) { scratchDef.orgPreferences.enabled.forEach((pref) => { storePrefs(this.settingData, pref, true); }); } if (scratchDef.orgPreferences.disabled) { scratchDef.orgPreferences.disabled.forEach((pref) => { storePrefs(this.settingData, pref, false); }); } // It would be nice if cli.ux.styledJSON could return a colorized JSON string instead of logging to stdout. const message = orgSettingsMessages.getMessage(apiVersion >= 47.0 ? 'deprecatedPrefFormat' : 'deprecatedPrefFormatLegacy', [ JSON.stringify({ orgPreferences: scratchDef.orgPreferences }, null, 4), JSON.stringify({ settings: this.settingData }, null, 4), ]); if (apiVersion >= 47.0) { throw new Error(message); } else { ux.warn(message); } // No longer need these delete scratchDef.orgPreferences; } /** This method converts all orgPreferenceSettings preferences into their respective * org settings objects. */ async extractAndMigrateSettings(scratchDef) { const oldScratchDef = JSON.stringify({ settings: scratchDef.settings }, null, 4); // Make sure we have old style preferences if (!this.orgPreferenceSettingsMigrationRequired(scratchDef)) { this.settingData = ts_types_1.getObject(scratchDef, 'settings'); return; } // First, let's map the old style tooling preferences into MD-API preferences this.settingData = {}; const ux = await command_1.UX.create(); function storePrefs(data, pref, prefVal) { let mdApiName = OrgPrefRegistry.newPrefNameForOrgSettingsMigration(pref); if (util.isNullOrUndefined(mdApiName)) { mdApiName = pref; } const orgPrefApi = OrgPrefRegistry.whichApiFromFinalPrefName(mdApiName); if (util.isNullOrUndefined(orgPrefApi)) { ux.warn(`Unknown org preference: ${pref}, ignored.`); return false; } if (OrgPrefRegistry.isMigrationDeprecated(orgPrefApi)) { ux.warn(`The setting "${pref}" is no longer supported as of API version 47.0`); return false; } if (!lodash_1.has(data, orgPrefApi)) { kit_1.set(data, orgPrefApi, {}); } const apiOrgPrefs = ts_types_1.getObject(data, orgPrefApi); // check to see if the value is already set kit_1.set(apiOrgPrefs, mdApiName, prefVal); return orgPrefApi != OrgPrefRegistry.ORG_PREFERENCE_SETTINGS; } const orgPreferenceSettings = ts_types_1.getObject(scratchDef, 'settings.orgPreferenceSettings'); delete scratchDef.settings.orgPreferenceSettings; this.settingData = ts_types_1.getObject(scratchDef, 'settings'); let migrated = false; for (const preference in orgPreferenceSettings) { if (storePrefs(this.settingData, preference, orgPreferenceSettings[preference])) { migrated = true; } } // Since we could have recommended some preferences that are still in OPS, only warn if any actually got moved there if (migrated) { // It would be nice if cli.ux.styledJSON could return a colorized JSON string instead of logging to stdout. const message = orgSettingsMessages.getMessage('migratedPrefFormat', [ oldScratchDef, JSON.stringify({ settings: this.settingData }, null, 4), ]); ux.warn(message); } } /** Create temporary deploy directory used to upload the scratch org shape. * This will create the dir, all of the .setting files and minimal object files needed for objectSettings */ async createDeployDir() { // Base dir for deployment is always the os.tmpdir(). const shapeDirName = `shape_${Date.now()}`; const destRoot = path.join(os.tmpdir(), shapeDirName); const settingsDir = path.join(destRoot, 'settings'); const objectsDir = path.join(destRoot, 'objects'); try { await core_1.fs.mkdirp(settingsDir); await core_1.fs.mkdirp(objectsDir); } catch (e) { // If directory creation failed, the root dir probably doesn't exist, so we're fine this.logger.debug('caught error:', e); } await Promise.all([this.writeSettingsIfNeeded(settingsDir), this.writeObjectSettingsIfNeeded(objectsDir)]); // If SFDX_MDAPI_TEMP_DIR is set, copy settings to that dir for people to inspect. const mdapiTmpDir = kit_1.env.getString('SFDX_MDAPI_TEMP_DIR'); if (mdapiTmpDir) { const tmpDir = path.join(mdapiTmpDir, shapeDirName); this.logger.debug(`Copying settings to: ${tmpDir}`); await fse.copy(destRoot, tmpDir); } return destRoot; } /** * Deploys the settings to the org. */ async deploySettingsViaFolder(username, sourceFolder) { const ux = await command_1.UX.create(); this.logger.debug(`deploying settings from ${sourceFolder}`); const componentSet = source_deploy_retrieve_1.ComponentSet.fromSource(sourceFolder); const deploy = await componentSet.deploy({ usernameOrConnection: username }); // Wait for polling to finish and get the DeployResult object const result = await deploy.pollStatus(); // display errors if any const errors = result.getFileResponses().filter((fileResponse) => { fileResponse.state === source_deploy_retrieve_1.ComponentStatus.Failed; }); if (errors.length > 0) { ux.styledHeader(`Component Failures [${errors.length}]`); ux.table(errors, { columns: [ { key: 'problemType', label: 'Type' }, { key: 'fullName', label: 'Name' }, { key: 'error', label: 'Problem' }, ], }); } if (result.response.status === 'Failed') { throw new core_1.SfdxError(`A scratch org was created with username ${username}, but the settings failed to deploy`, 'ProblemDeployingSettings', errors.map((e) => `${e.fullName}: ${e.filePath} | ${e.state === source_deploy_retrieve_1.ComponentStatus.Failed ? e.error : ''}`)); } } async writeObjectSettingsIfNeeded(objectsDir) { if (!this.objectSettingsData) { return; } await core_1.fs.mkdirp(objectsDir); // TODO: parallelize all this FS for perf for (const objectName of Object.keys(this.objectSettingsData)) { const value = this.objectSettingsData[objectName]; // writes the object file in source format const objectDir = path.join(objectsDir, this.cap(objectName)); await core_1.fs.mkdirp(objectDir); await jsonXmlTools_1.writeJSONasXML({ path: path.join(objectDir, `${this.cap(objectName)}.object-meta.xml`), type: 'CustomObject', json: this.createObjectFileContent(value), }); if (value.defaultRecordType) { const recordTypesDir = path.join(objectDir, 'recordTypes'); await core_1.fs.mkdirp(recordTypesDir); const RTFileContent = this.createRecordTypeFileContent(objectName, value); await jsonXmlTools_1.writeJSONasXML({ path: path.join(recordTypesDir, `${this.cap(value.defaultRecordType)}.recordType-meta.xml`), type: 'RecordType', json: RTFileContent, }); // for things that required a businessProcess if (RTFileContent.businessProcess) { await core_1.fs.mkdirp(path.join(objectDir, 'businessProcesses')); await jsonXmlTools_1.writeJSONasXML({ path: path.join(objectDir, 'businessProcesses', `${RTFileContent.businessProcess}.businessProcess-meta.xml`), type: 'BusinessProcess', json: this.createBusinessProcessFileContent(objectName, RTFileContent.businessProcess), }); } } } } async writeSettingsIfNeeded(settingsDir) { if (this.settingData) { await core_1.fs.mkdirp(settingsDir); for (const item of Object.keys(this.settingData)) { const value = ts_types_1.getObject(this.settingData, item); const typeName = this.cap(item); const fname = typeName.replace('Settings', ''); const fileContent = this._createSettingsFileContent(typeName, value); await core_1.fs.writeFile(path.join(settingsDir, fname + '.settings-meta.xml'), fileContent); } } } _createSettingsFileContent(name, json) { if (name == 'OrgPreferenceSettings') { // this is a stupid format let res = `<?xml version="1.0" encoding="UTF-8"?> <OrgPreferenceSettings xmlns="http://soap.sforce.com/2006/04/metadata"> `; res += Object.keys(json) .map((pref) => ` <preferences> <settingName>` + this.cap(pref) + `</settingName> <settingValue>` + ts_types_1.get(json, pref) + `</settingValue> </preferences>`) .join('\n'); res += '\n</OrgPreferenceSettings>'; return res; } else { return js2xmlparser.parse(name, json); } } createObjectFileContent(json) { const output = {}; if (json.sharingModel) { output.sharingModel = this.cap(json.sharingModel); } return output; } createRecordTypeFileContent(objectName, setting) { const output = { fullName: this.cap(setting.defaultRecordType), label: this.cap(setting.defaultRecordType), active: true, }; // all the edge cases if (['Case', 'Lead', 'Opportunity', 'Solution'].includes(this.cap(objectName))) { return { ...output, businessProcess: `${this.cap(setting.defaultRecordType)}Process` }; } return output; } createBusinessProcessFileContent(objectName, businessProcessName) { const ObjectToBusinessProcessPicklist = { Opportunity: { fullName: 'Prospecting' }, Case: { fullName: 'New', default: true }, Lead: { fullName: 'New - Not Contacted', default: true }, Solution: { fullName: 'Draft', default: true }, }; return { fullName: businessProcessName, isActive: true, values: [ObjectToBusinessProcessPicklist[this.cap(objectName)]], }; } cap(s) { return s ? (s.length > 0 ? s.charAt(0).toUpperCase() + s.substring(1) : '') : null; } } module.exports = SettingsGenerator; //# sourceMappingURL=scratchOrgSettingsGenerator.js.map