Skip to content

Commit

Permalink
feat: @W-8449669@ migrate force:data:soql:query (#16)
Browse files Browse the repository at this point in the history
refactor: move query support classes into plugin apis
  • Loading branch information
peternhale committed Jan 27, 2021
1 parent 41acd5f commit b6820f8
Show file tree
Hide file tree
Showing 13 changed files with 5,253 additions and 1,374 deletions.
19 changes: 19 additions & 0 deletions packages/plugin-data/messages/soql.query.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"description": "execute a SOQL query",
"noResults": "Your query returned no results.",
"queryToExecute": "SOQL query to execute",
"queryLongDescription": "SOQL query to execute.",
"queryToolingDescription": "execute query with Tooling API",
"queryInvalidReporter": "Unknown result format type. Must be one of the following values: %s",
"resultFormatDescription": "result format emitted to stdout; --json flag overrides this parameter",
"resultFormatLongDescription": "Format to use when displaying results. If you also specify the --json flag, --json overrides this parameter.",
"displayQueryRecordsRetrieved": "Total number of records retrieved: %s.",
"queryNoResults": "Your query returned no results.",
"queryRunningMessage": "Querying Data",
"queryMoreUpdateMessage": "Result size is %d, current count is %d",
"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 Name FROM ApexTrigger\" -t"
]
}
8 changes: 5 additions & 3 deletions packages/plugin-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,22 @@
"test": "sf-test"
},
"dependencies": {
"@oclif/config": "^1",
"@salesforce/command": "^3.0.3",
"@salesforce/core": "^2.13.0",
"@salesforce/data": "^0.1.1",
"@salesforce/ts-types": "^1.4.3",
"chalk": "^4.1.0",
"tslib": "^1"
},
"devDependencies": {
"@salesforce/dev-config": "^2.0.0",
"@salesforce/dev-scripts": "^0.6.2",
"@salesforce/prettier-config": "^0.0.1",
"@salesforce/ts-sinon": "^1.2.3",
"@types/chai-as-promised": "^7.1.3",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-salesforce": "^0.1.0",
Expand All @@ -97,6 +98,7 @@
"shx": "^0.3.3",
"sinon": "^9.0.2",
"ts-node": "^8.10.2",
"typescript": "^3.9.3"
"typescript": "^3.9.3",
"fast-xml-parser": "^3.17.5"
}
}
187 changes: 187 additions & 0 deletions packages/plugin-data/src/commands/force/data/soql/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import * as os from 'os';
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
import { Connection, Logger, Messages, Org } from '@salesforce/core';
import { ensureJsonArray, ensureJsonMap, ensureString, isJsonArray, toJsonMap } from '@salesforce/ts-types';
import { Tooling } from '@salesforce/core/lib/connection';
import { CsvReporter, FormatTypes, HumanReporter, JsonReporter } from '../../../../reporters';
import { Field, FieldType, SoqlQueryResult } from '../../../../dataSoqlQueryTypes';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-data', 'soql.query');

/**
* Class to handle a soql query
*
* Will collect all records and the column metadata of the query
*/
export class SoqlQuery {
public async runSoqlQuery(connection: Connection | Tooling, query: string, logger: Logger): Promise<SoqlQueryResult> {
let columns: Field[] = [];
logger.debug('running query');

const result = await connection.autoFetchQuery(query, { autoFetch: true, maxFetch: 50000 });
logger.debug(`Query complete with ${result.totalSize} records returned`);
if (result.totalSize) {
logger.debug('fetching columns for query');
columns = await this.retrieveColumns(connection, query);
}

// remove nextRecordsUrl and force done to true
delete result.nextRecordsUrl;
result.done = true;
return {
query,
columns,
result,
};
}
/**
* Utility to fetch the columns involved in a soql query.
*
* Columns are then transformed into one of three types, Field, SubqueryField and FunctionField. List of
* fields is returned as the product.
*
* @param connection
* @param query
*/

public async retrieveColumns(connection: Connection | Tooling, query: string): Promise<Field[]> {
// eslint-disable-next-line no-underscore-dangle
const columnUrl = `${connection._baseUrl()}/query?q=${encodeURIComponent(query)}&columns=true`;
const results = toJsonMap(await connection.request(columnUrl));
const columns: Field[] = [];
for (let column of ensureJsonArray(results.columnMetadata)) {
column = ensureJsonMap(column);
const name = ensureString(column.columnName);

if (isJsonArray(column.joinColumns) && column.joinColumns.length > 0) {
if (column.aggregate) {
const field: Field = {
fieldType: FieldType.subqueryField,
name,
fields: [],
};
for (const subcolumn of column.joinColumns) {
const f: Field = {
fieldType: FieldType.field,
name: ensureString(ensureJsonMap(subcolumn).columnName),
};
if (field.fields) field.fields.push(f);
}
columns.push(field);
} else {
for (const subcolumn of column.joinColumns) {
const f: Field = {
fieldType: FieldType.field,
name: `${name}.${ensureString(ensureJsonMap(subcolumn).columnName)}`,
};
columns.push(f);
}
}
} else if (column.aggregate) {
const field: Field = {
fieldType: FieldType.functionField,
name: ensureString(column.displayName),
};
// If it isn't an alias, skip so the display name is used when messaging rows
if (!/expr[0-9]+/.test(name)) {
field.alias = name;
}
columns.push(field);
} else {
columns.push({ fieldType: FieldType.field, name } as Field);
}
}
return columns;
}
}

