Skip to content

Commit 594bd27

Browse files
chore: move validation left, standard headers, util methods
1 parent 16c6347 commit 594bd27

File tree

4 files changed

+91
-51
lines changed

4 files changed

+91
-51
lines changed

messages/rest.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,26 @@ Make an authenticated HTTP request to Salesforce REST API and print the response
1010

1111
- Get the response in XML format by specifying the "Accept" HTTP header:
1212

13-
<%= config.bin %> <%= command.id %> 'services/data/v56.0/limits' --target-org my-org --header 'Accept: application/xml',
13+
<%= config.bin %> <%= command.id %> 'services/data/v56.0/limits' --target-org my-org --header 'Accept: application/xml'
14+
15+
- POST to create an Account object
16+
17+
<%= config.bin %> <%= command.id %> '/services/data/v46.0/sobjects/account' --body "{\"Name\" : \"Account from REST API\",\"ShippingCity\" : \"Boise\"}" --method POST
18+
19+
- or with a file 'info.json' containing
20+
21+
```json
22+
{
23+
"Name": "Demo",
24+
"ShippingCity": "Boise"
25+
}
26+
```
27+
28+
<%= config.bin %> <%= command.id %> '/services/data/v46.0/sobjects/account' --body info.json --method POST
29+
30+
- Update object
31+
32+
<%= config.bin %> <%= command.id %> '/services/data/v46.0/sobjects/account/<Account ID>' --body "{\"BillingCity\": \"San Francisco\"}" --method PATCH
1433

1534
# flags.include.summary
1635

src/commands/api/request/rest.ts

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import { EOL } from 'node:os';
8-
import { createWriteStream } from 'node:fs';
9-
import got, { Headers } from 'got';
7+
import { createWriteStream, readFileSync, existsSync } from 'node:fs';
8+
import { join } from 'node:path';
9+
import got from 'got';
1010
import type { AnyJson } from '@salesforce/ts-types';
1111
import { ProxyAgent } from 'proxy-agent';
1212
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
13-
import { Messages, Org, SfError } from '@salesforce/core';
13+
import { Messages, Org, SFDX_HTTP_HEADERS, SfError } from '@salesforce/core';
1414
import { Args } from '@oclif/core';
1515
import ansis from 'ansis';
16+
import { getHeaders } from '../../../shared/methods.js';
1617

