@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
456 lines • 19 kB
JavaScript
;
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