export class DataSoqlQueryCommand extends SfdxCommand {
public static readonly description = messages.getMessage('description');
public static readonly requiresProject = false;
public static readonly requiresUsername = true;
public static readonly examples = messages.getMessage('examples').split(os.EOL);

public static readonly flagsConfig: FlagsConfig = {
query: flags.string({
char: 'q',
required: true,
description: messages.getMessage('queryToExecute'),
}),
usetoolingapi: flags.boolean({
char: 't',
description: messages.getMessage('queryToolingDescription'),
}),
resultformat: flags.enum({
char: 'r',
description: messages.getMessage('resultFormatDescription'),
options: ['human', 'csv', 'json'],
default: 'human',
}),
};

// Overrides SfdxCommand. This is ensured since requiresUsername == true
protected org!: Org;

/**
* Command run implementation
*
* Returns either a DataSoqlQueryResult or a SfdxResult.
* When the user is using global '--json' flag an instance of SfdxResult is returned.
* This is necessary since '--json' flag reports results in the form of SfdxResult
* and bypasses the definition of start result. The goal is to have the output
* from '--json' and '--resulformat json' be the same.
*
* The DataSoqlQueryResult is necessary to communicate user selections to the reporters.
* The 'this' object available during display() function does not include user input to
* the command, which are necessary for reporter selection.
*
*/
public async run(): Promise<unknown> {
try {
if (this.flags.resultformat !== 'json') this.ux.startSpinner(messages.getMessage('queryRunningMessage'));
const query = new SoqlQuery();
const queryResult: SoqlQueryResult = await query.runSoqlQuery(
this.flags.usetoolingapi ? this.org.getConnection().tooling : this.org.getConnection(),
this.flags.query,
this.logger
);
const results = {
...queryResult,
};
this.displayResults(results);
return queryResult.result;
} finally {
if (this.flags.resultformat !== 'json') this.ux.stopSpinner();
}
}

private displayResults(queryResult: SoqlQueryResult): void {
// bypass if --json flag present
if (!this.flags.json) {
let reporter;
switch (this.flags.resultformat as keyof typeof FormatTypes) {
case 'human':
reporter = new HumanReporter(queryResult, queryResult.columns, this.ux, this.logger);
break;
case 'json':
reporter = new JsonReporter(queryResult, queryResult.columns, this.ux, this.logger);
break;
case 'csv':
reporter = new CsvReporter(queryResult, queryResult.columns, this.ux, this.logger);
break;
default:
throw new Error(`result format is invalid: ${this.flags.resultformat as string}`);
}
// delegate to selected reporter
reporter.display();
}
}
}
33 changes: 33 additions & 0 deletions packages/plugin-data/src/dataSoqlQueryTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { QueryResult } from 'jsforce';
import { Optional } from '@salesforce/ts-types';

export enum FieldType {
field,
subqueryField,
functionField,
}

/**
* interface to represent a field when describing the fields that make up a query result
*/
export interface Field {
fieldType: FieldType;
name: string;
fields?: Field[];
alias?: Optional<string>;
}

/**
* Type to define SoqlQuery results
*/
export type SoqlQueryResult = {
query: string;
result: QueryResult<unknown>;
columns: Field[];
};
Loading

0 comments on commit b6820f8

Please sign in to comment.