Skip to content

Commit

Permalink
Merge pull request #34 from salesforcecli/wr/removeUrlPrepending
Browse files Browse the repository at this point in the history
fix: change url builder, add @ prefix for files
  • Loading branch information
mdonnalley authored Oct 7, 2024
2 parents 341d2b9 + ad8984c commit 65953b9
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 54 deletions.
12 changes: 1 addition & 11 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,7 @@
"command": "api:request:rest",
"flagAliases": [],
"flagChars": ["H", "S", "X", "b", "f", "i", "o"],
"flags": [
"api-version",
"body",
"file",
"flags-dir",
"header",
"include",
"method",
"stream-to-file",
"target-org"
],
"flags": ["body", "file", "flags-dir", "header", "include", "method", "stream-to-file", "target-org"],
"plugin": "@salesforce/plugin-api"
}
]
16 changes: 8 additions & 8 deletions messages/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ For a full list of supported REST endpoints and resources, see https://developer

- List information about limits in the org with alias "my-org":

<%= config.bin %> <%= command.id %> 'limits' --target-org my-org
<%= config.bin %> <%= command.id %> 'services/data/v56.0/limits' --target-org my-org

- List all endpoints in your default org; write the output to a file called "output.txt" and include the HTTP response status and headers:

<%= config.bin %> <%= command.id %> '/' --stream-to-file output.txt --include
<%= config.bin %> <%= command.id %> '/services/data/v56.0/' --stream-to-file output.txt --include

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

<%= config.bin %> <%= command.id %> 'limits' --header 'Accept: application/xml'
<%= config.bin %> <%= command.id %> '/services/data/v56.0/limits' --header 'Accept: application/xml'

- Create an account record using the POST method; specify the request details directly in the "--body" flag:

<%= config.bin %> <%= command.id %> sobjects/account --body "{\"Name\" : \"Account from REST API\",\"ShippingCity\" : \"Boise\"}" --method POST
<%= config.bin %> <%= command.id %> /services/data/v56.0/sobjects/account --body "{\"Name\" : \"Account from REST API\",\"ShippingCity\" : \"Boise\"}" --method POST

- Create an account record using the information in a file called "info.json":
- Create an account record using the information in a file called "info.json" (note the @ prefixing the file name):

<%= config.bin %> <%= command.id %> 'sobjects/account' --body info.json --method POST
<%= config.bin %> <%= command.id %> '/services/data/v56.0/sobjects/account' --body @info.json --method POST

- Update an account record using the PATCH method:

<%= config.bin %> <%= command.id %> 'sobjects/account/<Account ID>' --body "{\"BillingCity\": \"San Francisco\"}" --method PATCH
<%= config.bin %> <%= command.id %> '/services/data/v56.0/sobjects/account/<Account ID>' --body "{\"BillingCity\": \"San Francisco\"}" --method PATCH

- Store the values for the request header, body, and so on, in a file, which you then specify with the --file flag; see the description of --file for more information:

Expand Down Expand Up @@ -81,4 +81,4 @@ HTTP header in "key:value" format.

# flags.body.summary

