@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
406 lines • 17.8 kB
JavaScript
"use strict";
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
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