diff --git a/package.json b/package.json index b0d95409b6..67bf96447e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "postcompile": "tsc -p test; tsc -p typedocExamples", "prepack": "sf-build", "pretest": "sf-compile-test", - "test": "sf-test --require ts-node/register" + "test": "sf-test" }, "keywords": [ "force", diff --git a/src/connection.ts b/src/connection.ts index 9268ac5074..858d6fc8b9 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -5,6 +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 { AsyncResult, DeployOptions, DeployResultLocator } from 'jsforce/api/metadata'; +import { Callback } from 'jsforce/connection'; import { Duration, maxBy, merge, env } from '@salesforce/kit'; import { asString, @@ -26,9 +28,12 @@ import { RequestInfo, Tooling as JSForceTooling, } from 'jsforce'; +// no types for Transport +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +import * as Transport from 'jsforce/lib/transport'; import { AuthFields, AuthInfo } from './authInfo'; import { MyDomainResolver } from './status/myDomainResolver'; - import { ConfigAggregator } from './config/configAggregator'; import { Logger } from './logger'; import { SfdxError } from './sfdxError'; @@ -49,6 +54,7 @@ export const SFDX_HTTP_HEADERS = { }; export const DNS_ERROR_NAME = 'Domain Not Found'; +export type DeployOptionsWithRest = DeployOptions & { rest?: boolean }; // This interface is so we can add the autoFetchQuery method to both the Connection // and Tooling classes and get nice typing info for it within editors. JSForce is @@ -108,6 +114,7 @@ export class Connection extends JSForceConnection { this.options = options; } + /** * Creates an instance of a Connection. Performs additional async initializations. * @@ -168,6 +175,63 @@ export class Connection extends JSForceConnection { this.logger = this.tooling._logger = await 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 + */ + public async deploy( + zipInput: Buffer, + options: DeployOptionsWithRest, + callback?: Callback + ): Promise> { + const rest = options.rest; + // neither API expects this option + delete options.rest; + if (rest) { + this.logger.debug('deploy with REST'); + 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: Error, httpResponse: { statusCode: number }, body: string) => { + let res; + try { + res = JSON.parse(body); + } catch (e) { + reject(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. diff --git a/src/keyChainImpl.ts b/src/keyChainImpl.ts index 232f415eb4..f162698dc9 100644 --- a/src/keyChainImpl.ts +++ b/src/keyChainImpl.ts @@ -165,7 +165,7 @@ export class KeychainAccess implements PasswordStore { } // eslint-disable-next-line @typescript-eslint/no-misused-promises - credManager.on('close', async (code) => { + credManager.on('close', async (code: number) => { try { return await this.osImpl.onGetCommandClose(code, stdout, stderr, opts, fn); } catch (e) { diff --git a/test/unit/connectionTest.ts b/test/unit/connectionTest.ts index f4d46ad90e..c95b3f6335 100644 --- a/test/unit/connectionTest.ts +++ b/test/unit/connectionTest.ts @@ -4,12 +4,16 @@ * 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 */ -import { get, JsonMap } from '@salesforce/ts-types'; +import { get, getObject, JsonMap } from '@salesforce/ts-types'; import { assert, expect } from 'chai'; import * as jsforce from 'jsforce'; import { fromStub, stubInterface, StubbedType } from '@salesforce/ts-sinon'; import { Duration } from '@salesforce/kit'; +// no types for Transport +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +import * as Transport from 'jsforce/lib/transport'; import { AuthInfo } from '../../src/authInfo'; import { MyDomainResolver } from '../../src/status/myDomainResolver'; import { ConfigAggregator, ConfigInfo } from '../../src/config/configAggregator'; @@ -186,6 +190,44 @@ describe('Connection', () => { expect(response).to.deep.equal(testResponse); }); + describe('deploy', () => { + it('deploy() will work with REST', async () => { + const conn = await Connection.create({ + authInfo: fromStub(testAuthInfoWithDomain), + }); + + const req = Transport.prototype._getHttpRequestModule(); + const postStub = $$.SANDBOX.stub(req, 'post').yields(null, null, '{"done": true}', { + form: () => { + return { append: () => {} }; + }, + }); + + await conn.deploy(new Buffer('test data'), { rest: true }, () => {}); + + const headers = getObject<{ Authorization: string; 'Sforce-Call-Options': string }>( + postStub.args[0][1], + 'headers', + { Authorization: '', 'Sforce-Call-Options': '' } + ); + + expect(postStub.callCount).to.equal(1); + expect(postStub.args[0][0]).to.equal('/services/data/v42.0/metadata/deployRequest'); + expect(headers.Authorization).to.include('OAuth'); + expect(headers['Sforce-Call-Options']).to.equal('client=sfdx-core'); + }); + + it('deploy() will work with SOAP', async () => { + const conn = await Connection.create({ + authInfo: fromStub(testAuthInfoWithDomain), + }); + const soapDeployStub = $$.SANDBOX.stub(conn.metadata, 'deploy').resolves(); + + await conn.deploy(new Buffer('test data'), { rest: false }, () => {}); + expect(soapDeployStub.callCount).to.equal(1); + }); + }); + it('autoFetchQuery() should call this.query with proper args', async () => { const records = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }]; const queryResponse = { totalSize: records.length, done: true, records };