Skip to content

W-16910154 fix: change url builder, add @ prefix for files #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
Loading