UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

1,100 lines 65.1 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 */ /* 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