UNPKG

@salesforce/core

Version:

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

456 lines 19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SingleRecordQueryErrors = exports.Connection = exports.DNS_ERROR_NAME = exports.SFDX_HTTP_HEADERS = void 0; /* * 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 */ const url_1 = require("url"); const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const jsforce_1 = require("jsforce"); // no types for Transport // @ts-ignore const Transport = require("jsforce/lib/transport"); const myDomainResolver_1 = require("./status/myDomainResolver"); const configAggregator_1 = require("./config/configAggregator"); const logger_1 = require("./logger"); const sfdxError_1 = require("./sfdxError"); const sfdc_1 = require("./util/sfdc"); const lifecycleEvents_1 = require("./lifecycleEvents"); /** * The 'async' in our request override replaces the jsforce promise with the node promise, then returns it back to * jsforce which expects .thenCall. Add .thenCall to the node promise to prevent breakage. */ // @ts-ignore Promise.prototype.thenCall = jsforce_1.Promise.prototype.thenCall; 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 = 'Domain Not Found'; /** * 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_1.Connection { /** * 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 || {}); // eslint-disable-next-line @typescript-eslint/unbound-method this.tooling.autoFetchQuery = Connection.prototype.autoFetchQuery; this.options = options; } /** * Creates an instance of a Connection. Performs additional async initializations. * * @param options Constructor options. */ static async create(options) { var _a, _b; const baseOptions = { version: (_a = options.connectionOptions) === null || _a === void 0 ? void 0 : _a.version, callOptions: { client: clientId, }, }; if (!baseOptions.version) { // Set the API version obtained from the config aggregator. const configAggregator = options.configAggregator || (await configAggregator_1.ConfigAggregator.create()); baseOptions.version = ts_types_1.asString(configAggregator.getInfo('apiVersion').value); } // Get connection options from auth info and create a new jsForce connection options.connectionOptions = Object.assign(baseOptions, options.authInfo.getConnectionOptions()); const conn = new this(options); await conn.init(); try { // No version passed in or in the config, so load one. if (!baseOptions.version) { const cachedVersion = await conn.loadInstanceApiVersion(); if (cachedVersion) { conn.setApiVersion(cachedVersion); } } else { conn.logger.debug(`The apiVersion ${baseOptions.version} was found from ${((_b = options.connectionOptions) === null || _b === void 0 ? void 0 : _b.version) ? 'passed in options' : 'config'}`); } } catch (e) { 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(`Using 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'); } /** * TODO: This should be moved into JSForce V2 once ready * this is only a temporary solution to support both REST and SOAP APIs * * 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 * @param callback */ async deploy(zipInput, options, callback) { const rest = options.rest; // neither API expects this option delete options.rest; if (rest) { this.logger.debug('deploy with REST'); // do a quick auth refresh because the raw transport used doesn't handle expired AccessTokens await this.refreshAuth(); const headers = { Authorization: this && `OAuth ${this.accessToken}`, clientId: this.oauth2 && this.oauth2.clientId, 'Sforce-Call-Options': 'client=sfdx-core', }; const url = `${this.baseUrl()}/metadata/deployRequest`; const request = Transport.prototype._getHttpRequestModule(); return new Promise((resolve, reject) => { const req = request.post(url, { headers }, (err, httpResponse, body) => { let res; try { res = JSON.parse(body); } catch (e) { reject(sfdxError_1.SfdxError.wrap(body)); } resolve(res); }); const form = req.form(); // Add the zip file form.append('file', zipInput, { contentType: 'application/zip', filename: 'package.xml', }); // Add the deploy options form.append('entity_content', JSON.stringify({ deployOptions: options }), { contentType: 'application/json', }); }); } else { this.logger.debug('deploy with SOAP'); return this.metadata.deploy(zipInput, options, callback); } } /** * 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. */ async request(request, options) { const requestInfo = ts_types_1.isString(request) ? { method: 'GET', url: request } : request; requestInfo.headers = Object.assign({}, exports.SFDX_HTTP_HEADERS, requestInfo.headers); this.logger.debug(`request: ${JSON.stringify(requestInfo)}`); return super.request(requestInfo, options); } /** * Send REST API request with given HTTP request info, with connected session information * and SFDX headers. This method returns a raw http response which includes a response body and statusCode. * * @param request HTTP request object or URL to GET request. */ async requestRaw(request) { await this.refreshAuth(); const headers = this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {}; kit_1.merge(headers, exports.SFDX_HTTP_HEADERS, request.headers); return this._transport.httpRequest({ method: request.method, url: request.url, headers, body: request.body, }); } /** * The Force API base url for the instance. */ baseUrl() { // essentially the same as pathJoin(super.instanceUrl, 'services', 'data', `v${super.version}`); return super._baseUrl(); } /** * TODO: This should be moved into JSForce V2 once ready * this is only a temporary solution to support both REST and SOAP APIs * * Will deploy a recently validated deploy request * * @param options.id = the deploy ID that's been validated already from a previous checkOnly deploy request * @param options.rest = a boolean whether or not to use the REST API */ async deployRecentValidation(options) { const rest = options.rest; delete options.rest; if (rest) { const url = `${this.baseUrl()}/metadata/deployRequest`; const messageBody = JSON.stringify({ validatedDeployRequestId: options.id, }); const requestInfo = { method: 'POST', url, body: messageBody, }; const requestOptions = { headers: 'json' }; return this.request(requestInfo, requestOptions); } else { // the _invoke is private in jsforce, we can call the SOAP deployRecentValidation like this // @ts-ignore return this.metadata['_invoke']('deployRecentValidation', { validationId: options.id, }); } } /** * Retrieves the highest api version that is supported by the target server instance. */ async retrieveMaxApiVersion() { await this.isResolvable(); const versions = await this.request(`${this.instanceUrl}/services/data`); this.logger.debug(`response for org versions: ${versions.map((item) => item.version).join(',')}`); const max = ts_types_1.ensure(kit_1.maxBy(versions, (version) => version.version)); return max.version; } /** * Use the latest API version available on `this.instanceUrl`. */ async useLatestApiVersion() { try { this.setApiVersion(await this.retrieveMaxApiVersion()); } catch (err) { if (err.name === exports.DNS_ERROR_NAME) { throw err; // 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:', err); } } /** * Verify that instance has a reachable DNS entry, otherwise will throw error */ async isResolvable() { var _a; if (!((_a = this.options.connectionOptions) === null || _a === void 0 ? void 0 : _a.instanceUrl)) { throw new sfdxError_1.SfdxError('Connection has no instanceUrl', 'NoInstanceUrl', [ 'Make sure the instanceUrl is set in your command or config', ]); } const resolver = await myDomainResolver_1.MyDomainResolver.create({ url: new url_1.URL(this.options.connectionOptions.instanceUrl), }); try { await resolver.resolve(); return true; } catch (e) { throw new sfdxError_1.SfdxError('The org cannot be found', exports.DNS_ERROR_NAME, [ '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', ]); } } /** * Get the API version used for all connection requests. */ getApiVersion() { return this.version; } /** * Set the API version for all connection requests. * * **Throws** *{@link SfdxError}{ name: 'IncorrectAPIVersion' }* Incorrect API version. * * @param version The API version. */ setApiVersion(version) { if (!sfdc_1.sfdc.validateApiVersion(version)) { throw new sfdxError_1.SfdxError(`Invalid API version ${version}. Expecting format "[1-9][0-9].0", i.e. 42.0`, 'IncorrectAPIVersion'); } this.version = version; } /** * Getter for the AuthInfo. */ getAuthInfoFields() { 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.getAuthInfoFields().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) { 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 executeOptions The query options. NOTE: the autoFetch option will always be true. */ async autoFetchQuery(soql, executeOptions = {}) { 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('maxQueryLimit').value || executeOptions.maxFetch || 10000; const options = Object.assign(executeOptions, { autoFetch: true, maxFetch, }); const records = []; return new Promise((resolve, reject) => { const query = this.query(soql, options) .on('record', (rec) => records.push(rec)) .on('error', (err) => reject(err)) .on('end', () => { const totalSize = ts_types_1.getNumber(query, 'totalSize', 0); // records.legnth can be 0 in count() query, but totalSize is bigger. if (records.length && totalSize > records.length) { void lifecycleEvents_1.Lifecycle.getInstance().emitWarning(`The query result is missing ${totalSize - 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 "SFDX_MAX_QUERY_LIMIT" to ${totalSize} or greater than ${maxFetch}.`); } resolve({ done: true, totalSize, records, }); }); }); } /** * 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 sfdxError_1.SfdxError(`No record found for ${soql}`, exports.SingleRecordQueryErrors.NoRecords); } if (result.totalSize > 1) { throw new sfdxError_1.SfdxError(options.returnChoicesOnMultiple ? `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 get request on the baseUrl to force an auth refresh * Useful for the raw methods (request, requestRaw) that use the accessToken directly and don't handle refreshes */ async refreshAuth() { this.logger.debug('Refreshing auth for org.'); const requestInfo = { url: this.baseUrl(), method: 'GET', }; await this.request(requestInfo); } async loadInstanceApiVersion() { const authFileFields = this.options.authInfo.getFields(); const lastCheckedDateString = authFileFields.instanceApiVersionLastRetrieved; let version = ts_types_1.getString(authFileFields, 'instanceApiVersion'); let lastChecked; try { if (lastCheckedDateString && ts_types_1.isString(lastCheckedDateString)) { lastChecked = Date.parse(lastCheckedDateString); } } catch (e) { /* Do nothing, it will just request the version again */ } // Grab the latest api version from the server and cache it in the auth file const useLatest = async () => { // verifies DNS await this.useLatestApiVersion(); version = this.getApiVersion(); await this.options.authInfo.save({ instanceApiVersion: version, // 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(), }); }; const ignoreCache = kit_1.env.getBoolean('SFDX_IGNORE_API_VERSION_CACHE', false); if (lastChecked && !ignoreCache) { const now = new Date(); const has24HoursPastSinceLastCheck = now.getTime() - lastChecked > kit_1.Duration.hours(24).milliseconds; this.logger.debug(`Last checked on ${lastCheckedDateString} (now is ${now.toLocaleString()}) - ${has24HoursPastSinceLastCheck ? '' : 'not '}getting latest`); if (has24HoursPastSinceLastCheck) { await useLatest(); } } else { this.logger.debug(`Using the latest because lastChecked=${lastChecked} and SFDX_IGNORE_API_VERSION_CACHE=${ignoreCache}`); // No version found in the file (we never checked before) // so get the latest. await useLatest(); } this.logger.debug(`Loaded latest apiVersion ${version}`); return version; } } exports.Connection = Connection; exports.SingleRecordQueryErrors = { NoRecords: 'SingleRecordQuery_NoRecords', MultipleRecords: 'SingleRecordQuery_MultipleRecords', }; //# sourceMappingURL=connection.js.map