1718
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1819
const messages = Messages.loadMessages('@salesforce/plugin-api', 'rest');
@@ -23,12 +24,7 @@ export class Rest extends SfCommand<void> {
2324
public static state = 'beta';
2425
public static enableJsonFlag = false;
2526
public static readonly flags = {
26-
// TODO: getting a false positive from this eslint rule.
27-
// summary is already set in the org flag.
28-
// eslint-disable-next-line sf-plugin/flag-summary
29-
'target-org': Flags.requiredOrg({
30-
helpValue: 'username',
31-
}),
27+
'target-org': Flags.requiredOrg(),
3228
include: Flags.boolean({
3329
char: 'i',
3430
summary: messages.getMessage('flags.include.summary'),
@@ -67,45 +63,38 @@ export class Rest extends SfCommand<void> {
6763
}),
6864
};
6965

70-
private static getHeaders(keyValPair: string[]): Headers {
71-
const headers: { [key: string]: string } = {};
72-
73-
for (const header of keyValPair) {
74-
const [key, ...rest] = header.split(':');
75-
const value = rest.join(':').trim();
76-
if (!key || !value) {
77-
throw new SfError(`Failed to parse HTTP header: "${header}".`, 'Failed To Parse HTTP Header', [
78-
'Make sure the header is in a "key:value" format, e.g. "Accept: application/json"',
79-
]);
80-
}
81-
headers[key] = value;
82-
}
83-
84-
return headers;
85-
}
86-
8766
public async run(): Promise<void> {
8867
const { flags, args } = await this.parse(Rest);
8968

9069
const org = flags['target-org'];
9170
const streamFile = flags['stream-to-file'];
71+
const headers = flags.header ? getHeaders(flags.header) : {};
72+
73+
const url = new URL(`${org.getField<string>(Org.Fields.INSTANCE_URL)}/${args.endpoint}`);
74+
const body =
75+
flags.method === 'GET'
76+
? undefined
77+
: // if they've passed in a file name, check and read it
78+
existsSync(join(process.cwd(), flags.body ?? ''))
79+
? readFileSync(join(process.cwd(), flags.body ?? ''))
80+
: // otherwise it's a stdin, and we use it directly
81+
flags.body;
9282

9383
await org.refreshAuth();
9484

95-
const url = `${org.getField<string>(Org.Fields.INSTANCE_URL)}/${args.endpoint}`;
96-
9785
const options = {
9886
agent: { https: new ProxyAgent() },
9987
method: flags.method,
10088
headers: {
89+
...SFDX_HTTP_HEADERS,
10190
Authorization: `Bearer ${
10291
// we don't care about apiVersion here, just need to get the access token.
10392
// eslint-disable-next-line sf-plugin/get-connection-with-version
10493
org.getConnection().getConnectionOptions().accessToken!
10594
}`,
106-
...(flags.header ? Rest.getHeaders(flags.header) : {}),
95+
...headers,
10796
},
108-
body: flags.method === 'GET' ? undefined : flags.body,
97+
body,
10998
throwHttpErrors: false,
11099
followRedirect: false,
111100
};
@@ -127,12 +116,10 @@ export class Rest extends SfCommand<void> {
127116

128117
// Print HTTP response status and headers.
129118
if (flags.include) {
130-
let httpInfo = `HTTP/${res.httpVersion} ${res.statusCode} ${EOL}`;
131-
132-
for (const [header] of Object.entries(res.headers)) {
133-
httpInfo += `${ansis.blue.bold(header)}: ${res.headers[header] as string}${EOL}`;
134-
}
135-
this.log(httpInfo);
119+
this.log(`HTTP/${res.httpVersion} ${res.statusCode}`);
120+
Object.entries(res.headers).map(([header, value]) => {
121+
this.log(`${ansis.blue.bold(header)}: ${Array.isArray(value) ? value.join(',') : value ?? '<undefined>'}`);
122+
});
136123
}
137124

138125
try {

src/shared/methods.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2023, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import { SfError } from '@salesforce/core';
8+
import type { Headers } from 'got';
9+
10+
export function getHeaders(keyValPair: string[]): Headers {
11+
const headers: { [key: string]: string } = {};
12+
13+
for (const header of keyValPair) {
14+
const [key, ...rest] = header.split(':');
15+
const value = rest.join(':').trim();
16+
if (!key || !value) {
17+
throw new SfError(`Failed to parse HTTP header: "${header}".`, 'Failed To Parse HTTP Header', [
18+
'Make sure the header is in a "key:value" format, e.g. "Accept: application/json"',
19+
]);
20+
}
21+
headers[key] = value;
22+
}
23+
24+
return headers;
25+
}

test/commands/api/request/rest.test.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import fs from 'node:fs';
88
import * as process from 'node:process';
99
import path from 'node:path';
10+
import * as assert from 'node:assert';
1011
import { SfError } from '@salesforce/core';
1112
import { expect } from 'chai';
1213
import stripAnsi from 'strip-ansi';
1314
import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup';
1415
import { sleep } from '@salesforce/kit';
1516
import nock = require('nock');
17+
import { stubUx } from '@salesforce/sf-plugins-core';
1618
import { Rest } from '../../../../src/commands/api/request/rest.js';
1719

1820
describe('rest', () => {
@@ -21,8 +23,7 @@ describe('rest', () => {
2123
username: '[email protected]',
2224
});
2325

24-
let stdoutSpy: sinon.SinonSpy;
25-
26+
let uxStub: ReturnType<typeof stubUx>;
2627
const orgLimitsResponse = {
2728
ActiveScratchOrgs: {
2829
Max: 200,
@@ -32,8 +33,7 @@ describe('rest', () => {
3233

3334
beforeEach(async () => {
3435
await $$.stubAuths(testOrg);
35-
36-
stdoutSpy = $$.SANDBOX.stub(process.stdout, 'write');
36+
uxStub = stubUx($$.SANDBOX);
3737
});
3838

3939
afterEach(() => {
@@ -45,21 +45,31 @@ describe('rest', () => {
4545

4646
await Rest.run(['services/data/v56.0/limits', '--target-org', '[email protected]']);
4747

48-
const output = stripAnsi(stdoutSpy.args.flat().join(''));
48+
expect(uxStub.styledJSON.args[0][0]).to.deep.equal(orgLimitsResponse);
49+
});
4950

50-
expect(JSON.parse(output)).to.deep.equal(orgLimitsResponse);
51+
it('should throw error for invalid header args', async () => {
52+
try {
53+
await Rest.run(['services/data/v56.0/limits', '--target-org', '[email protected]', '-H', 'myInvalidHeader']);
54+
assert.fail('the above should throw');
55+
} catch (e) {
56+
expect((e as SfError).name).to.equal('Failed To Parse HTTP Header');
57+
expect((e as SfError).message).to.equal('Failed to parse HTTP header: "myInvalidHeader".');
58+
expect((e as SfError).actions).to.deep.equal([
59+
'Make sure the header is in a "key:value" format, e.g. "Accept: application/json"',
60+
]);
61+
}
5162
});
5263

5364
it('should redirect to file', async () => {
5465
nock(testOrg.instanceUrl).get('/services/data/v56.0/limits').reply(200, orgLimitsResponse);
55-
66+
const writeSpy = $$.SANDBOX.stub(process.stdout, 'write');
5667
await Rest.run(['services/data/v56.0/limits', '--target-org', '[email protected]', '--stream-to-file', 'myOutput.txt']);
5768

5869
// gives it a second to resolve promises and close streams before we start asserting
5970
await sleep(1000);
60-
const output = stripAnsi(stdoutSpy.args.flat().join(''));
6171

62-
expect(output).to.deep.equal('File saved to myOutput.txt' + '\n');
72+
expect(writeSpy.args.flat().join('')).to.deep.equal('File saved to myOutput.txt' + '\n');
6373
expect(JSON.parse(fs.readFileSync('myOutput.txt', 'utf8'))).to.deep.equal(orgLimitsResponse);
6474

6575
after(() => {
@@ -76,6 +86,7 @@ describe('rest', () => {
7686
<Remaining>198</Remaining>
7787
</ActiveScratchOrgs>
7888
</LimitsSnapshot>`;
89+
const writeSpy = $$.SANDBOX.stub(process.stdout, 'write');
7990

8091
nock(testOrg.instanceUrl, {
8192
reqheaders: {
@@ -87,7 +98,7 @@ describe('rest', () => {
8798

8899
await Rest.run(['services/data', '--header', 'Accept: application/xml', '--target-org', '[email protected]']);
89100

90-
const output = stripAnsi(stdoutSpy.args.flat().join(''));
101+
const output = stripAnsi(writeSpy.args.flat().join(''));
91102

92103
// https://github.com/oclif/core/blob/ff76400fb0bdfc4be0fa93056e86183b9205b323/src/command.ts#L248-L253
93104
expect(output).to.equal(xmlRes + '\n');
@@ -117,8 +128,6 @@ describe('rest', () => {
117128

118129
await Rest.run(['services/data/v56.0/limites', '--target-org', '[email protected]']);
119130

120-
const output = stripAnsi(stdoutSpy.args.flat().join(''));
121-
122-
expect(JSON.parse(output)).to.deep.equal(orgLimitsResponse);
131+
expect(uxStub.styledJSON.args[0][0]).to.deep.equal(orgLimitsResponse);
123132
});
124133
});

0 commit comments

Comments
 (0)