From 4acc1da182c8c735089348f93bded14f6c685626 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 4 Oct 2024 10:48:40 -0600 Subject: [PATCH 1/3] fix: change url builder, add @ prefix for files --- command-snapshot.json | 12 +------- messages/rest.md | 16 +++++----- src/commands/api/request/rest.ts | 25 +++++++++++----- test/commands/api/request/rest/rest.nut.ts | 28 +++++++++++++---- test/commands/api/request/rest/rest.test.ts | 30 ++++++++----------- test/test-files/data-project/bulkOpen.json | 6 ++++ test/test-files/data-project/fileUpload.json | 2 +- .../data-project/profilePicUpload.json | 2 +- test/test-files/data-project/raw.json | 2 +- test/test-files/data-project/rest.json | 2 +- 10 files changed, 71 insertions(+), 54 deletions(-) create mode 100644 test/test-files/data-project/bulkOpen.json diff --git a/command-snapshot.json b/command-snapshot.json index e4cd25e..63d771f 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -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" } ] diff --git a/messages/rest.md b/messages/rest.md index c23d80f..bb7924d 100644 --- a/messages/rest.md +++ b/messages/rest.md @@ -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/' --body "{\"BillingCity\": \"San Francisco\"}" --method PATCH + <%= config.bin %> <%= command.id %> '/services/data/v56.0/sobjects/account/' --body "{\"BillingCity\": \"San Francisco\"}" -X 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: @@ -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 it with '@' like CURL diff --git a/src/commands/api/request/rest.ts b/src/commands/api/request/rest.ts index a263a25..1b62f30 100644 --- a/src/commands/api/request/rest.ts +++ b/src/commands/api/request/rest.ts @@ -55,7 +55,6 @@ export class Rest extends SfCommand { public static enableJsonFlag = false; public static readonly flags = { 'target-org': Flags.requiredOrg(), - 'api-version': Flags.orgApiVersion(), include: includeFlag, method: Flags.option({ options: methodOptions, @@ -73,6 +72,7 @@ export class Rest extends SfCommand { description: messages.getMessage('flags.file.description'), helpValue: 'file', char: 'f', + exclusive: ['body'], }), 'stream-to-file': streamToFileFlag, body: Flags.string({ @@ -106,12 +106,7 @@ export class Rest extends SfCommand { // 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(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(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'; @@ -120,8 +115,22 @@ export class Rest extends SfCommand { // 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) { diff --git a/test/commands/api/request/rest/rest.nut.ts b/test/commands/api/request/rest/rest.nut.ts index d36b09e..1e41eaa 100644 --- a/test/commands/api/request/rest/rest.nut.ts +++ b/test/commands/api/request/rest/rest.nut.ts @@ -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)).to.have.length; @@ -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('')).to.be.true; @@ -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"'); @@ -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'); @@ -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'); diff --git a/test/commands/api/request/rest/rest.test.ts b/test/commands/api/request/rest/rest.test.ts index 699d7c6..1e20c33 100644 --- a/test/commands/api/request/rest/rest.test.ts +++ b/test/commands/api/request/rest/rest.test.ts @@ -52,7 +52,7 @@ 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); }); @@ -60,14 +60,14 @@ describe('rest', () => { 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'); @@ -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); @@ -119,9 +111,7 @@ describe('rest', () => { .reply(200, xmlRes); await Rest.run([ - '/', - '--api-version', - '42.0', + '/services/data/v42.0/', '--method', 'GET', '--header', @@ -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".'); @@ -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); }); diff --git a/test/test-files/data-project/bulkOpen.json b/test/test-files/data-project/bulkOpen.json new file mode 100644 index 0000000..f62aff1 --- /dev/null +++ b/test/test-files/data-project/bulkOpen.json @@ -0,0 +1,6 @@ +{ + "object": "Account", + "contentType": "CSV", + "operation": "insert", + "lineEnding": "LF" +} diff --git a/test/test-files/data-project/fileUpload.json b/test/test-files/data-project/fileUpload.json index c68fd48..b6462fd 100644 --- a/test/test-files/data-project/fileUpload.json +++ b/test/test-files/data-project/fileUpload.json @@ -20,5 +20,5 @@ } ] }, - "url": "connect/files/users/me" + "url": "/services/data/v56.0/connect/files/users/me" } diff --git a/test/test-files/data-project/profilePicUpload.json b/test/test-files/data-project/profilePicUpload.json index 26b54ad..f45a5e7 100644 --- a/test/test-files/data-project/profilePicUpload.json +++ b/test/test-files/data-project/profilePicUpload.json @@ -25,5 +25,5 @@ } ] }, - "url": "connect/user-profiles/me/photo" + "url": "/services/data/v56.0/connect/user-profiles/me/photo" } diff --git a/test/test-files/data-project/raw.json b/test/test-files/data-project/raw.json index 3addfe2..668d907 100644 --- a/test/test-files/data-project/raw.json +++ b/test/test-files/data-project/raw.json @@ -14,6 +14,6 @@ "mode": "raw" }, "url": { - "raw": "/limits" + "raw": "/services/data/v56.0/limits" } } diff --git a/test/test-files/data-project/rest.json b/test/test-files/data-project/rest.json index 05b1e09..39a6d46 100644 --- a/test/test-files/data-project/rest.json +++ b/test/test-files/data-project/rest.json @@ -1,4 +1,4 @@ { "headers": ["Accept:application/json"], - "url": "/limits" + "url": "/services/data/v56.0/limits" } From 2221f36ab81aa72e3a8264539649523a9f4798ef Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 4 Oct 2024 11:54:10 -0600 Subject: [PATCH 2/3] Update messages/rest.md Co-authored-by: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> --- messages/rest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/rest.md b/messages/rest.md index bb7924d..f4391ef 100644 --- a/messages/rest.md +++ b/messages/rest.md @@ -32,7 +32,7 @@ For a full list of supported REST endpoints and resources, see https://developer - Update an account record using the PATCH method: - <%= config.bin %> <%= command.id %> '/services/data/v56.0/sobjects/account/' --body "{\"BillingCity\": \"San Francisco\"}" -X PATCH + <%= config.bin %> <%= command.id %> '/services/data/v56.0/sobjects/account/' --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: From ad8984c1b53c675afa38d2e7b5d714297ccdc65a Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 4 Oct 2024 11:54:14 -0600 Subject: [PATCH 3/3] Update messages/rest.md Co-authored-by: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> --- messages/rest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/rest.md b/messages/rest.md index f4391ef..f5c2ddf 100644 --- a/messages/rest.md +++ b/messages/rest.md @@ -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. If passing a file, prefix it with '@' like CURL +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 '@'.