UNPKG

@salesforce/core

Version:

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

717 lines 29.4 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Org = void 0; const path_1 = require("path"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const aliases_1 = require("../config/aliases"); const config_1 = require("../config/config"); const configAggregator_1 = require("../config/configAggregator"); const orgUsersConfig_1 = require("../config/orgUsersConfig"); const sandboxOrgConfig_1 = require("../config/sandboxOrgConfig"); const global_1 = require("../global"); const logger_1 = require("../logger"); const sfdxError_1 = require("../sfdxError"); const fs_1 = require("../util/fs"); const sfdc_1 = require("../util/sfdc"); const globalInfoConfig_1 = require("../config/globalInfoConfig"); const messages_1 = require("../messages"); const connection_1 = require("./connection"); const authInfo_1 = require("./authInfo"); const orgConfigProperties_1 = require("./orgConfigProperties"); messages_1.Messages.importMessagesDirectory(__dirname); const messages = messages_1.Messages.load('@salesforce/core', 'org', ['notADevHub']); /** * 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 defaultusername 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 { /** * @ignore */ constructor(options) { super(options); this.status = Org.Status.UNKNOWN; this.options = 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 { const rootFolder = await config_1.Config.resolveRootFolder(false); dataPath = path_1.join(rootFolder, global_1.Global.SFDX_STATE_FOLDER, orgDataPath ? orgDataPath : 'orgs'); this.logger.debug(`cleaning data for path: ${dataPath}`); } catch (err) { if (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 () => await fs_1.fs.remove(dataPath), dataPath, throwWhenRemoveFails); } /** * @ignore */ async retrieveOrgUsersConfig() { return await orgUsersConfig_1.OrgUsersConfig.create(orgUsersConfig_1.OrgUsersConfig.getOptions(this.getOrgId())); } /** * Removes the scratch org config file at $HOME/.sfdx/[name].json, any project level org * files, all user auth files for the org, matching default config settings, and any * matching aliases. * * @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(throwWhenRemoveFails); 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(); } /** * Check that this org is a scratch org by asking the dev hub if it knows about it. * * **Throws** *{@link SfdxError}{ name: 'NotADevHubError' }* Not a Dev Hub. * * **Throws** *{@link SfdxError}{ 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) || this.configAggregator.getPropertyValue(config_1.SfdxPropertyKeys.DEFAULT_DEV_HUB_USERNAME); } const devHubConnection = (await Org.create({ aliasOrUsername })).getConnection(); const thisOrgAuthConfig = this.getConnection().getAuthInfoFields(); const trimmedId = sfdc_1.sfdc.trimTo15(thisOrgAuthConfig.orgId); const DEV_HUB_SOQL = `SELECT CreatedDate,Edition,ExpirationDate FROM ActiveScratchOrg WHERE ScratchOrg='${trimmedId}'`; let results; try { results = await devHubConnection.query(DEV_HUB_SOQL); } catch (err) { if (err.name === 'INVALID_TYPE') { throw messages.createError('notADevHub', [devHubConnection.getUsername()]); } throw err; } if (ts_types_1.getNumber(results, 'records.length') !== 1) { throw new sfdxError_1.SfdxError('No results', 'NoResultsError'); } 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 = 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 (ts_types_1.isBoolean(isDevHub)) { return isDevHub; } else { return false; } } /** * 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 && 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 = 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 (ts_types_1.isBoolean(isScratch)) { return isScratch; } else { return false; } } /** * 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() { let cache = this.getField(Org.Fields.IS_SCRATCH); if (!cache) { await this.updateLocalInformation(); cache = this.getField(Org.Fields.IS_SCRATCH); } return cache; } /** * Returns `true` if the org is a sandbox. * * **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 * sandbox**. If you need accuracy, use the {@link Org.determineIfDevHubOrg} method. */ isSandbox() { const isSandbox = this.getField(Org.Fields.IS_SANDBOX); if (ts_types_1.isBoolean(isSandbox)) { return isSandbox; } else { return false; } } /** * 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() { let cache = this.getField(Org.Fields.IS_SANDBOX); if (!cache) { await this.updateLocalInformation(); cache = this.getField(Org.Fields.IS_SANDBOX); } return cache; } /** * 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 organization = await this.retrieveOrganizationInformation(); const isScratch = organization.IsSandbox && organization.TrialExpirationDate; const isSandbox = organization.IsSandbox && !organization.TrialExpirationDate; const info = await globalInfoConfig_1.GlobalInfo.getInstance(); info.updateOrg(username, { [Org.Fields.NAME]: organization.Name, [Org.Fields.INSTANCE_NAME]: organization.InstanceName, [Org.Fields.NAMESPACE_PREFIX]: organization.NamespacePrefix, [Org.Fields.IS_SANDBOX]: isSandbox, [Org.Fields.IS_SCRATCH]: isScratch, [Org.Fields.TRIAL_EXPIRATION_DATE]: organization.TrialExpirationDate, }); await info.write(); } } /** * Refreshes the auth for this org's instance by calling HTTP GET on the baseUrl of the connection object. */ async refreshAuth() { this.logger.debug('Refreshing auth for org.'); const requestInfo = { url: this.getConnection().baseUrl(), method: 'GET', }; const conn = this.getConnection(); await conn.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 = ts_types_1.ensure(this.getUsername()); const usernames = ts_types_1.ensureJsonArray(contents.usernames || [thisUsername]); return Promise.all(usernames.map((username) => { if (username === thisUsername) { return authInfo_1.AuthInfo.create({ username: this.getConnection().getUsername(), }); } else { return authInfo_1.AuthInfo.create({ username: 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 sfdxError_1.SfdxError('Missing auth info', 'MissingAuthInfo'); } const authInfo = 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 (!ts_types_1.isArray(usernames)) { throw new sfdxError_1.SfdxError('Usernames is not an array', 'UnexpectedDataFormat'); } let shouldUpdate = false; const thisUsername = 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 SfdxError}{ name: 'MissingAuthInfoError' }* Auth info is missing. * * @param {AuthInfo | string} auth The AuthInfo containing the username to remove. */ async removeUsername(auth) { if (!auth) { throw new sfdxError_1.SfdxError('Missing auth info', 'MissingAuthInfoError'); } const authInfo = 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 || []); contents.usernames = usernames.filter((username) => username !== targetUser); await orgConfig.write(); return this; } /** * Sets the key/value pair in the sandbox config for this org. For convenience `this` object is returned. * * * @param {key} The key for this value * @param {value} The value to save */ async setSandboxOrgConfigField(field, value) { const sandboxOrgConfig = await this.retrieveSandboxOrgConfig(); sandboxOrgConfig.set(field, value); await sandboxOrgConfig.write(); return this; } /** * Returns an org config field. Returns undefined if the field is not set or invalid. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async getSandboxOrgConfigField(field) { const sandboxOrgConfig = await this.retrieveSandboxOrgConfig(); return sandboxOrgConfig.get(field); } /** * 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 await 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-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Legacy. We really shouldn't be doing this. const ownProp = this[key]; if (ownProp && typeof ownProp !== 'function') return ownProp; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return this.getConnection().getAuthInfoFields()[key]; } /** * Returns a map of requested fields. */ getFields(keys) { const json = {}; return keys.reduce((map, key) => { map[key] = this.getField(key); return map; }, json); } /** * Returns the JSForce connection for the org. */ getConnection() { return this.connection; } /** * Initialize async components. */ async init() { this.logger = await logger_1.Logger.child('Org'); 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) || // Fall back to old sfdx key this.configAggregator.getPropertyValue(config_1.SfdxPropertyKeys.DEFAULT_DEV_HUB_USERNAME) : this.configAggregator.getPropertyValue(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG) || // Fall back to old sfdx key this.configAggregator.getPropertyValue(config_1.SfdxPropertyKeys.DEFAULT_USERNAME); this.options.aliasOrUsername = aliasOrUsername || undefined; } const username = this.options.aliasOrUsername; this.connection = await connection_1.Connection.create({ // If no username is provided or resolvable from an alias, AuthInfo will throw an SfdxError. authInfo: await authInfo_1.AuthInfo.create({ username: (username != null && (await aliases_1.Aliases.fetch(username))) || username, }), }); } else { this.connection = this.options.connection; } this.orgId = this.getField(Org.Fields.ORG_ID); } /** * **Throws** *{@link SfdxError}{ name: 'NotSupportedError' }* Throws an unsupported error. */ getDefaultOptions() { throw new sfdxError_1.SfdxError('Not Supported', 'NotSupportedError'); } /** * 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 config = await globalInfoConfig_1.GlobalInfo.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}`); config.unsetOrg(username); } await config.write(); } /** * 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(); } } /** * @ignore */ async retrieveSandboxOrgConfig() { return await sandboxOrgConfig_1.SandboxOrgConfig.create(sandboxOrgConfig_1.SandboxOrgConfig.getOptions(this.getOrgId())); } 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) { 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()}`); if (await config.exists()) { const authInfos = await this.readUserAuthFiles(); const aliases = await aliases_1.Aliases.create(aliases_1.Aliases.getDefaultOptions()); this.logger.info(`Cleaning up usernames in org: ${this.getOrgId()}`); for (const auth of authInfos) { const username = auth.getFields().username; const aliasKeys = (username && aliases.getKeysByValue(username)) || []; aliases.unsetAll(aliasKeys); let orgForUser; if (username === this.getUsername()) { orgForUser = this; } else { const info = await authInfo_1.AuthInfo.create({ username }); const connection = await connection_1.Connection.create({ authInfo: info }); orgForUser = await Org.create({ connection }); } const removeConfig = async (configInfo) => { if ((configInfo.value === username || aliasKeys.includes(configInfo.value)) && (configInfo.isGlobal() || configInfo.isLocal())) { await config_1.Config.update(configInfo.isGlobal(), configInfo.key, undefined); } }; await removeConfig(this.configAggregator.getInfo(config_1.SfdxPropertyKeys.DEFAULT_DEV_HUB_USERNAME)); await removeConfig(this.configAggregator.getInfo(config_1.SfdxPropertyKeys.DEFAULT_USERNAME)); await removeConfig(this.configAggregator.getInfo(orgConfigProperties_1.OrgConfigProperties.TARGET_DEV_HUB)); await removeConfig(this.configAggregator.getInfo(orgConfigProperties_1.OrgConfigProperties.TARGET_ORG)); await orgForUser.removeAuth(); } await aliases.write(); } } /** * Remove an associate sandbox config. * * @param throwWhenRemoveFails true if manageDelete should throw or not if the deleted fails. */ async removeSandboxConfig(throwWhenRemoveFails) { const sandboxOrgConfig = await this.retrieveSandboxOrgConfig(); if (await sandboxOrgConfig.exists()) { await this.manageDelete(async () => await sandboxOrgConfig.unlink(), sandboxOrgConfig.getPath(), throwWhenRemoveFails); } } } exports.Org = Org; (function (Org) { /** * Scratch Org status. */ let Status; (function (Status) { /** * The scratch org is active. */ Status["ACTIVE"] = "ACTIVE"; /** * The scratch org has expired. */ Status["EXPIRED"] = "EXPIRED"; /** * The org is a scratch Org but no dev hub is indicated. */ Status["UNKNOWN"] = "UNKNOWN"; /** * The dev hub configuration is reporting an active Scratch org but the AuthInfo cannot be found. */ Status["MISSING"] = "MISSING"; })(Status = Org.Status || (Org.Status = {})); /** * Org Fields. */ // A subset of fields from AuthInfoFields and properties that are specific to Org, // and properties that are defined on Org itself. let Fields; (function (Fields) { /** * The org alias. */ // From AuthInfo Fields["ALIAS"] = "alias"; Fields["CREATED"] = "created"; // From Organization Fields["NAME"] = "name"; Fields["NAMESPACE_PREFIX"] = "namespacePrefix"; Fields["INSTANCE_NAME"] = "instanceName"; Fields["TRIAL_EXPIRATION_DATE"] = "trailExpirationDate"; /** * The Salesforce instance the org was created on. e.g. `cs42`. */ Fields["CREATED_ORG_INSTANCE"] = "createdOrgInstance"; /** * The username of the dev hub org that created this org. Only populated for scratch orgs. */ Fields["DEV_HUB_USERNAME"] = "devHubUsername"; /** * The full url of the instance the org lives on. */ Fields["INSTANCE_URL"] = "instanceUrl"; /** * Is the current org a dev hub org. e.g. They have access to the `ScratchOrgInfo` object. */ Fields["IS_DEV_HUB"] = "isDevHub"; /** * Is the current org a scratch org. e.g. Organization has IsSandbox == true and TrialExpirationDate != null. */ Fields["IS_SCRATCH"] = "isScratch"; /** * Is the current org a dev hub org. e.g. Organization has IsSandbox == true and TrialExpirationDate == null. */ Fields["IS_SANDBOX"] = "isSandbox"; /** * The login url of the org. e.g. `https://login.salesforce.com` or `https://test.salesforce.com`. */ Fields["LOGIN_URL"] = "loginUrl"; /** * The org ID. */ Fields["ORG_ID"] = "orgId"; /** * The `OrgStatus` of the org. */ Fields["STATUS"] = "status"; /** * The snapshot used to create the scratch org. */ Fields["SNAPSHOT"] = "snapshot"; // Should it be on org? Leave it off for now, as it might // be confusing to the consumer what this actually is. // USERNAMES = 'usernames', // Keep separation of concerns. I think these should be on a "user" that belongs to the org. // Org can have a list of user objects that belong to it? Should connection be on user and org.getConnection() // gets the orgs current user for the process? Maybe we just want to keep with the Org only model for // the end of time? // USER_ID = 'userId', // USERNAME = 'username', // PASSWORD = 'password', // USER_PROFILE_NAME = 'userProfileName' })(Fields = Org.Fields || (Org.Fields = {})); })(Org = exports.Org || (exports.Org = {})); //# sourceMappingURL=org.js.map