UNPKG

@salesforce/core

Version:

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

406 lines 17.8 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.SingleRecordQueryErrors = exports.Connection = exports.DNS_ERROR_NAME = exports.SFDX_HTTP_HEADERS = void 0; /* eslint-disable @typescript-eslint/ban-ts-comment */ const node_url_1 = require("node:url"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const jsforce_node_1 = require("@jsforce/jsforce-node"); const tooling_1 = require("@jsforce/jsforce-node/lib/api/tooling"); const myDomainResolver_1 = require("../status/myDomainResolver"); const configAggregator_1 = require("../config/configAggregator"); const logger_1 = require("../logger/logger"); const sfError_1 = require("../sfError"); const sfdc_1 = require("../util/sfdc"); const messages_1 = require("../messages"); const lifecycleEvents_1 = require("../lifecycleEvents"); const orgConfigProperties_1 = require("./orgConfigProperties"); ; const messages = new messages_1.Messages('@salesforce/core', 'connection', new Map([["incorrectAPIVersionError", "Invalid API version %s. Expecting format \"[1-9][0-9].0\", i.e. 42.0"], ["domainNotFoundError", "The org cannot be found"], ["domainNotFoundError.actions", ["Verify that the org still exists,", "If your org is newly created, wait a minute and run your command again,", "If you deployed or updated the org's My Domain, logout from the CLI and authenticate again,", "If you are running in a CI environment with a DNS that blocks external IPs, try setting SFDX_DISABLE_DNS_CHECK=true"]], ["noInstanceUrlError", "Connection has no instanceUrl."], ["noInstanceUrlError.actions", "Make sure the instanceUrl is set in your command or config."], ["noApiVersionsError", "Org failed to respond with its list of API versions. This is usually the result of domain changes like activating MyDomain or Enhanced Domains"], ["noApiVersionsError.actions", "Re-authenticate to the org."]])); const clientId = `sfdx toolbelt:${process.env.SFDX_SET_CLIENT_IDS ?? ''}`; exports.SFDX_HTTP_HEADERS = { 'content-type': 'application/json', 'user-agent': clientId, }; exports.DNS_ERROR_NAME = 'DomainNotFoundError'; /** * Handles connections and requests to Salesforce Orgs. * * ``` * // Uses latest API version * const connection = await Connection.create({ * authInfo: await AuthInfo.create({ username: 'myAdminUsername' }) * }); * connection.query('SELECT Name from Account'); * * // Use different API version * connection.setApiVersion("42.0"); * connection.query('SELECT Name from Account'); * ``` */ class Connection extends jsforce_node_1.Connection { // The following are all initialized in either this constructor or the super constructor, sometimes conditionally... // We want to use 1 logger for this class and the jsForce base classes so override // the jsForce connection.tooling.logger and connection.logger. logger; options; // All connections are tied to a username username; // Save whether we've successfully resolved this connection's instance URL. hasResolved = false; // Save the max API version of this connection's org. maxApiVersion; /** * Constructor * **Do not directly construct instances of this class -- use {@link Connection.create} instead.** * * @param options The options for the class instance. * @ignore */ constructor(options) { super(options.connectionOptions ?? {}); this.options = options; this.username = options.authInfo.getUsername(); } /** * Tooling api reference. */ get tooling() { return super.tooling; } /** * Creates an instance of a Connection. Performs additional async initializations. * * @param options Constructor options. */ static async create(options) { // Get connection options from auth info and create a new jsForce connection const connectionOptions = { version: await getOptionsVersion(options), callOptions: { client: clientId, }, ...options.authInfo.getConnectionOptions(), // this assertion is questionable, but has existed before core7 }; const conn = new this({ ...options, connectionOptions }); await conn.init(); try { // No version passed in or in the config, so load one. if (!connectionOptions.version) { await conn.useLatestApiVersion(); } else { conn.logger.debug(`The org-api-version ${connectionOptions.version} was found from ${options.connectionOptions?.version ? 'passed in options' : 'config'}`); } } catch (err) { const e = err; if (e.name === exports.DNS_ERROR_NAME) { throw e; } conn.logger.debug(`Error trying to load the API version: ${e.name} - ${e.message}`); } conn.logger.debug(`Connection created with apiVersion ${conn.getApiVersion()}`); return conn; } /** * Async initializer. */ async init() { // eslint-disable-next-line no-underscore-dangle this.logger = this.tooling._logger = await logger_1.Logger.child('connection'); } /** * deploy a zipped buffer from the SDRL with REST or SOAP * * @param zipInput data to deploy * @param options JSForce deploy options + a boolean for rest */ async deploy(zipInput, options) { // neither API expects this option const { rest, ...optionsWithoutRest } = options; if (rest) { this.logger.debug('deploy with REST'); await this.refreshAuth(); return this.metadata.deployRest(zipInput, optionsWithoutRest); } else { this.logger.debug('deploy with SOAP'); return this.metadata.deploy(zipInput, optionsWithoutRest); } } /** * Send REST API request with given HTTP request info, with connected session information * and SFDX headers. * * @param request HTTP request object or URL to GET request. * @param options HTTP API request options. */ request(request, options) { const httpRequest = (0, ts_types_1.isString)(request) ? { method: 'GET', url: request } : request; // prevent duplicate headers by lowercasing the keys on the incoming request const lowercasedHeaders = httpRequest.headers ? Object.fromEntries(Object.entries(httpRequest.headers).map(([key, value]) => [key.toLowerCase(), value])) : {}; httpRequest.headers = { ...exports.SFDX_HTTP_HEADERS, ...lowercasedHeaders, }; this.logger.getRawLogger().debug(httpRequest, 'request'); return super.request(httpRequest, options); } /** * The Force API base url for the instance. */ baseUrl() { // essentially the same as pathJoin(super.instanceUrl, 'services', 'data', `v${super.version}`); // eslint-disable-next-line no-underscore-dangle return super._baseUrl(); } /** * Retrieves the highest api version that is supported by the target server instance. */ async retrieveMaxApiVersion() { // Check saved value first, then cache. if ((this.maxApiVersion ??= this.getCachedApiVersion())) { return this.maxApiVersion; } await this.isResolvable(); this.logger.debug(`Fetching API versions supported for org: ${this.getUsername()}`); const versions = await this.request(`${this.instanceUrl}/services/data`); // if the server doesn't return a list of versions, it's possibly a instanceUrl issue where the local file is out of date. if (!Array.isArray(versions)) { this.logger.debug(`server response for retrieveMaxApiVersion: ${versions}`); throw messages.createError('noApiVersionsError'); } this.logger.debug(`response for org versions: ${versions.map((item) => item.version).join(',')}`); this.maxApiVersion = (0, ts_types_1.ensure)((0, kit_1.maxBy)(versions, (version) => version.version)).version; // cache the max API version just fetched await this.options.authInfo.save({ instanceApiVersion: this.maxApiVersion, // This will get messed up if the user changes their local time on their machine. // Not a big deal since it will just get updated sooner/later. instanceApiVersionLastRetrieved: new Date().toLocaleString(), }); return this.maxApiVersion; } /** * Use the latest API version available on `this.instanceUrl`. */ async useLatestApiVersion() { try { this.setApiVersion(await this.retrieveMaxApiVersion()); } catch (err) { const error = err; if (error.name === exports.DNS_ERROR_NAME) { throw error; // throws on DNS connection errors } // Don't fail if we can't use the latest, just use the default this.logger.warn('Failed to set the latest API version:', error); } } /** * Verify that instance has a reachable DNS entry, otherwise will throw error */ async isResolvable() { if (this.hasResolved) { return this.hasResolved; } if (!this.options.connectionOptions?.instanceUrl) { throw messages.createError('noInstanceUrlError'); } const resolver = await myDomainResolver_1.MyDomainResolver.create({ url: new node_url_1.URL(this.options.connectionOptions.instanceUrl), }); try { await resolver.resolve(); this.hasResolved = true; return true; } catch (e) { throw messages.createError('domainNotFoundError', [], [], e); } } /** * Get the API version used for all connection requests. */ getApiVersion() { return this.version; } /** * Set the API version for all connection requests. * * **Throws** *{@link SfError}{ name: 'IncorrectAPIVersionError' }* Incorrect API version. * * @param version The API version. */ setApiVersion(version) { if (!(0, sfdc_1.validateApiVersion)(version)) { throw messages.createError('incorrectAPIVersionError', [version]); } this.version = version; } /** * Getter for AuthInfo. */ getAuthInfo() { return this.options.authInfo; } /** * Getter for the AuthInfo fields. */ getAuthInfoFields() { // If the StateAggregator.orgs.remove is called, the AuthFields are no longer accessible. return this.options.authInfo.getFields() || {}; } /** * Getter for the auth fields. */ getConnectionOptions() { return this.options.authInfo.getConnectionOptions(); } /** * Getter for the username of the Salesforce Org. */ getUsername() { return this.username; } /** * Returns true if this connection is using access token auth. */ isUsingAccessToken() { return this.options.authInfo.isUsingAccessToken(); } /** * Normalize a Salesforce url to include a instance information. * * @param url Partial url. */ normalizeUrl(url) { // eslint-disable-next-line no-underscore-dangle return this._normalizeUrl(url); } /** * Executes a query and auto-fetches (i.e., "queryMore") all results. This is especially * useful with large query result sizes, such as over 2000 records. The default maximum * fetch size is 10,000 records. Modify this via the options argument. * * @param soql The SOQL string. * @param queryOptions The query options. NOTE: the autoFetch option will always be true. */ async autoFetchQuery(soql, queryOptions = { tooling: false }) { const config = await configAggregator_1.ConfigAggregator.create(); // take the limit from the calling function, then the config, then default 10,000 const maxFetch = (config.getInfo(orgConfigProperties_1.OrgConfigProperties.ORG_MAX_QUERY_LIMIT).value || queryOptions.maxFetch) ?? 10_000; const { tooling, ...queryOptionsWithoutTooling } = queryOptions; const options = Object.assign(queryOptionsWithoutTooling, { autoFetch: true, maxFetch, }); const query = tooling ? await this.tooling.query(soql, options) : await this.query(soql, options); if (query.records.length && query.totalSize > query.records.length) { void lifecycleEvents_1.Lifecycle.getInstance().emitWarning(`The query result is missing ${query.totalSize - query.records.length} records due to a ${maxFetch} record limit. Increase the number of records returned by setting the config value "maxQueryLimit" or the environment variable "SF_ORG_MAX_QUERY_LIMIT" to ${query.totalSize} or greater than ${maxFetch}.`); } return query; } /** * Executes a query using either standard REST or Tooling API, returning a single record. * Will throw if either zero records are found OR multiple records are found. * * @param soql The SOQL string. * @param options The query options. */ async singleRecordQuery(soql, options = { choiceField: 'Name', }) { const result = options.tooling ? await this.tooling.query(soql) : await this.query(soql); if (result.totalSize === 0) { throw new sfError_1.SfError(`No record found for ${soql}`, exports.SingleRecordQueryErrors.NoRecords); } if (result.totalSize > 1) { throw new sfError_1.SfError(options.returnChoicesOnMultiple ? // eslint-disable-next-line @typescript-eslint/no-unsafe-return `Multiple records found. ${result.records.map((item) => item[options.choiceField]).join(',')}` : 'The query returned more than 1 record', exports.SingleRecordQueryErrors.MultipleRecords); } return result.records[0]; } /** * 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.baseUrl(), method: 'HEAD', }; await this.request(requestInfo); } getCachedApiVersion() { // Exit early if the API version cache is disabled. if (kit_1.env.getBoolean('SFDX_IGNORE_API_VERSION_CACHE', false)) { this.logger.debug('Using latest API version since SFDX_IGNORE_API_VERSION_CACHE = true'); return; } // Get API version cache data const authFileFields = this.options.authInfo.getFields(); const lastCheckedDateString = authFileFields.instanceApiVersionLastRetrieved; const version = authFileFields.instanceApiVersion; let lastChecked; try { if (lastCheckedDateString && (0, ts_types_1.isString)(lastCheckedDateString)) { lastChecked = Date.parse(lastCheckedDateString); } } catch (e) { /* Do nothing, it will just request the version again */ } // Check if the cache has expired if (lastChecked) { const now = new Date(); const has24HoursPastSinceLastCheck = now.getTime() - lastChecked > kit_1.Duration.hours(24).milliseconds; this.logger.debug(`API version cache last checked on ${lastCheckedDateString} (now is ${now.toLocaleString()})`); if (!has24HoursPastSinceLastCheck && version) { // return cached API version this.logger.debug(`Using cached API version: ${version}`); return version; } else { this.logger.debug('API version cache expired. Re-fetching latest.'); } } } } exports.Connection = Connection; exports.SingleRecordQueryErrors = { NoRecords: 'SingleRecordQuery_NoRecords', MultipleRecords: 'SingleRecordQuery_MultipleRecords', }; const getOptionsVersion = async (options) => { if (options.connectionOptions) { return options.connectionOptions.version; } // Set the API version obtained from the config aggregator. const configAggregator = options.configAggregator ?? (await configAggregator_1.ConfigAggregator.create()); return (0, ts_types_1.asString)(configAggregator.getInfo('org-api-version').value); }; // jsforce does some interesting proxy loading on lib classes. // Setting this in the Connection.tooling getter will not work, it // must be set on the prototype. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore tooling_1.Tooling.prototype.autoFetchQuery = Connection.prototype.autoFetchQuery; // eslint-disable-line @typescript-eslint/unbound-method //# sourceMappingURL=connection.js.map