Skip to content

Commit

Permalink
feat: cache api version on auth info
Browse files Browse the repository at this point in the history
  • Loading branch information
amphro committed Feb 18, 2021
1 parent bac4373 commit cb21cf0
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 39 deletions.
2 changes: 2 additions & 0 deletions src/authInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface AuthFields {
createdOrgInstance?: string;
devHubUsername?: string;
instanceUrl?: string;
instanceApiVersion?: string;
instanceApiVersionLastRetrieved?: number;
isDevHub?: boolean;
loginUrl?: string;
orgId?: string;
Expand Down
70 changes: 60 additions & 10 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { URL } from 'url';
import { maxBy, merge } from '@salesforce/kit';
import { asString, ensure, getNumber, isString, JsonCollection, JsonMap, Optional } from '@salesforce/ts-types';
import { Duration, maxBy, merge, env } from '@salesforce/kit';
import { asString, ensure, getNumber, getString, isString, JsonCollection, JsonMap, Optional } from '@salesforce/ts-types';
import {
Connection as JSForceConnection,
ConnectionOptions,
Expand Down Expand Up @@ -107,29 +107,45 @@ export class Connection extends JSForceConnection {
this: new (options: Connection.Options) => Connection,
options: Connection.Options
): Promise<Connection> {
const configAggregator = options.configAggregator || (await ConfigAggregator.create());
const versionFromConfig = asString(configAggregator.getInfo('apiVersion').value);
const baseOptions: ConnectionOptions = {
// Set the API version obtained from the config aggregator.
// Will use jsforce default if undefined.
version: versionFromConfig,
version: options.connectionOptions?.version,
callOptions: {
client: clientId,
},
};

if (!baseOptions.version) {
// Set the API version obtained from the config aggregator.
const configAggregator = options.configAggregator || (await ConfigAggregator.create());
baseOptions.version = 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();
// verifies that subsequent requests to org will not hit DNS errors

if (!versionFromConfig) {
await conn.useLatestApiVersion();
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 ${options.connectionOptions?.version ? 'passed in options' : 'config'}`);
}
} catch(e) {
if (e.name === 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.
*/
Expand Down Expand Up @@ -361,6 +377,40 @@ export class Connection extends JSForceConnection {
}
return result.records[0];
}

private async loadInstanceApiVersion(): Promise<Optional<string | null>> {
const authFileFields = this.options.authInfo.getFields();
const lastChecked = authFileFields.instanceApiVersionLastRetrieved;
let version = getString(authFileFields, 'instanceApiVersion');

// 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();
this.options.authInfo.save({
instanceApiVersion: version,
instanceApiVersionLastRetrieved: Date.now(),
});
};

const ignoreCache = env.getBoolean('SFDX_IGNORE_API_VERSION_CACHE', false);
if (lastChecked && !ignoreCache) {
const now = Date.now();
const has24HoursPastSinceLastCheck = now - lastChecked > Duration.hours(24).milliseconds;
this.logger.debug(`Last checked on ${lastChecked} (now is ${now}) - ${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;
}
}

export const SingleRecordQueryErrors = {
Expand Down
94 changes: 65 additions & 29 deletions test/unit/connectionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { MyDomainResolver } from '../../src/status/myDomainResolver';
import { ConfigAggregator, ConfigInfo } from '../../src/config/configAggregator';
import { Connection, SFDX_HTTP_HEADERS, DNS_ERROR_NAME, SingleRecordQueryErrors } from '../../src/connection';
import { testSetup, shouldThrow } from '../../src/testSetup';
import { fromStub, stubInterface, StubbedType } from '@salesforce/ts-sinon';
import { Duration } from '@salesforce/kit';

// Setup the test environment.
const $$ = testSetup();
Expand All @@ -24,18 +26,8 @@ describe('Connection', () => {
let requestMock: sinon.SinonStub;
let initializeStub: sinon.SinonStub;

const testAuthInfo = {
isOauth: () => true,
getConnectionOptions: () => testConnectionOptions,
};

const testAuthInfoWithDomain = {
...testAuthInfo,
getConnectionOptions: () => ({
...testConnectionOptions,
instanceUrl: 'https://connectionTest/instanceUrl',
}),
};
let testAuthInfo: StubbedType<AuthInfo>;
let testAuthInfoWithDomain: StubbedType<AuthInfo>;

beforeEach(() => {
$$.SANDBOXES.CONNECTION.restore();
Expand All @@ -46,21 +38,37 @@ describe('Connection', () => {
requestMock = $$.SANDBOX.stub(jsforce.Connection.prototype, 'request')
.onFirstCall()
.resolves([{ version: '42.0' }]);

// Create proxied instances of AuthInfo
testAuthInfo = stubInterface<AuthInfo>($$.SANDBOX, {
isOauth: () => true,
getFields: () => ({}),
getConnectionOptions: () => testConnectionOptions,
});

testAuthInfoWithDomain = stubInterface<AuthInfo>($$.SANDBOX, {
isOauth: () => true,
getFields: () => ({}),
getConnectionOptions: () => ({
...testConnectionOptions,
instanceUrl: 'https://connectionTest/instanceUrl',
}),
});
});

it('create() should throw on DNS errors', async () => {
$$.SANDBOX.restore();
$$.SANDBOX.stub(MyDomainResolver.prototype, 'resolve').rejects({ name: DNS_ERROR_NAME });

try {
await shouldThrow(Connection.create({ authInfo: testAuthInfoWithDomain as AuthInfo }));
await shouldThrow(Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) }));
} catch (e) {
expect(e).to.have.property('name', DNS_ERROR_NAME);
}
});

it('create() should create a connection using AuthInfo and SFDX options', async () => {
const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });

expect(conn.request).to.exist;
expect(conn['oauth2']).to.be.an('object');
Expand All @@ -71,12 +79,40 @@ describe('Connection', () => {
});

it('create() should create a connection with the latest API version', async () => {
const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
expect(conn.getApiVersion()).to.equal('42.0');
});

it('create() should create a connection with the provided API version', async () => {
const conn = await Connection.create({
authInfo: fromStub(testAuthInfo),
connectionOptions: { version: '50.0' }
});
expect(conn.getApiVersion()).to.equal('50.0');
});

it('create() should create a connection with the cached API version', async () => {
testAuthInfo.getFields.returns({
instanceApiVersionLastRetrieved: Date.now() - Duration.hours(10).milliseconds,
instanceApiVersion: '51.0'
});
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
expect(conn.getApiVersion()).to.equal('51.0');
});

it('create() should create a connection with the cached API version updated with latest', async () => {
testAuthInfo.getFields.returns({
instanceApiVersionLastRetrieved: 123,
instanceApiVersion: '40.0'
});

const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
expect(conn.getApiVersion()).to.equal('42.0');
expect(testAuthInfo.save.called).to.be.true;
});

it('setApiVersion() should throw with invalid version', async () => {
const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });

try {
conn.setApiVersion('v23.0');
Expand All @@ -97,7 +133,7 @@ describe('Connection', () => {
};

const conn = await Connection.create({
authInfo: testAuthInfoWithDomain as AuthInfo,
authInfo: fromStub(testAuthInfoWithDomain),
});
// Test passing a string to conn.request()
const response1 = await conn.request(testUrl);
Expand All @@ -113,7 +149,7 @@ describe('Connection', () => {
const testUrl = 'connectionTest/request/url/describe';

const conn = await Connection.create({
authInfo: testAuthInfoWithDomain as AuthInfo,
authInfo: fromStub(testAuthInfoWithDomain),
});

// Test passing a RequestInfo object and options to conn.request()
Expand All @@ -134,7 +170,7 @@ describe('Connection', () => {
requestMock.onSecondCall().returns(Promise.resolve(testResponse));

const conn = await Connection.create({
authInfo: testAuthInfoWithDomain as AuthInfo,
authInfo: fromStub(testAuthInfoWithDomain),
});

const testUrl = '/services/data/v42.0/tooling/sobjects';
Expand All @@ -157,7 +193,7 @@ describe('Connection', () => {
const querySpy = $$.SANDBOX.spy(jsforce.Connection.prototype, 'query');
const soql = 'TEST_SOQL';

const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
const queryResults = await conn.autoFetchQuery(soql);

expect(queryResults).to.deep.equal({
Expand All @@ -175,7 +211,7 @@ describe('Connection', () => {
requestMock.onSecondCall().returns(Promise.resolve(queryResponse));
const soql = 'TEST_SOQL';

const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
const toolingQuerySpy = $$.SANDBOX.spy(conn.tooling, 'query');
const queryResults = await conn.tooling.autoFetchQuery(soql);

Expand All @@ -195,7 +231,7 @@ describe('Connection', () => {
const queryResponse = { totalSize: 50000, done: true, records };
requestMock.returns(Promise.resolve(queryResponse));

const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
const toolingQuerySpy = $$.SANDBOX.spy(conn.tooling, 'query');
$$.SANDBOX.stub(ConfigAggregator.prototype, 'getInfo').returns({ value: 50000 } as ConfigInfo);
await conn.tooling.autoFetchQuery(soql);
Expand All @@ -212,7 +248,7 @@ describe('Connection', () => {
const queryResponse = { totalSize: 5, done: true, records };
requestMock.returns(Promise.resolve(queryResponse));

const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfo) });
const toolingQuerySpy = $$.SANDBOX.spy(conn.tooling, 'query');
$$.SANDBOX.stub(ConfigAggregator.prototype, 'getInfo').returns({ value: 3 } as ConfigInfo);
await conn.tooling.autoFetchQuery(soql);
Expand All @@ -226,7 +262,7 @@ describe('Connection', () => {
const errorMsg = 'QueryFailed';
requestMock.onSecondCall().throws(new Error(errorMsg));
const conn = await Connection.create({
authInfo: testAuthInfoWithDomain as AuthInfo,
authInfo: fromStub(testAuthInfoWithDomain),
});

try {
Expand All @@ -245,7 +281,7 @@ describe('Connection', () => {
const soql = 'TEST_SOQL';
requestMock.onSecondCall().resolves({ totalSize: 1, records: [mockSingleRecord] });

const conn = await Connection.create({ authInfo: testAuthInfoWithDomain as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) });

const queryResult = await conn.singleRecordQuery(soql);
expect(queryResult).to.deep.equal({
Expand All @@ -255,7 +291,7 @@ describe('Connection', () => {

it('singleRecordQuery throws on no-records', async () => {
requestMock.returns(Promise.resolve({ totalSize: 0, records: [] }));
const conn = await Connection.create({ authInfo: testAuthInfoWithDomain as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) });

try {
await conn.singleRecordQuery('TEST_SOQL');
Expand All @@ -267,7 +303,7 @@ describe('Connection', () => {

it('singleRecordQuery throws on multiple records', async () => {
requestMock.returns(Promise.resolve({ totalSize: 2, records: [{ id: 1 }, { id: 2 }] }));
const conn = await Connection.create({ authInfo: testAuthInfoWithDomain as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) });

try {
await conn.singleRecordQuery('TEST_SOQL');
Expand All @@ -279,7 +315,7 @@ describe('Connection', () => {

it('singleRecordQuery throws on multiple records with options', async () => {
requestMock.returns(Promise.resolve({ totalSize: 2, records: [{ id: 1 }, { id: 2 }] }));
const conn = await Connection.create({ authInfo: testAuthInfoWithDomain as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) });

try {
await conn.singleRecordQuery('TEST_SOQL', { returnChoicesOnMultiple: true, choiceField: 'id' });
Expand All @@ -298,7 +334,7 @@ describe('Connection', () => {
requestMock.returns(Promise.resolve({ totalSize: 1, records: [mockSingleRecord] }));
const soql = 'TEST_SOQL';

const conn = await Connection.create({ authInfo: testAuthInfoWithDomain as AuthInfo });
const conn = await Connection.create({ authInfo: fromStub(testAuthInfoWithDomain) });
const toolingQuerySpy = $$.SANDBOX.spy(conn.tooling, 'query');
const queryResults = await conn.singleRecordQuery(soql, { tooling: true });
expect(queryResults).to.deep.equal({
Expand Down

0 comments on commit cb21cf0

Please sign in to comment.