File or content for the body of the HTTP request. Specify "-" to read from standard input or "" for an empty body.
File or content for the body of the HTTP request. Specify "-" to read from standard input or "" for an empty body. If passing a file, prefix the filename with '@'.
25 changes: 17 additions & 8 deletions src/commands/api/request/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export class Rest extends SfCommand<void> {
public static enableJsonFlag = false;
public static readonly flags = {
'target-org': Flags.requiredOrg(),
'api-version': Flags.orgApiVersion(),
include: includeFlag,
method: Flags.option({
options: methodOptions,
Expand All @@ -73,6 +72,7 @@ export class Rest extends SfCommand<void> {
description: messages.getMessage('flags.file.description'),
helpValue: 'file',
char: 'f',
exclusive: ['body'],
}),
'stream-to-file': streamToFileFlag,
body: Flags.string({
Expand Down Expand Up @@ -106,12 +106,7 @@ export class Rest extends SfCommand<void> {

// the conditional above ensures we either have an arg or it's in the file - now we just have to find where the URL value is
const specified = args.url ?? (fileOptions?.url as { raw: string }).raw ?? fileOptions?.url;
const url = new URL(
`${org.getField<string>(Org.Fields.INSTANCE_URL)}/services/data/v${
flags['api-version'] ?? (await org.retrieveMaxApiVersion())
// replace first '/' to create valid URL
}/${specified.replace(/\//y, '')}`
);
const url = new URL(`${org.getField<string>(Org.Fields.INSTANCE_URL)}/${specified.replace(/\//y, '')}`);

// default the method to GET here to allow flags to override, but not hinder reading from files, rather than setting the default in the flag definition
const method = flags.method ?? fileOptions?.method ?? 'GET';
Expand All @@ -120,8 +115,22 @@ export class Rest extends SfCommand<void> {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new SfError(`"${method}" must be one of ${methodOptions.join(', ')}`);
}
// body can be undefined;
// if we have a --body @myfile.json, read the file
// if we have a --body '{"key":"value"}' use that
// else read from --file's body
let body;
if (method !== 'GET') {
if (flags.body && flags.body.startsWith('@')) {
// remove the '@' and read it
body = readFileSync(flags.body.substring(1));
} else if (flags.body) {
body = flags.body;
} else if (!flags.body) {
body = getBodyContents(fileOptions?.body);
}
}

const body = method !== 'GET' ? flags.body ?? getBodyContents(fileOptions?.body) : undefined;
let headers = getHeaders(flags.header ?? fileOptions?.header);

if (body instanceof FormData) {
Expand Down
28 changes: 22 additions & 6 deletions test/commands/api/request/rest/rest.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ skipIfWindows('api:request:rest NUT', () => {

describe('std out', () => {
it('get result in json format', () => {
const result = execCmd("api request rest 'limits'").shellOutput.stdout;
const result = execCmd("api request rest '/services/data/v56.0/limits'").shellOutput.stdout;

// make sure we got a JSON object back
expect(Object.keys(JSON.parse(result) as Record<string, unknown>)).to.have.length;
Expand All @@ -71,7 +71,8 @@ skipIfWindows('api:request:rest NUT', () => {
});

it('should pass headers', () => {
const result = execCmd("api request rest 'limits' -H 'Accept: application/xml'").shellOutput.stdout;
const result = execCmd("api request rest '/services/data/v56.0/limits' -H 'Accept: application/xml'").shellOutput
.stdout;

// the headers will change this to xml
expect(result.startsWith('<?xml version="1.0" encoding="UTF-8"?><LimitsSnapshot>')).to.be.true;
Expand All @@ -94,9 +95,22 @@ skipIfWindows('api:request:rest NUT', () => {
expect(res).to.include('"standardEmailPhotoUrl"');
});

it('can send --body as a file', () => {
const res = execCmd(
`api request rest /services/data/v60.0/jobs/ingest -X POST --body @${join(
testSession.project.dir,
'bulkOpen.json'
)}`
).shellOutput.stdout;
// this prints as json to stdout, verify a few key/values
expect(res).to.include('"id":');
expect(res).to.include('"operation": "insert"');
expect(res).to.include('"object": "Account"');
});

it('can send raw data, with disabled headers', () => {
const res = execCmd(`api request rest --file ${join(testSession.project.dir, 'raw.json')}`).shellOutput.stdout;
// this prints as json to stdout, verify a few key/valuess
// this prints as json to stdout, verify a few key/values
expect(res).to.include('"AnalyticsExternalDataSizeMB":');
expect(res).to.include('"SingleEmail"');
expect(res).to.include('"PermissionSets"');
Expand All @@ -105,7 +119,8 @@ skipIfWindows('api:request:rest NUT', () => {

describe('stream-to-file', () => {
it('get result in json format', () => {
const result = execCmd("api request rest 'limits' --stream-to-file out.txt").shellOutput.stdout;
const result = execCmd("api request rest '/services/data/v56.0/limits' --stream-to-file out.txt").shellOutput
.stdout;

expect(result.trim()).to.equal('File saved to out.txt');

Expand All @@ -115,8 +130,9 @@ skipIfWindows('api:request:rest NUT', () => {
});

it('should pass headers', () => {
const result = execCmd("api request rest 'limits' -H 'Accept: application/xml' --stream-to-file out.txt")
.shellOutput.stdout;
const result = execCmd(
"api request rest '/services/data/v56.0/limits' -H 'Accept: application/xml' --stream-to-file out.txt"
).shellOutput.stdout;

expect(result.trim()).to.equal('File saved to out.txt');

Expand Down
30 changes: 13 additions & 17 deletions test/commands/api/request/rest/rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,22 @@ describe('rest', () => {
it('should request org limits and default to "GET" HTTP method', async () => {
nock(testOrg.instanceUrl).get('/services/data/v56.0/limits').reply(200, orgLimitsResponse);

await Rest.run(['--api-version', '56.0', 'limits', '--target-org', 'test@hub.com']);
await Rest.run(['services/data/v56.0/limits', '--target-org', 'test@hub.com']);

expect(uxStub.styledJSON.args[0][0]).to.deep.equal(orgLimitsResponse);
});

it("should strip leading '/'", async () => {
nock(testOrg.instanceUrl).get('/services/data/v56.0/limits').reply(200, orgLimitsResponse);

await Rest.run(['--api-version', '56.0', '/limits', '--target-org', 'test@hub.com']);
await Rest.run(['/services/data/v56.0/limits', '--target-org', 'test@hub.com']);

expect(uxStub.styledJSON.args[0][0]).to.deep.equal(orgLimitsResponse);
});

it('should throw error for invalid header args', async () => {
try {
await Rest.run(['limits', '--target-org', 'test@hub.com', '-H', 'myInvalidHeader']);
await Rest.run(['/services/data/v56.0/limits', '--target-org', 'test@hub.com', '-H', 'myInvalidHeader']);
assert.fail('the above should throw');
} catch (e) {
expect((e as SfError).name).to.equal('Failed To Parse HTTP Header');
Expand All @@ -81,15 +81,7 @@ describe('rest', () => {
it('should redirect to file', async () => {
nock(testOrg.instanceUrl).get('/services/data/v56.0/limits').reply(200, orgLimitsResponse);
const writeSpy = $$.SANDBOX.stub(process.stdout, 'write');
await Rest.run([
'--api-version',
'56.0',
'limits',
'--target-org',
'test@hub.com',
'--stream-to-file',
'myOutput.txt',
]);
await Rest.run(['/services/data/v56.0/limits', '--target-org', 'test@hub.com', '--stream-to-file', 'myOutput.txt']);

// gives it a second to resolve promises and close streams before we start asserting
await sleep(1000);
Expand Down Expand Up @@ -119,9 +111,7 @@ describe('rest', () => {
.reply(200, xmlRes);

await Rest.run([
'/',
'--api-version',
'42.0',
'/services/data/v42.0/',
'--method',
'GET',
'--header',
Expand All @@ -136,7 +126,13 @@ describe('rest', () => {

it('should validate HTTP headers are in a "key:value" format', async () => {
try {
await Rest.run(['services/data', '--header', 'Accept application/xml', '--target-org', 'test@hub.com']);
await Rest.run([
'/services/data/v56.0/limits',
'--header',
'Accept application/xml',
'--target-org',
'test@hub.com',
]);
} catch (e) {
const err = e as SfError;
expect(err.message).to.equal('Failed to parse HTTP header: "Accept application/xml".');
Expand Down Expand Up @@ -208,7 +204,7 @@ describe('rest', () => {
location: `${testOrg.instanceUrl}/services/data/v56.0/limits`,
});

await Rest.run(['limites', '--api-version', '56.0', '--target-org', 'test@hub.com']);
await Rest.run(['/services/data/v56.0/limites', '--target-org', 'test@hub.com']);

expect(uxStub.styledJSON.args[0][0]).to.deep.equal(orgLimitsResponse);
});
Expand Down
6 changes: 6 additions & 0 deletions test/test-files/data-project/bulkOpen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"object": "Account",
"contentType": "CSV",
"operation": "insert",
"lineEnding": "LF"
}
2 changes: 1 addition & 1 deletion test/test-files/data-project/fileUpload.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
}
]
},
"url": "connect/files/users/me"
"url": "/services/data/v56.0/connect/files/users/me"
}
2 changes: 1 addition & 1 deletion test/test-files/data-project/profilePicUpload.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
}
]
},
"url": "connect/user-profiles/me/photo"
"url": "/services/data/v56.0/connect/user-profiles/me/photo"
}
2 changes: 1 addition & 1 deletion test/test-files/data-project/raw.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"mode": "raw"
},
"url": {
"raw": "/limits"
"raw": "/services/data/v56.0/limits"
}
}
2 changes: 1 addition & 1 deletion test/test-files/data-project/rest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"headers": ["Accept:application/json"],
"url": "/limits"
"url": "/services/data/v56.0/limits"
}

0 comments on commit 65953b9

Please sign in to comment.