From f3ec65ee0e703dd9530604be7c85c2c7dd81c944 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 18 Jul 2022 15:31:56 -0600 Subject: [PATCH 1/3] feat: add a soqlqueryfile parameter to allow longer queries --- command-snapshot.json | 40 ++++++++++++++----- messages/soql.query.json | 5 ++- src/commands/force/data/soql/query.ts | 11 ++++- .../data/soql/query/dataSoqlQuery.nut.ts | 28 ++++++++++++- .../soql/query/dataSoqlQueryCommand.test.ts | 30 +++++++++++++- 5 files changed, 98 insertions(+), 16 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index d2b74f9b..f979e1d9 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -2,12 +2,14 @@ { "command": "force:data:bulk:delete", "plugin": "@salesforce/plugin-data", - "flags": ["apiversion", "csvfile", "json", "loglevel", "sobjecttype", "targetusername", "wait"] + "flags": ["apiversion", "csvfile", "json", "loglevel", "sobjecttype", "targetusername", "wait"], + "alias": [] }, { "command": "force:data:bulk:status", "plugin": "@salesforce/plugin-data", - "flags": ["apiversion", "batchid", "jobid", "json", "loglevel", "targetusername"] + "flags": ["apiversion", "batchid", "jobid", "json", "loglevel", "targetusername"], + "alias": [] }, { "command": "force:data:bulk:upsert", @@ -22,12 +24,14 @@ "sobjecttype", "targetusername", "wait" - ] + ], + "alias": [] }, { "command": "force:data:record:create", "plugin": "@salesforce/plugin-data", - "flags": ["apiversion", "json", "loglevel", "perflog", "sobjecttype", "targetusername", "usetoolingapi", "values"] + "flags": ["apiversion", "json", "loglevel", "perflog", "sobjecttype", "targetusername", "usetoolingapi", "values"], + "alias": [] }, { "command": "force:data:record:delete", @@ -42,7 +46,8 @@ "targetusername", "usetoolingapi", "where" - ] + ], + "alias": [] }, { "command": "force:data:record:get", @@ -57,7 +62,8 @@ "targetusername", "usetoolingapi", "where" - ] + ], + "alias": [] }, { "command": "force:data:record:update", @@ -73,17 +79,30 @@ "usetoolingapi", "values", "where" - ] + ], + "alias": [] }, { "command": "force:data:soql:query", "plugin": "@salesforce/plugin-data", - "flags": ["apiversion", "json", "loglevel", "perflog", "query", "resultformat", "targetusername", "usetoolingapi"] + "flags": [ + "apiversion", + "json", + "loglevel", + "perflog", + "query", + "resultformat", + "soqlqueryfile", + "targetusername", + "usetoolingapi" + ], + "alias": [] }, { "command": "force:data:tree:export", "plugin": "@salesforce/plugin-data", - "flags": ["apiversion", "json", "loglevel", "outputdir", "plan", "prefix", "query", "targetusername"] + "flags": ["apiversion", "json", "loglevel", "outputdir", "plan", "prefix", "query", "targetusername"], + "alias": [] }, { "command": "force:data:tree:import", @@ -97,6 +116,7 @@ "plan", "sobjecttreefiles", "targetusername" - ] + ], + "alias": [] } ] diff --git a/messages/soql.query.json b/messages/soql.query.json index ca33a7ad..1cd4ddb4 100644 --- a/messages/soql.query.json +++ b/messages/soql.query.json @@ -11,10 +11,13 @@ "queryNoResults": "Your query returned no results.", "queryRunningMessage": "Querying Data", "queryMoreUpdateMessage": "Result size is %d, current count is %d", + "soqlqueryfile": "A SOQL query stored in a file", "examples": [ "$ sfdx force:data:soql:query -q \"SELECT Id, Name, Account.Name FROM Contact\"", "$ sfdx force:data:soql:query -q \"SELECT Id, Name FROM Account WHERE ShippingState IN ('CA', 'NY')\"", "$ sfdx force:data:soql:query -q \"SELECT Id, Name FROM Account WHERE ShippingState IN ('CA', 'NY')\" --perflog --json", - "$ sfdx force:data:soql:query -q \"SELECT Name FROM ApexTrigger\" -t" + "$ sfdx force:data:soql:query -q \"SELECT Name FROM ApexTrigger\" -t", + "$ sfdx force:data:soql:query --soqlqueryfile query.txt", + "$ sfdx force:data:soql:query --soqlqueryfile query.txt -t" ] } diff --git a/src/commands/force/data/soql/query.ts b/src/commands/force/data/soql/query.ts index 24a25342..df192c64 100644 --- a/src/commands/force/data/soql/query.ts +++ b/src/commands/force/data/soql/query.ts @@ -6,6 +6,7 @@ */ import * as os from 'os'; +import * as fs from 'fs'; import { flags, FlagsConfig } from '@salesforce/command'; import { CliUx } from '@oclif/core'; import { Connection, Logger, Messages, SfdxConfigAggregator } from '@salesforce/core'; @@ -182,8 +183,13 @@ export class DataSoqlQueryCommand extends DataCommand { public static readonly flagsConfig: FlagsConfig = { query: flags.string({ char: 'q', - required: true, description: messages.getMessage('queryToExecute'), + exclusive: ['soqlqueryfile'], + }), + soqlqueryfile: flags.filepath({ + char: 'f', + description: messages.getMessage('soqlqueryfile'), + exclusive: ['query'], }), usetoolingapi: flags.boolean({ char: 't', @@ -220,9 +226,10 @@ export class DataSoqlQueryCommand extends DataCommand { if (this.flags.resultformat !== 'json') this.ux.startSpinner(messages.getMessage('queryRunningMessage')); const query = new SoqlQuery(); const conn = this.getConnection(); + const queryString = (this.flags.query as string) ?? fs.readFileSync(this.flags.soqlqueryfile, 'utf8'); const queryResult: SoqlQueryResult = await query.runSoqlQuery( conn as Connection, - this.flags.query, + queryString, this.logger, this.configAggregator ); diff --git a/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts b/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts index 78526cdb..7e8157de 100644 --- a/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts +++ b/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts @@ -5,11 +5,11 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import * as path from 'path'; +import * as fs from 'fs'; import * as shell from 'shelljs'; -import { isArray, AnyJson, ensureString } from '@salesforce/ts-types'; +import { AnyJson, Dictionary, ensureString, getString, isArray } from '@salesforce/ts-types'; import { expect } from 'chai'; import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; -import { Dictionary, getString } from '@salesforce/ts-types'; export interface QueryResult { totalSize: number; @@ -116,6 +116,16 @@ describe('data:soql:query command', () => { expect(stdError).to.include('unexpected token'); }); + it('should return account records, from --soqlqueryfile', () => { + const filepath = path.join(testSession.dir, 'soql.txt'); + fs.writeFileSync(filepath, 'SELECT'); + + const result = execCmd(`force:data:soql:query --soqlqueryfile ${filepath}`, { ensureExitCode: 1 }).shellOutput + .stderr; + const stdError = result?.toLowerCase(); + expect(stdError).to.include('unexpected token'); + }); + it('should error with no such column', () => { // querying ApexClass including SymbolTable column w/o using tooling api will cause "no such column" error const result = runQuery('SELECT Id, Name, SymbolTable from ApexClass', { @@ -194,6 +204,20 @@ describe('data:soql:query command', () => { expect(queryResult).to.match(/ID\s+?NAME\s+?PHONE\s+?WEBSITE\s+?NUMBEROFEMPLOYEES\s+?INDUSTRY/g); expect(queryResult).to.match(/Total number of records retrieved: 1\./g); }); + + it('should return account records, from --soqlqueryfile', () => { + const query = + "SELECT Id, Name, Phone, Website, NumberOfEmployees, Industry FROM Account WHERE Name LIKE 'SampleAccount%' limit 1"; + const filepath = path.join(testSession.dir, 'soql.txt'); + fs.writeFileSync(filepath, query); + + const queryResult = execCmd(`force:data:soql:query --soqlqueryfile ${filepath}`, { ensureExitCode: 0 }) + .shellOutput.stdout; + + expect(queryResult).to.match(/ID\s+?NAME\s+?PHONE\s+?WEBSITE\s+?NUMBEROFEMPLOYEES\s+?INDUSTRY/g); + expect(queryResult).to.match(/Total number of records retrieved: 1\./g); + }); + it('should return account records with nested contacts', () => { const query = "SELECT Id, Name, Phone, Website, NumberOfEmployees, Industry, (SELECT Lastname, Title, Email FROM Contacts) FROM Account WHERE Name LIKE 'SampleAccount%'"; diff --git a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts index e95a9367..88643606 100644 --- a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts +++ b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts @@ -23,7 +23,7 @@ interface QueryResult { result: { totalSize: number; records: [] }; } -describe.skip('Execute a SOQL statement', function (): void { +describe('Execute a SOQL statement', function (): void { let sandbox: SinonSandbox; beforeEach(() => { sandbox = sinon.createSandbox(); @@ -148,6 +148,34 @@ describe.skip('Execute a SOQL statement', function (): void { expect(ctx.stdout).to.include('records retrieved: 1'); }); }); + describe('flag validation between --query and --soqlqueryfile', () => { + beforeEach(() => { + soqlQuerySpy = sandbox + .stub(SoqlQuery.prototype, 'runSoqlQuery') + .resolves(soqlQueryExemplars.complexSubQuery.soqlQueryResult); + }); + + afterEach(() => { + sandbox.restore(); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .stderr() + .command([ + QUERY_COMMAND, + '--targetusername', + 'test@org.com', + '--query', + 'SELECT Amount, Id, Name,StageName, CloseDate, (SELECT Id, ListPrice, PriceBookEntry.UnitPrice, PricebookEntry.Name, PricebookEntry.Id, PricebookEntry.product2.Family FROM OpportunityLineItems) FROM Opportunity', + '--soqlqueryfile', + 'soql.txt', + ]) + .it('should have human results for a complex subquery', (ctx) => { + expect(ctx.stderr).to.include('--soqlqueryfile= cannot also be provided when using --query='); + }); + }); describe('reporters produce the correct aggregate query', () => { beforeEach(() => { soqlQuerySpy = sandbox From 32693369af7c1e9d4eb7b4422eb9b4bfb2b2b7c1 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Fri, 22 Jul 2022 08:58:02 -0600 Subject: [PATCH 2/3] test: update test description, renable skipped tests --- test/commands/force/data/bulk/status.test.ts | 2 -- .../force/data/soql/query/dataSoqlQuery.nut.ts | 2 +- .../data/soql/query/dataSoqlQueryCommand.test.ts | 11 ----------- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/test/commands/force/data/bulk/status.test.ts b/test/commands/force/data/bulk/status.test.ts index f958878c..a746078c 100644 --- a/test/commands/force/data/bulk/status.test.ts +++ b/test/commands/force/data/bulk/status.test.ts @@ -15,7 +15,6 @@ interface StatusResult { describe('force:data:bulk:status', () => { test - .skip() .do(() => { const Job = { getAuthInfoFields: () => { @@ -70,7 +69,6 @@ describe('force:data:bulk:status', () => { }); test - .skip() .do(() => { const Job = { getAuthInfoFields: () => { diff --git a/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts b/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts index 7e8157de..03304dd8 100644 --- a/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts +++ b/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts @@ -116,7 +116,7 @@ describe('data:soql:query command', () => { expect(stdError).to.include('unexpected token'); }); - it('should return account records, from --soqlqueryfile', () => { + it('should produce correct error when invalid soql provided', () => { const filepath = path.join(testSession.dir, 'soql.txt'); fs.writeFileSync(filepath, 'SELECT'); diff --git a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts index 88643606..3efe9778 100644 --- a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts +++ b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts @@ -149,19 +149,8 @@ describe('Execute a SOQL statement', function (): void { }); }); describe('flag validation between --query and --soqlqueryfile', () => { - beforeEach(() => { - soqlQuerySpy = sandbox - .stub(SoqlQuery.prototype, 'runSoqlQuery') - .resolves(soqlQueryExemplars.complexSubQuery.soqlQueryResult); - }); - - afterEach(() => { - sandbox.restore(); - }); - test .withOrg({ username: 'test@org.com' }, true) - .stdout() .stderr() .command([ QUERY_COMMAND, From 90dadd20c8c9d4660d1850c00a0d5fa63700a30a Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 25 Jul 2022 15:17:59 -0600 Subject: [PATCH 3/3] chore: fix flag validation, test name --- src/commands/force/data/soql/query.ts | 2 ++ .../commands/force/data/soql/query/dataSoqlQueryCommand.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/force/data/soql/query.ts b/src/commands/force/data/soql/query.ts index df192c64..72dd80b1 100644 --- a/src/commands/force/data/soql/query.ts +++ b/src/commands/force/data/soql/query.ts @@ -185,11 +185,13 @@ export class DataSoqlQueryCommand extends DataCommand { char: 'q', description: messages.getMessage('queryToExecute'), exclusive: ['soqlqueryfile'], + exactlyOne: ['query', 'soqlqueryfile'], }), soqlqueryfile: flags.filepath({ char: 'f', description: messages.getMessage('soqlqueryfile'), exclusive: ['query'], + exactlyOne: ['query', 'soqlqueryfile'], }), usetoolingapi: flags.boolean({ char: 't', diff --git a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts index 3efe9778..e7148448 100644 --- a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts +++ b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts @@ -161,7 +161,7 @@ describe('Execute a SOQL statement', function (): void { '--soqlqueryfile', 'soql.txt', ]) - .it('should have human results for a complex subquery', (ctx) => { + .it('should throw an error when both query (inline/file query) flags are specified', (ctx) => { expect(ctx.stderr).to.include('--soqlqueryfile= cannot also be provided when using --query='); }); });