@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
1,100 lines • 65.1 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
*/
/* eslint-disable class-methods-use-this */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Org = exports.SandboxEvents = exports.OrgTypes = void 0;
exports.sandboxIsResumable = sandboxIsResumable;
const node_path_1 = require("node:path");
const fs = __importStar(require("node:fs"));
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const config_1 = require("../config/config");
const configAggregator_1 = require("../config/configAggregator");
const orgUsersConfig_1 = require("../config/orgUsersConfig");
const global_1 = require("../global");
const lifecycleEvents_1 = require("../lifecycleEvents");
const logger_1 = require("../logger/logger");
const sfError_1 = require("../sfError");
const sfdc_1 = require("../util/sfdc");
const webOAuthServer_1 = require("../webOAuthServer");
const messages_1 = require("../messages");
const stateAggregator_1 = require("../stateAggregator/stateAggregator");
const pollingClient_1 = require("../status/pollingClient");
const connection_1 = require("./connection");
const authInfo_1 = require("./authInfo");
const scratchOrgCreate_1 = require("./scratchOrgCreate");
const orgConfigProperties_1 = require("./orgConfigProperties");
;
const messages = new messages_1.Messages('@salesforce/core', 'org', new Map([["notADevHub", "The provided dev hub username %s is not a valid dev hub."], ["noUsernameFound", "No username found."], ["noDevHubFound", "Unable to associate this scratch org with a DevHub."], ["deleteOrgHubError", "The Dev Hub org cannot be deleted."], ["insufficientAccessToDelete", "You do not have the appropriate permissions to delete a scratch org. Please contact your Salesforce admin."], ["scratchOrgNotFound", "Attempting to delete an expired or deleted org"], ["sandboxDeleteFailed", "The sandbox org deletion failed with a result of %s."], ["sandboxNotFound", "We can't find a SandboxProcess for the sandbox %s."], ["sandboxInfoCreateFailed", "The sandbox org creation failed with a result of %s."], ["sandboxInfoRefreshFailed", "The sandbox org refresh failed with a result of %s."], ["missingAuthUsername", "The sandbox %s does not have an authorized username."], ["orgPollingTimeout", "Sandbox status is %s; timed out waiting for completion."], ["NotFoundOnDevHub", "The scratch org does not belong to the dev hub username %s."], ["AuthInfoOrgIdUndefined", "AuthInfo orgId is undefined."], ["sandboxCreateNotComplete", "The sandbox creation has not completed."], ["SandboxProcessNotFoundBySandboxName", "We can't find a SandboxProcess with the SandboxName %s."], ["MultiSandboxProcessNotFoundBySandboxName", "We found more than one SandboxProcess with the SandboxName %s."], ["sandboxNotResumable", "The sandbox %s cannot resume with status of %s."]]));
var OrgTypes;
(function (OrgTypes) {
OrgTypes["Scratch"] = "scratch";
OrgTypes["Sandbox"] = "sandbox";
})(OrgTypes || (exports.OrgTypes = OrgTypes = {}));
var SandboxEvents;
(function (SandboxEvents) {
SandboxEvents["EVENT_STATUS"] = "status";
SandboxEvents["EVENT_ASYNC_RESULT"] = "asyncResult";
SandboxEvents["EVENT_RESULT"] = "result";
SandboxEvents["EVENT_AUTH"] = "auth";
SandboxEvents["EVENT_RESUME"] = "resume";
SandboxEvents["EVENT_MULTIPLE_SBX_PROCESSES"] = "multipleMatchingSbxProcesses";
})(SandboxEvents || (exports.SandboxEvents = SandboxEvents = {}));
const resumableSandboxStatus = ['Activating', 'Pending', 'Pending Activation', 'Processing', 'Sampling', 'Completed'];
function sandboxIsResumable(value) {
return resumableSandboxStatus.includes(value);
}
const sandboxProcessFields = [
'Id',
'Status',
'SandboxName',
'SandboxInfoId',
'LicenseType',
'CreatedDate',
'CopyProgress',
'SandboxOrganization',
'SourceId',
'Description',
'EndDate',
];
/**
* Provides a way to manage a locally authenticated Org.
*
* **See** {@link AuthInfo}
*
* **See** {@link Connection}
*
* **See** {@link Aliases}
*
* **See** {@link Config}
*
* ```
* // Email username
* const org1: Org = await Org.create({ aliasOrUsername: 'foo@example.com' });
* // The target-org config property
* const org2: Org = await Org.create();
* // Full Connection
* const org3: Org = await Org.create({
* connection: await Connection.create({
* authInfo: await AuthInfo.create({ username: 'username' })
* })
* });
* ```
*
* **See** https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_cli_usernames_orgs.htm
*/
class Org extends kit_1.AsyncOptionalCreatable {
status = Org.Status.UNKNOWN;
configAggregator;
// Initialized in create
logger;
connection;
options;
orgId;
/**
* @ignore
*/
constructor(options) {
super(options);
this.options = options ?? {};
}
/**
* create a sandbox from a production org
* 'this' needs to be a production org with sandbox licenses available
*
* @param sandboxReq SandboxRequest options to create the sandbox with
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
*/
async createSandbox(sandboxReq, options = {
wait: kit_1.Duration.minutes(6),
async: false,
interval: kit_1.Duration.seconds(30),
}) {
this.logger.debug(sandboxReq, 'CreateSandbox called with SandboxRequest');
const createResult = await this.connection.tooling.create('SandboxInfo', sandboxReq);
this.logger.debug(createResult, 'Return from calling tooling.create');
if (Array.isArray(createResult) || !createResult.success) {
throw messages.createError('sandboxInfoCreateFailed', [JSON.stringify(createResult)]);
}
const sandboxCreationProgress = await this.querySandboxProcessBySandboxInfoId(createResult.id);
this.logger.debug(sandboxCreationProgress, 'Return from calling singleRecordQuery with tooling');
const isAsync = !!options.async;
if (isAsync) {
// The user didn't want us to poll, so simply return the status
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxCreationProgress);
return sandboxCreationProgress;
}
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(sandboxCreationProgress, `create - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxCreationProgress,
wait,
pollInterval,
});
}
/**
* Refresh (update) a sandbox from a production org.
* 'this' needs to be a production org with sandbox licenses available
*
* @param sandboxInfo SandboxInfo to update the sandbox with
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
*/
async refreshSandbox(sandboxInfo, options = {
wait: kit_1.Duration.minutes(6),
async: false,
interval: kit_1.Duration.seconds(30),
}) {
this.logger.debug(sandboxInfo, 'RefreshSandbox called with SandboxInfo');
const refreshResult = await this.connection.tooling.update('SandboxInfo', sandboxInfo);
this.logger.debug(refreshResult, 'Return from calling tooling.update');
if (!refreshResult.success) {
throw messages.createError('sandboxInfoRefreshFailed', [JSON.stringify(refreshResult)]);
}
const soql = `SELECT ${sandboxProcessFields.join(',')} FROM SandboxProcess WHERE SandboxName='${sandboxInfo.SandboxName}' ORDER BY CreatedDate DESC`;
const sbxProcessObjects = (await this.connection.tooling.query(soql)).records.filter((item) => !item.Status.startsWith('Del'));
this.logger.debug(sbxProcessObjects, `SandboxProcesses for ${sandboxInfo.SandboxName}`);
// throw if none found
if (sbxProcessObjects?.length === 0) {
throw new Error(`No SandboxProcesses found for: ${sandboxInfo.SandboxName}`);
}
const sandboxRefreshProgress = sbxProcessObjects[0];
const isAsync = !!options.async;
if (isAsync) {
// The user didn't want us to poll, so simply return the status
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxRefreshProgress);
return sandboxRefreshProgress;
}
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(sandboxRefreshProgress, `refresh - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxRefreshProgress,
wait,
pollInterval,
});
}
/**
*
* @param sandboxReq SandboxRequest options to create the sandbox with
* @param sourceSandboxName the name of the sandbox that your new sandbox will be based on
* @param options Wait: The amount of time to wait before timing out, defaults to 0, Interval: The time interval between polling defaults to 30 seconds
* @returns {SandboxProcessObject} the newly created sandbox process object
*/
async cloneSandbox(sandboxReq, sourceSandboxName, options) {
const SourceId = (await this.querySandboxProcessBySandboxName(sourceSandboxName)).SandboxInfoId;
this.logger.debug(`Clone sandbox sourceId ${SourceId}`);
return this.createSandbox({ ...sandboxReq, SourceId }, options);
}
/**
* Resume a sandbox create or refresh from a production org.
* `this` needs to be a production org with sandbox licenses available.
*
* @param resumeSandboxRequest SandboxRequest options to create/refresh the sandbox with
* @param options Wait: The amount of time to wait (default: 0 minutes) before timing out,
* Interval: The time interval (default: 30 seconds) between polling
*/
async resumeSandbox(resumeSandboxRequest, options = {
wait: kit_1.Duration.minutes(0),
async: false,
interval: kit_1.Duration.seconds(30),
}) {
this.logger.debug(resumeSandboxRequest, 'ResumeSandbox called with ResumeSandboxRequest');
let sandboxCreationProgress;
// seed the sandboxCreationProgress via the resumeSandboxRequest options
if (resumeSandboxRequest.SandboxProcessObjId) {
sandboxCreationProgress = await this.querySandboxProcessById(resumeSandboxRequest.SandboxProcessObjId);
}
else if (resumeSandboxRequest.SandboxName) {
try {
// There can be multiple sandbox processes returned when querying by name. Use the most recent
// process and fire a warning event with all processes.
sandboxCreationProgress = await this.querySandboxProcessBySandboxName(resumeSandboxRequest.SandboxName);
}
catch (err) {
if (err instanceof sfError_1.SfError && err.name === 'SingleRecordQuery_MultipleRecords' && err.data) {
const sbxProcesses = err.data;
// 0 index will always be the most recently created process since the query sorts on created date desc.
sandboxCreationProgress = sbxProcesses[0];
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, sbxProcesses);
}
else {
throw err;
}
}
}
else {
throw messages.createError('sandboxNotFound', [
resumeSandboxRequest.SandboxName ?? resumeSandboxRequest.SandboxProcessObjId,
]);
}
this.logger.debug(sandboxCreationProgress, 'Return from calling singleRecordQuery with tooling');
if (!sandboxIsResumable(sandboxCreationProgress.Status)) {
throw messages.createError('sandboxNotResumable', [
sandboxCreationProgress.SandboxName,
sandboxCreationProgress.Status,
]);
}
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_RESUME, sandboxCreationProgress);
const [wait, pollInterval] = this.validateWaitOptions(options);
// if wait is 0, return the sandboxCreationProgress immediately
if (wait.seconds === 0) {
if (sandboxCreationProgress.Status === 'Completed') {
// check to see if sandbox can authenticate via sandboxAuth endpoint
const sandboxInfo = await this.sandboxSignupComplete(sandboxCreationProgress);
if (sandboxInfo) {
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_AUTH, sandboxInfo);
try {
this.logger.debug(sandboxInfo, 'sandbox signup complete');
await this.writeSandboxAuthFile(sandboxCreationProgress, sandboxInfo);
return sandboxCreationProgress;
}
catch (err) {
// eat the error, we don't want to throw an error if we can't write the file
}
}
}
await lifecycleEvents_1.Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxCreationProgress);
throw messages.createError('sandboxCreateNotComplete');
}
this.logger.debug(sandboxCreationProgress, `resume - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxCreationProgress,
wait,
pollInterval,
});
}
/**
* Creates a scratchOrg
* 'this' needs to be a valid dev-hub
*
* @param {options} ScratchOrgCreateOptions
* @returns {ScratchOrgCreateResult}
*/
async scratchOrgCreate(options) {
return (0, scratchOrgCreate_1.scratchOrgCreate)({ ...options, hubOrg: this });
}
/**
* Reports sandbox org creation status. If the org is ready, authenticates to the org.
*
* @param {sandboxname} string the sandbox name
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
* @returns {SandboxProcessObject} the sandbox process object
*/
async sandboxStatus(sandboxname, options) {
return this.authWithRetriesByName(sandboxname, options);
}
/**
* Clean all data files in the org's data path. Usually <workspace>/.sfdx/orgs/<username>.
*
* @param orgDataPath A relative path other than "orgs/".
* @param throwWhenRemoveFails Should the remove org operations throw an error on failure?
*/
async cleanLocalOrgData(orgDataPath, throwWhenRemoveFails = false) {
let dataPath;
try {
dataPath = await this.getLocalDataDir(orgDataPath);
this.logger.debug(`cleaning data for path: ${dataPath}`);
}
catch (err) {
if (err instanceof Error && err.name === 'InvalidProjectWorkspaceError') {
// If we aren't in a project dir, we can't clean up data files.
// If the user unlink this org outside of the workspace they used it in,
// data files will be left over.
return;
}
throw err;
}
return this.manageDelete(async () => fs.promises.rmdir(dataPath), dataPath, throwWhenRemoveFails);
}
/**
* @ignore
*/
async retrieveOrgUsersConfig() {
return orgUsersConfig_1.OrgUsersConfig.create(orgUsersConfig_1.OrgUsersConfig.getOptions(this.getOrgId()));
}
/**
* Cleans up all org related artifacts including users, sandbox config (if a sandbox), source tracking files, and auth file.
*
* @param throwWhenRemoveFails Determines if the call should throw an error or fail silently.
*/
async remove(throwWhenRemoveFails = false) {
// If deleting via the access token there shouldn't be any auth config files
// so just return;
if (this.getConnection().isUsingAccessToken()) {
return Promise.resolve();
}
await this.removeSandboxConfig();
await this.removeUsers(throwWhenRemoveFails);
await this.removeUsersConfig();
// An attempt to remove this org's auth file occurs in this.removeUsersConfig. That's because this org's usersname is also
// included in the OrgUser config file.
//
// So, just in case no users are added to this org we will try the remove again.
await this.removeAuth();
await this.removeSourceTrackingFiles();
}
/**
* Check if org is a sandbox org by checking its SandboxOrgConfig.
*
*/
async isSandbox() {
return (await stateAggregator_1.StateAggregator.getInstance()).sandboxes.hasFile(this.getOrgId());
}
/**
* Check that this org is a scratch org by asking the dev hub if it knows about it.
*
* **Throws** *{@link SfError}{ name: 'NotADevHubError' }* Not a Dev Hub.
*
* **Throws** *{@link SfError}{ name: 'NoResultsError' }* No results.
*
* @param devHubUsernameOrAlias The username or alias of the dev hub org.
*/
async checkScratchOrg(devHubUsernameOrAlias) {
let aliasOrUsername = devHubUsernameOrAlias;
if (!aliasOrUsername) {
aliasOrUsername = this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB);
}
const devHubConnection = (await Org.create({ aliasOrUsername })).getConnection();
const thisOrgAuthConfig = this.getConnection().getAuthInfoFields();
const trimmedId = (0, sfdc_1.trimTo15)(thisOrgAuthConfig.orgId);
const DEV_HUB_SOQL = `SELECT CreatedDate,Edition,ExpirationDate FROM ActiveScratchOrg WHERE ScratchOrg='${trimmedId}'`;
try {
const results = await devHubConnection.query(DEV_HUB_SOQL);
if (results.records.length !== 1) {
throw new sfError_1.SfError('No results', 'NoResultsError');
}
}
catch (err) {
if (err instanceof Error && err.name === 'INVALID_TYPE') {
throw messages.createError('notADevHub', [devHubConnection.getUsername()]);
}
throw err;
}
return thisOrgAuthConfig;
}
/**
* Returns the Org object or null if this org is not affiliated with a Dev Hub (according to the local config).
*/
async getDevHubOrg() {
if (this.isDevHubOrg()) {
return this;
}
else if (this.getField(Org.Fields.DEV_HUB_USERNAME)) {
const devHubUsername = (0, ts_types_1.ensureString)(this.getField(Org.Fields.DEV_HUB_USERNAME));
return Org.create({
connection: await connection_1.Connection.create({
authInfo: await authInfo_1.AuthInfo.create({ username: devHubUsername }),
}),
});
}
}
/**
* Returns `true` if the org is a Dev Hub.
*
* **Note** This relies on a cached value in the auth file. If that property
* is not cached, this method will **always return false even if the org is a
* dev hub**. If you need accuracy, use the {@link Org.determineIfDevHubOrg} method.
*/
isDevHubOrg() {
const isDevHub = this.getField(Org.Fields.IS_DEV_HUB);
if ((0, ts_types_1.isBoolean)(isDevHub)) {
return isDevHub;
}
else {
return false;
}
}
/**
* Will delete 'this' instance remotely and any files locally
*
* @param controllingOrg username or Org that 'this.devhub' or 'this.production' refers to. AKA a DevHub for a scratch org, or a Production Org for a sandbox
*/
async deleteFrom(controllingOrg) {
const resolvedOrg = typeof controllingOrg === 'string'
? await Org.create({
aggregator: this.configAggregator,
aliasOrUsername: controllingOrg,
})
: controllingOrg;
if (await this.isSandbox()) {
await this.deleteSandbox(resolvedOrg);
}
else {
await this.deleteScratchOrg(resolvedOrg);
}
}
/**
* Will delete 'this' instance remotely and any files locally
*/
async delete() {
const username = (0, ts_types_1.ensureString)(this.getUsername());
// unset any aliases referencing this org
const stateAgg = await stateAggregator_1.StateAggregator.getInstance();
const existingAliases = stateAgg.aliases.getAll(username);
await stateAgg.aliases.unsetValuesAndSave(existingAliases);
// unset any configs referencing this org
await Promise.all([...existingAliases, username].flatMap((name) => this.configAggregator.unsetByValue(name)));
if (await this.isSandbox()) {
await this.deleteSandbox();
}
else {
await this.deleteScratchOrg();
}
}
/**
* Returns `true` if the org is a Dev Hub.
*
* Use a cached value. If the cached value is not set, then check access to the
* ScratchOrgInfo object to determine if the org is a dev hub.
*
* @param forceServerCheck Ignore the cached value and go straight to the server
* which will be required if the org flips on the dev hub after the value is already
* cached locally.
*/
async determineIfDevHubOrg(forceServerCheck = false) {
const cachedIsDevHub = this.getField(Org.Fields.IS_DEV_HUB);
if (!forceServerCheck && (0, ts_types_1.isBoolean)(cachedIsDevHub)) {
return cachedIsDevHub;
}
if (this.isDevHubOrg()) {
return true;
}
this.logger.debug('isDevHub is not cached - querying server...');
const conn = this.getConnection();
let isDevHub = false;
try {
await conn.query('SELECT Id FROM ScratchOrgInfo limit 1');
isDevHub = true;
}
catch (err) {
/* Not a dev hub */
}
const username = (0, ts_types_1.ensure)(this.getUsername());
const authInfo = await authInfo_1.AuthInfo.create({ username });
await authInfo.save({ isDevHub });
// Reset the connection with the updated auth file
this.connection = await connection_1.Connection.create({ authInfo });
return isDevHub;
}
/**
* Returns `true` if the org is a scratch org.
*
* **Note** This relies on a cached value in the auth file. If that property
* is not cached, this method will **always return false even if the org is a
* scratch org**. If you need accuracy, use the {@link Org.determineIfScratch} method.
*/
isScratch() {
const isScratch = this.getField(Org.Fields.IS_SCRATCH);
if ((0, ts_types_1.isBoolean)(isScratch)) {
return isScratch;
}
else {
return false;
}
}
/**
* Returns `true` if the org uses source tracking.
* Side effect: updates files where the property doesn't currently exist
*/
async tracksSource() {
// use the property if it exists
const tracksSource = this.getField(Org.Fields.TRACKS_SOURCE);
if ((0, ts_types_1.isBoolean)(tracksSource)) {
return tracksSource;
}
// scratch orgs with no property use tracking by default
if (await this.determineIfScratch()) {
// save true for next time to avoid checking again
await this.setTracksSource(true);
return true;
}
if (await this.determineIfSandbox()) {
// does the sandbox know about the SourceMember object?
const supportsSourceMembers = await this.supportsSourceTracking();
await this.setTracksSource(supportsSourceMembers);
return supportsSourceMembers;
}
// any other non-sandbox, non-scratch orgs won't use tracking
await this.setTracksSource(false);
return false;
}
/**
* Set the tracking property on the org's auth file
*
* @param value true or false (whether the org should use source tracking or not)
*/
async setTracksSource(value) {
const originalAuth = await authInfo_1.AuthInfo.create({ username: this.getUsername() });
return originalAuth.handleAliasAndDefaultSettings({
setDefault: false,
setDefaultDevHub: false,
setTracksSource: value,
});
}
/**
* Returns `true` if the org is a scratch org.
*
* Use a cached value. If the cached value is not set, then check
* `Organization.IsSandbox == true && Organization.TrialExpirationDate != null`
* using {@link Org.retrieveOrganizationInformation}.
*/
async determineIfScratch() {
const cache = this.getField(Org.Fields.IS_SCRATCH);
if (cache !== undefined) {
return cache;
}
const updated = await this.updateLocalInformation();
return updated?.isScratch === true;
}
/**
* Returns `true` if the org is a sandbox.
*
* Use a cached value. If the cached value is not set, then check
* `Organization.IsSandbox == true && Organization.TrialExpirationDate == null`
* using {@link Org.retrieveOrganizationInformation}.
*/
async determineIfSandbox() {
const cache = this.getField(Org.Fields.IS_SANDBOX);
if (cache !== undefined) {
return cache;
}
const updated = await this.updateLocalInformation();
return updated?.isSandbox === true;
}
/**
* Retrieve a handful of fields from the Organization table in Salesforce. If this does not have the
* data you need, just use {@link Connection.singleRecordQuery} with `SELECT <needed fields> FROM Organization`.
*
* @returns org information
*/
async retrieveOrganizationInformation() {
return this.getConnection().singleRecordQuery('SELECT Name, InstanceName, IsSandbox, TrialExpirationDate, NamespacePrefix FROM Organization');
}
/**
* Some organization information is locally cached, such as if the org name or if it is a scratch org.
* This method populates/updates the filesystem from information retrieved from the org.
*/
async updateLocalInformation() {
const username = this.getUsername();
if (username) {
const [stateAggregator, organization] = await Promise.all([
stateAggregator_1.StateAggregator.getInstance(),
this.retrieveOrganizationInformation(),
]);
const updateFields = {
[Org.Fields.NAME]: organization.Name,
[Org.Fields.INSTANCE_NAME]: organization.InstanceName,
[Org.Fields.NAMESPACE_PREFIX]: organization.NamespacePrefix,
[Org.Fields.IS_SANDBOX]: organization.IsSandbox && !organization.TrialExpirationDate,
[Org.Fields.IS_SCRATCH]: organization.IsSandbox && Boolean(organization.TrialExpirationDate),
[Org.Fields.TRIAL_EXPIRATION_DATE]: organization.TrialExpirationDate,
};
stateAggregator.orgs.update(username, updateFields);
await stateAggregator.orgs.write(username);
return updateFields;
}
}
/**
* Executes a HEAD request on the baseUrl to force an auth refresh.
* This is useful for the raw methods (request, requestRaw) that use the accessToken directly and don't handle refreshes.
*
* This method issues a request using the current access token to check if it is still valid.
* If the request returns 200, no refresh happens, and we keep the token.
* If it returns 401, jsforce will request a new token and set it in the connection instance.
*/
async refreshAuth() {
this.logger.debug('Refreshing auth for org.');
const requestInfo = {
url: this.getConnection().baseUrl(),
method: 'HEAD',
};
await this.getConnection().request(requestInfo);
}
/**
* Reads and returns the content of all user auth files for this org as an array.
*/
async readUserAuthFiles() {
const config = await this.retrieveOrgUsersConfig();
const contents = await config.read();
const thisUsername = (0, ts_types_1.ensure)(this.getUsername());
const usernames = (0, ts_types_1.ensureJsonArray)(contents.usernames ?? [thisUsername]);
return Promise.all(usernames.map((username) => authInfo_1.AuthInfo.create({
username: username === thisUsername ? this.getConnection().getUsername() : (0, ts_types_1.ensureString)(username),
})));
}
/**
* Adds a username to the user config for this org. For convenience `this` object is returned.
*
* ```
* const org: Org = await Org.create({
* connection: await Connection.create({
* authInfo: await AuthInfo.create('foo@example.com')
* })
* });
* const userAuth: AuthInfo = await AuthInfo.create({
* username: 'bar@example.com'
* });
* await org.addUsername(userAuth);
* ```
*
* @param {AuthInfo | string} auth The AuthInfo for the username to add.
*/
async addUsername(auth) {
if (!auth) {
throw new sfError_1.SfError('Missing auth info', 'MissingAuthInfo');
}
const authInfo = (0, ts_types_1.isString)(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth;
this.logger.debug(`adding username ${authInfo.getFields().username}`);
const orgConfig = await this.retrieveOrgUsersConfig();
const contents = await orgConfig.read();
// TODO: This is kind of screwy because contents values can be `AnyJson | object`...
// needs config refactoring to improve
const usernames = contents.usernames ?? [];
if (!Array.isArray(usernames)) {
throw new sfError_1.SfError('Usernames is not an array', 'UnexpectedDataFormat');
}
let shouldUpdate = false;
const thisUsername = (0, ts_types_1.ensure)(this.getUsername());
if (!usernames.includes(thisUsername)) {
usernames.push(thisUsername);
shouldUpdate = true;
}
const username = authInfo.getFields().username;
if (username) {
usernames.push(username);
shouldUpdate = true;
}
if (shouldUpdate) {
orgConfig.set('usernames', usernames);
await orgConfig.write();
}
return this;
}
/**
* Removes a username from the user config for this object. For convenience `this` object is returned.
*
* **Throws** *{@link SfError}{ name: 'MissingAuthInfoError' }* Auth info is missing.
*
* @param {AuthInfo | string} auth The AuthInfo containing the username to remove.
*/
async removeUsername(auth) {
if (!auth) {
throw new sfError_1.SfError('Missing auth info', 'MissingAuthInfoError');
}
const authInfo = (0, ts_types_1.isString)(auth) ? await authInfo_1.AuthInfo.create({ username: auth }) : auth;
this.logger.debug(`removing username ${authInfo.getFields().username}`);
const orgConfig = await this.retrieveOrgUsersConfig();
const contents = await orgConfig.read();
const targetUser = authInfo.getFields().username;
const usernames = (contents.usernames ?? []).filter((username) => username !== targetUser);
orgConfig.set('usernames', usernames);
await orgConfig.write();
return this;
}
/**
* set the sandbox config related to this given org
*
* @param orgId {string} orgId of the sandbox
* @param config {SandboxFields} config of the sandbox
*/
async setSandboxConfig(orgId, config) {
(await stateAggregator_1.StateAggregator.getInstance()).sandboxes.set(orgId, config);
return this;
}
/**
* get the sandbox config for the given orgId
*
* @param orgId {string} orgId of the sandbox
*/
async getSandboxConfig(orgId) {
return (await stateAggregator_1.StateAggregator.getInstance()).sandboxes.read(orgId);
}
/**
* Retrieves the highest api version that is supported by the target server instance. If the apiVersion configured for
* Sfdx is greater than the one returned in this call an api version mismatch occurs. In the case of the CLI that
* results in a warning.
*/
async retrieveMaxApiVersion() {
return this.getConnection().retrieveMaxApiVersion();
}
/**
* Returns the admin username used to create the org.
*/
getUsername() {
return this.getConnection().getUsername();
}
/**
* Returns the orgId for this org.
*/
getOrgId() {
return this.orgId ?? this.getField(Org.Fields.ORG_ID);
}
/**
* Returns for the config aggregator.
*/
getConfigAggregator() {
return this.configAggregator;
}
/**
* Returns an org field. Returns undefined if the field is not set or invalid.
*/
getField(key) {
/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
// @ts-ignore Legacy. We really shouldn't be doing this.
const ownProp = this[key];
if (ownProp && typeof ownProp !== 'function')
return ownProp;
// @ts-ignore
return this.getConnection().getAuthInfoFields()[key];
/* eslint-enable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
}
/**
* Returns a map of requested fields.
*/
getFields(keys) {
return Object.fromEntries(keys.map((key) => [key, this.getField(key)]));
}
/**
* Returns the JSForce connection for the org.
* side effect: If you pass it an apiVersion, it will set it on the Org
* so that future calls to getConnection() will also use that version.
*
* @param apiVersion The API version to use for the connection.
*/
getConnection(apiVersion) {
if (apiVersion) {
if (this.connection.getApiVersion() === apiVersion) {
this.logger.warn(`Default API version is already ${apiVersion}`);
}
else {
this.connection.setApiVersion(apiVersion);
}
}
return this.connection;
}
async supportsSourceTracking() {
if (this.isScratch()) {
return true;
}
try {
await this.getConnection().tooling.sobject('SourceMember').describe();
return true;
}
catch (err) {
if (err.message.includes('The requested resource does not exist')) {
return false;
}
throw err;
}
}
/**
* query SandboxProcess via sandbox name
*
* @param name SandboxName to query for
*/
async querySandboxProcessBySandboxName(name) {
return this.querySandboxProcess(`SandboxName='${name}'`);
}
/**
* query SandboxProcess via SandboxInfoId
*
* @param id SandboxInfoId to query for
*/
async querySandboxProcessBySandboxInfoId(id) {
return this.querySandboxProcess(`SandboxInfoId='${id}'`);
}
/**
* query SandboxProcess via Id
*
* @param id SandboxProcessId to query for
*/
async querySandboxProcessById(id) {
return this.querySandboxProcess(`Id='${id}'`);
}
/**
* query SandboxProcess via SandboxOrganization (sandbox Org ID)
*
* @param sandboxOrgId SandboxOrganization ID to query for
*/
async querySandboxProcessByOrgId(sandboxOrgId) {
// Must query with a 15 character Org ID
return this.querySandboxProcess(`SandboxOrganization='${(0, sfdc_1.trimTo15)(sandboxOrgId)}'`);
}
/**
* Initialize async components.
*/
async init() {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
this.logger = (await logger_1.Logger.child('Org')).getRawLogger();
this.configAggregator = this.options.aggregator ? this.options.aggregator : await configAggregator_1.ConfigAggregator.create();
if (!this.options.connection) {
if (this.options.aliasOrUsername == null) {
this.configAggregator = this.getConfigAggregator();
const aliasOrUsername = this.options.isDevHub
? this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB)
: this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG);
this.options.aliasOrUsername = aliasOrUsername ?? undefined;
}
const username = stateAggregator.aliases.resolveUsername(this.options.aliasOrUsername);
if (!username) {
throw messages.createError('noUsernameFound');
}
this.connection = await connection_1.Connection.create({
// If no username is provided or resolvable from an alias, AuthInfo will throw an SfError.
authInfo: await authInfo_1.AuthInfo.create({ username, isDevHub: this.options.isDevHub }),
});
}
else {
this.connection = this.options.connection;
}
this.orgId = this.getField(Org.Fields.ORG_ID);
}
/**
* **Throws** *{@link SfError}{ name: 'NotSupportedError' }* Throws an unsupported error.
*/
// eslint-disable-next-line class-methods-use-this
getDefaultOptions() {
throw new sfError_1.SfError('Not Supported', 'NotSupportedError');
}
async getLocalDataDir(orgDataPath) {
const rootFolder = await config_1.Config.resolveRootFolder(false);
return (0, node_path_1.join)(rootFolder, global_1.Global.SFDX_STATE_FOLDER, orgDataPath ? orgDataPath : 'orgs');
}
/**
* Gets the sandboxProcessObject and then polls for it to complete.
*
* @param sandboxProcessName sanbox process name
* @param options { wait?: Duration; interval?: Duration }
* @returns {SandboxProcessObject} The SandboxProcessObject for the sandbox
*/
async authWithRetriesByName(sandboxProcessName, options) {
return this.authWithRetries(await this.queryLatestSandboxProcessBySandboxName(sandboxProcessName), options);
}
/**
* Polls the sandbox org for the sandboxProcessObject.
*
* @param sandboxProcessObj: The in-progress sandbox signup request
* @param options { wait?: Duration; interval?: Duration }
* @returns {SandboxProcessObject}
*/
async authWithRetries(sandboxProcessObj, options = {
wait: kit_1.Duration.minutes(0),
interval: kit_1.Duration.seconds(30),
}) {
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(sandboxProcessObj, `AuthWithRetries sandboxProcessObj, max wait time of ${wait.minutes} minutes`);
return this.pollStatusAndAuth({
sandboxProcessObj,
wait,
pollInterval,
});
}
/**
* Query the sandbox for the SandboxProcessObject by sandbox name
*
* @param sandboxName The name of the sandbox to query
* @returns {SandboxProcessObject} The SandboxProcessObject for the sandbox
*/
async queryLatestSandboxProcessBySandboxName(sandboxNameIn) {
const { tooling } = this.getConnection();
this.logger.debug(`QueryLatestSandboxProcessBySandboxName called with SandboxName: ${sandboxNameIn}`);
const queryStr = `SELECT ${sandboxProcessFields.join(',')} FROM SandboxProcess WHERE SandboxName='${sandboxNameIn}' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1`;
const queryResult = await tooling.query(queryStr);
this.logger.debug(queryResult, 'Return from calling queryToolingApi');
if (queryResult?.records?.length === 1) {
return queryResult.records[0];
}
else if (queryResult.records && queryResult.records.length > 1) {
throw messages.createError('MultiSandboxProcessNotFoundBySandboxName', [sandboxNameIn]);
}
else {
throw messages.createError('SandboxProcessNotFoundBySandboxName', [sandboxNameIn]);
}
}
// eslint-disable-next-line class-methods-use-this
async queryProduction(org, field, value) {
return org.connection.singleRecordQuery(`SELECT SandboxInfoId FROM SandboxProcess WHERE ${field} ='${value}' AND Status NOT IN ('D', 'E')`, { tooling: true });
}
async destroySandbox(org, id) {
return org.getConnection().tooling.delete('SandboxInfo', id);
}
async destroyScratchOrg(org, id) {
return org.getConnection().delete('ActiveScratchOrg', id);
}
/**
* this method will delete the sandbox org from the production org and clean up any local files
*
* @param prodOrg - Production org associated with this sandbox
* @private
*/
async deleteSandbox(prodOrg) {
const sandbox = await this.getSandboxConfig(this.getOrgId());
const resolvedProdOrg = prodOrg ??
(await Org.create({
aggregator: this.configAggregator,
aliasOrUsername: sandbox?.prodOrgUsername,
}));
let sandboxInfoId = sandbox?.sandboxInfoId;
if (!sandboxInfoId) {
let result;
try {
// grab sandboxName from config or try to calculate from the sandbox username
const sandboxName = sandbox?.sandboxName ?? (this.getUsername() ?? '').split(`${resolvedProdOrg.getUsername()}.`)[1];
if (!sandboxName) {
this.logger.debug('Sandbox name is not available');
// jump to query by orgId
throw new Error();
}
this.logger.debug(`attempting to locate sandbox with sandbox ${sandboxName}`);
try {
result = await this.queryProduction(resolvedProdOrg, 'SandboxName', sandboxName);
}
catch (err) {
this.logger.debug(`Failed to find sandbox with sandbox name: ${sandboxName}`);
// jump to query by orgId
throw err;
}
}
catch {
// if an error is thrown, don't panic yet. we'll try querying by orgId
const trimmedId = (0, sfdc_1.trimTo15)(this.getOrgId());
this.logger.debug(`defaulting to trimming id from ${this.getOrgId()} to ${trimmedId}`);
try {
result = await this.queryProduction(resolvedProdOrg, 'SandboxOrganization', trimmedId);
sandboxInfoId = result.SandboxInfoId;
}
catch {
// eating exceptions when trying to find sandbox process record by orgId
// allows idempotent cleanup of sandbox orgs
this.logger.debug(`Failed find a SandboxProcess for the sandbox org: ${this.getOrgId()}`);
}
}
}
if (sandboxInfoId) {
const deleteResult = await this.destroySandbox(resolvedProdOrg, sandboxInfoId);
this.logger.debug(deleteResult, 'Return from calling tooling.delete');
}
// cleanup remaining artifacts
await this.remove();
}
/**
* If this Org is a scratch org, calling this method will delete the scratch org from the DevHub and clean up any local files
*
* @param devHub - optional DevHub Org of the to-be-deleted scratch org
* @private
*/
async deleteScratchOrg(devHub) {
// if we didn't get a devHub, we'll get it from the this org
const resolvedDevHub = devHub ?? (await this.getDevHubOrg());
if (!resolvedDevHub) {
throw messages.createError('noDevHubFound');
}
if (resolvedDevHub.getOrgId() === this.getOrgId()) {
// we're attempting to delete a DevHub
throw messages.createError('deleteOrgHubError');
}
try {
const devHubConn = resolvedDevHub.getConnection();
const username = this.getUsername();
const activeScratchOrgRecordId = (await devHubConn.singleRecordQuery(`SELECT Id FROM ActiveScratchOrg WHERE SignupUsername='${username}'`)).Id;
this.logger.trace(`found matching ActiveScratchOrg with SignupUsername: ${username}. Deleting...`);
await this.destroyScratchOrg(resolvedDevHub, activeScratchOrgRecordId);
await this.remove();
}
catch (err) {
this.logger.info(err instanceof Error ? err.message : err);
if (err instanceof Error && (err.name === 'INVALID_TYPE' || err.name === 'INSUFFICIENT_ACCESS_OR_READONLY')) {
// most likely from devHubConn.delete
this.logger.info('Insufficient privilege to access ActiveScratchOrgs.');
throw messages.createError('insufficientAccessToDelete');
}
if (err instanceof Error && err.name === connection_1.SingleRecordQueryErrors.NoRecords) {
// most likely from singleRecordQuery
this.logger.info('The above error can be the result of deleting an expired or already deleted org.');
this.logger.info('attempting to cleanup the auth file');
await this.removeAuth();
throw messages.createError('scratchOrgNotFound');
}
throw err;
}
}
/**
* Delete an auth info file from the local file system and any related cache information for
* this Org. You don't want to call this method directly. Instead consider calling Org.remove()
*/
async removeAuth() {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
const username = this.getUsername();
// If there is no username, it has already been removed from the globalInfo.
// We can skip the unset and just ensure that globalInfo is updated.
if (username) {
this.logger.debug(`Removing auth for user: ${username}`);
this.logger.debug(`Clearing auth cache for user: ${username}`);
await stateAggregator.orgs.remove(username);
}
}
/**
* Deletes the users config file
*/
async removeUsersConfig() {
const config = await this.retrieveOrgUsersConfig();
if (await config.exists()) {
this.logger.debug(`Removing org users config at: ${config.getPath()}`);
await config.unlink();
}
}
async manageDelete(cb, dirPath, throwWhenRemoveFails) {
return cb().catch((e) => {
if (throwWhenRemoveFails) {
throw e;
}
else {
this.logger.warn(`failed to read directory ${dirPath}`);
return;
}
});
}
/**
* Remove the org users auth file.
*
* @param throwWhenRemoveFails true if manageDelete should throw or not if the deleted fails.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async removeUsers(throwWhenRemoveFails) {
const stateAggregator = await stateAggregator_1.StateAggregator.getInstance();
this.logger.debug(`Removing users associate with org: ${this.getOrgId()}`);
const config = await this.retrieveOrgUsersConfig();
this.logger.debug(`using path for org users: ${config.getPath()}`);
const usernames = (await this.readUserAuthFiles()).map((auth) => auth.getFields().username).filter(ts_types_1.isString);
await Promise.all(usernames.map(async (username) => {
const orgForUser = username === this.getUsername()
? this
: await Org.create({
connection: await conne