salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
342 lines (340 loc) • 16 kB
JavaScript
"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