salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
410 lines (408 loc) • 15.9 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
*/
/* --------------------------------------------------------------------------------------------------------------------
* WARNING: This file has been deprecated and should now be considered locked against further changes. Its contents
* have been partially or wholely superceded by functionality included in the @salesforce/core npm package, and exists
* now to service prior uses in this repository only until they can be ported to use the new @salesforce/core library.
*
* If you need or want help deciding where to add new functionality or how to migrate to the new library, please
* contact the CLI team at alm-cli@salesforce.com.
* ----------------------------------------------------------------------------------------------------------------- */
// Node
const path = require("path");
// Thirdparty
const BBPromise = require("bluebird");
const optional = require("optional-js");
const _ = require("lodash");
const mkdirp = require("mkdirp");
// Local
const core_1 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const core_2 = require("@salesforce/core");
const orgConfigAttributes = require("../org/orgConfigAttributes");
const MdapiDeployApi = require("../mdapi/mdapiDeployApi");
const Messages = require("../messages");
const configApi_1 = require("./configApi");
// import * as almError from './almError';
const configValidator = require("./configValidator");
const logger = require("./logApi");
const srcDevUtil = require("./srcDevUtil");
const defaultConnectedAppInfo = require('./defaultConnectedApp');
const messages = Messages();
const urls = require('../urls');
const fs = BBPromise.promisifyAll(require('fs'));
const _buildNoOrgError = (org) => {
let message = messages.getMessage('defaultOrgNotFound', org.type);
if (!_.isNil(org.name)) {
message = messages.getMessage('namedOrgNotFound', org.name);
}
const noConfigError = new Error(message);
noConfigError.name = 'NoOrgFound';
if (org.type === core_1.Config.DEFAULT_USERNAME) {
kit_1.set(noConfigError, 'action', messages.getMessage('defaultOrgNotFoundAction'));
}
else if (org.type === core_1.Config.DEFAULT_DEV_HUB_USERNAME) {
kit_1.set(noConfigError, 'action', messages.getMessage('defaultOrgNotFoundDevHubAction'));
}
return noConfigError;
};
/**
* Represents a config json file in the state folder that consumers can interact with.
*
* TODO Extract out
* TODO Make async. Has huge implications on source*.js files
* TODO remove config with workspace.js in sfdx-core
*/
class StateFile {
constructor(config, filePath, contents = {}) {
this.path = path.join(config.getProjectPath(), srcDevUtil.getWorkspaceStateFolderName(), filePath);
this.backupPath = `${this.path}.bak`;
this.contents = contents;
}
_read(filePath) {
// TODO use readJSON when async
try {
return JSON.parse(fs.readFileSync(filePath));
}
catch (e) {
if (e.code === 'ENOENT') {
return {};
}
else {
throw e;
}
}
}
_write(filePath, contents) {
mkdirp.sync(path.dirname(filePath));
fs.writeFileSync(filePath, JSON.stringify(contents, null, 4));
}
_exist(filePath) {
try {
return fs.statSync(filePath);
}
catch (err) {
return false;
}
}
_delete(filePath) {
if (this._exist(filePath)) {
return fs.unlinkSync(filePath);
}
return false;
}
read() {
this.contents = this._read(this.path);
return this.contents;
}
write(newContents) {
if (!_.isNil(newContents)) {
this.contents = newContents;
}
this._write(this.path, this.contents);
return this.contents;
}
exist() {
return this._exist(this.path);
}
delete() {
return this._delete(this.path);
}
backup() {
if (this.exist()) {
this._write(this.backupPath, this.read());
}
}
revert() {
if (this._exist(this.backupPath)) {
this.write(this._read(this.backupPath));
this._delete(this.backupPath);
}
return this.contents;
}
}
/**
* @deprecated The functionality is moving to sfdx-core
*/
class Org {
/**
* Construct a new org. No configuration is initialized at this point. To
* get any auth data, getConfig must first be called which will try to get
* the default org of the given type unless the setName method is called.
* Any calls to org.force will call getConfig.
*
* @param {Force} force The force api
* @param {string} type The type of org for the CLI. This is used to store
* and find defaults for the project.
* @constructor
*/
constructor(force, type = core_1.Config.DEFAULT_USERNAME) {
// eslint-disable-next-line
const Force = require('./force');
this.force = optional.ofNullable(force).orElse(new Force(new configApi_1.Config()));
this.config = this.force.getConfig();
this.logger = logger.child('Org');
this.type = type;
this.mdDeploy = new MdapiDeployApi(this);
}
retrieveMaxApiVersion() {
// getting the max api version does not require auth. So if you think adding a call to refreshAuth here is the correct
// thing to do. it's not!
return this.force.getApiVersions(this).then((versions) => _.maxBy(versions, (_ver) => _ver.version));
}
/**
* Gets the name of this scratch org.
*/
getName() {
return this.name;
}
async resolveDefaultName() {
// If the name is set, we don't want to resolve the default
if (this.getName()) {
return;
}
const sfdxConfig = await this.resolvedAggregator();
const name = sfdxConfig.getPropertyValue(this.type);
const orgName = (await core_1.Aliases.create({})).get(name);
this.setName(orgName !== null && orgName !== void 0 ? orgName : name);
}
/**
* Sets the name of this scratch org. After setting the name any call to getConfig will result in the org associated
* with $HOME/.sfdx/[name].json being returned.
*
* @param name - the name of the org.
*/
setName(name) {
this.name = name;
this.logger.setConfig('username', name);
this.force.logger.setConfig('username', name);
}
async resolvedAggregator() {
if (!this.aggregator) {
this.aggregator = core_1.ConfigAggregator.create();
}
return this.aggregator;
}
async initializeConfig() {
let config;
try {
config = await core_1.Config.create({ isGlobal: false });
}
catch (err) {
if (err.name === 'InvalidProjectWorkspace') {
config = await core_1.Config.create({ isGlobal: true });
}
else {
throw err;
}
}
return config;
}
getDataPath(filename) {
const username = this.getName();
if (!username) {
throw _buildNoOrgError(this);
}
// Create a path like <project>/.sfdx/orgs/<username>/<filename>
return path.join(...['orgs', username, filename].filter((e) => !!e));
}
/**
* Clean all data files in the org's data path, then remove the data directory.
* Usually <workspace>/.sfdx/orgs/<username>
*/
cleanData(orgDataPath) {
let dataPath;
try {
dataPath = path.join(this.config.getProjectPath(), srcDevUtil.getWorkspaceStateFolderName(), orgDataPath || this.getDataPath());
}
catch (err) {
if (err.name === 'InvalidProjectWorkspace') {
// If we aren't in a project dir, we can't clean up data files.
// If the user deletes this org outside of the workspace they used it in,
// data files will be left over.
return;
}
throw err;
}
const removeDir = (dirPath) => {
let stats;
try {
stats = fs
.readdirSync(dirPath)
.map((file) => path.join(dirPath, file))
.map((filePath) => ({ filePath, stat: fs.statSync(filePath) }));
stats.filter(({ stat }) => stat.isDirectory()).forEach(({ filePath }) => removeDir(filePath));
stats.filter(({ stat }) => stat.isFile()).forEach(({ filePath }) => fs.unlinkSync(filePath));
fs.rmdirSync(dirPath);
}
catch (err) {
this.logger.warn(`failed to read directory ${dirPath}`);
}
};
removeDir(dataPath);
}
/**
* Get the full path to the file storing the maximum revision value from the last valid pull from workspace scratch org
*
* @param wsPath - The root path of the workspace
* @returns {*}
*/
getMaxRevision() {
return new StateFile(this.config, this.getDataPath('maxRevision.json'));
}
/**
* Get the full path to the file storing the workspace source path information
*
* @param wsPath - The root path of the workspace
* @returns {*}
*/
getSourcePathInfos() {
return new StateFile(this.config, this.getDataPath('sourcePathInfos.json'));
}
/**
* Returns a promise to retrieve the ScratchOrg configuration for this workspace.
*
* @returns {BBPromise}
*/
getConfig() {
if (this.authConfig) {
return BBPromise.resolve(this.authConfig);
}
return this.resolveDefaultName()
.then(() => this.resolvedAggregator())
.then((sfdxConfig) => {
const username = this.getName();
// The username of the org can be set by the username config var, env var, or command line.
// If the username is not set, getName will resolve to the default username for the workspace.
// If the username is an access token, use that instead of getting the username auth file.
const accessTokenMatch = _.isString(username) && username.match(/^(00D\w{12,15})![\.\w]*$/);
if (accessTokenMatch) {
let instanceUrl;
const orgId = accessTokenMatch[1];
this.usingAccessToken = true;
// If it is an env var, use it instead of the local workspace sfdcLoginUrl property,
// otherwise try to use the local sfdx-project property instead.
if (sfdxConfig.getInfo('instanceUrl').isEnvVar()) {
instanceUrl = sfdxConfig.getPropertyValue('instanceUrl');
}
else {
instanceUrl = sfdxConfig.getPropertyValue('instanceUrl') || urls.production;
}
// If the username isn't an email, is it as a accessToken
return {
accessToken: username,
instanceUrl,
orgId,
};
}
else {
return srcDevUtil
.getGlobalConfig(`${username}.json`)
.then((config) => configValidator.getCleanObject(config, orgConfigAttributes, false))
.then((config) => {
if (_.isNil(config.clientId)) {
config.clientId = defaultConnectedAppInfo.legacyClientId;
config.clientSecret = defaultConnectedAppInfo.legacyClientSecret;
}
return config;
})
.catch((error) => {
let returnError = error;
if (error.code === 'ENOENT') {
returnError = _buildNoOrgError(this);
}
return BBPromise.reject(returnError);
});
}
})
.then((config) => {
this.authConfig = config;
return config;
});
}
getFileName() {
return `${this.name}.json`;
}
/**
* Returns a promise to save a valid workspace scratch org configuration to disk.
*
* @param configObject - The object to save. If the object isn't valid an error will be thrown.
* { orgId:, redirectUri:, accessToken:, refreshToken:, instanceUrl:, clientId: }
* @param saveAsDefault {boolean} - whether to save this org as the default for this workspace.
* @returns {BBPromise.<Object>} Not the access tokens will be encrypted. Call get config to get decrypted access tokens.
*/
saveConfig(configObject, saveAsDefault) {
if (this.usingAccessToken) {
return BBPromise.resolve(configObject);
}
this.name = configObject.username;
let savedData;
// For security reasons we don't want to arbitrarily write the configObject to disk.
return configValidator
.getCleanObject(configObject, orgConfigAttributes, true)
.then((dataToSave) => {
savedData = dataToSave;
return srcDevUtil.saveGlobalConfig(this.getFileName(), savedData);
})
.then(async () => {
core_2.AuthInfo.clearCache(configObject.username);
this.logger.info(`Saved org configuration: ${this.getFileName()}`);
if (saveAsDefault) {
const config = await this.initializeConfig();
config.set(this.type, this.alias || this.name);
await config.write();
}
this.authConfig = configObject;
return BBPromise.resolve(savedData);
});
}
/**
* @deprecated See Org.ts in sfdx-core
*/
static async create(username, defaultType) {
// If orgType is undefined, Org will use the right default.
const org = new Org(undefined, defaultType);
if (_.isString(username) && !_.isEmpty(username)) {
// Check if the user is an alias
const alias = await core_1.Aliases.create({});
const actualUsername = alias.get(username);
const verbose = srcDevUtil.isVerbose();
if (_.isString(actualUsername) && !_.isEmpty(actualUsername)) {
if (verbose) {
logger.log(`Using resolved username ${actualUsername} from alias ${username}${logger.getEOL()}`);
}
org.alias = username;
org.setName(actualUsername);
}
else {
if (verbose) {
logger.log(`Using specified username ${username}${logger.getEOL()}`);
}
org.setName(username);
}
}
// If the username isn't set or passed in, the default username
// will be resolved on config.
await org.getConfig();
return org;
}
}
/**
* Org types that can be set as a default for local and global configs.
* All commands target USERNAME, except commands that specify a different
* default, like org:create specifing DEVHUB has a default.
*/
Org.Defaults = {
DEVHUB: core_1.Config.DEFAULT_DEV_HUB_USERNAME,
USERNAME: core_1.Config.DEFAULT_USERNAME,
list() {
return [core_1.Config.DEFAULT_DEV_HUB_USERNAME, core_1.Config.DEFAULT_USERNAME];
},
};
module.exports = Org;
//# sourceMappingURL=scratchOrgApi.js.map