Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,9 @@ export const convertQueryResultToCSV = (queryResult: QueryResult): string => {
* @returns Promise resolving to query results with records and metadata
*/
const runSoqlQuery = async (connection: Connection, query: string, useTooling = false): Promise<QueryResult> => {
channelService.appendLine(nls.localize('data_query_running_query'));
channelService.appendLine(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the channel logs just to make sure these worked. It's not important to keep them if you don't want to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this 🙂

nls.localize('data_query_running_query', useTooling ? nls.localize('tooling_API') : nls.localize('REST_API'))
);

// Get user-configured query limit (if any)
const maxFetch = await getMaxFetch();
Expand Down
46 changes: 27 additions & 19 deletions packages/salesforcedx-vscode-soql/src/commands/queryPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import * as vscode from 'vscode';
import { nls } from '../messages';
import { channelService } from '../services/channel';
import { getConnection } from '../services/org';
import { formatErrorMessage, GetDocumentQueryAndApiInputs, GetQueryAndApiInputs, QueryAndApiInputs } from './queryUtils';
import {
formatErrorMessage,
GetDocumentQueryInputsForPlan,
GetQueryInputsForPlan,
QueryInputs
} from './queryUtils';

type QueryPlanNote = {
description: string;
Expand All @@ -33,7 +38,7 @@ type QueryPlanResponse = {
plans: QueryPlanEntry[];
};

export const formatQueryPlanResults = (response: QueryPlanResponse): string => {
const formatQueryPlanResults = (response: QueryPlanResponse): string => {
const { plans } = response;

if (!plans?.length) {
Expand Down Expand Up @@ -61,14 +66,16 @@ export const formatQueryPlanResults = (response: QueryPlanResponse): string => {
const table = createTable(rows, columns, nls.localize('query_plan_table_title'));

const seenNotes = new Set<string>();
const allNotes = plans.flatMap(plan => plan.notes ?? []).filter(note => {
const key = `${note.description}|${note.tableEnumOrId}|${note.fields.join(',')}`;
if (seenNotes.has(key)) {
return false;
}
seenNotes.add(key);
return true;
});
const allNotes = plans
.flatMap(plan => plan.notes ?? [])
.filter(note => {
const key = `${note.description}|${note.tableEnumOrId}|${note.fields.join(',')}`;
if (seenNotes.has(key)) {
return false;
}
seenNotes.add(key);
return true;
});
if (allNotes.length === 0) {
return table;
}
Expand All @@ -81,23 +88,20 @@ export const formatQueryPlanResults = (response: QueryPlanResponse): string => {
};

class QueryPlanExecutor {
public async execute(response: ContinueResponse<QueryAndApiInputs>): Promise<void> {
public async execute(response: ContinueResponse<QueryInputs>): Promise<void> {
if (vscode.workspace.getConfiguration('salesforcedx-vscode-core').get<boolean>('clearOutputTab', false)) {
channelService.clear();
}

const { query, api } = response.data;
const { query } = response.data;

try {
const connection = await getConnection();
channelService.appendLine(nls.localize('query_plan_running'));
channelService.appendLine(nls.localize('query_plan_running', nls.localize('REST_API')));

const apiVersion = connection.getApiVersion();
const encodedQuery = encodeURIComponent(query);
const path =
api === 'TOOLING'
? `/services/data/v${apiVersion}/tooling/query?explain=${encodedQuery}`
: `/services/data/v${apiVersion}/query?explain=${encodedQuery}`;
const path = `/services/data/v${apiVersion}/query?explain=${encodedQuery}`;

const result = await connection.request<QueryPlanResponse>(path);
channelService.appendLine(`\n${formatQueryPlanResults(result)}\n`);
Expand All @@ -111,14 +115,18 @@ class QueryPlanExecutor {
}

export const queryPlan = Effect.fn('sf.data.query.explain')(function* () {
const commandlet = new SfCommandlet(sfProjectPreconditionChecker, new GetQueryAndApiInputs(), new QueryPlanExecutor());
const commandlet = new SfCommandlet(
sfProjectPreconditionChecker,
new GetQueryInputsForPlan(),
new QueryPlanExecutor()
);
yield* Effect.promise(() => commandlet.run());
});

export const queryPlanDocument = Effect.fn('sf.data.query.explain.document')(function* () {
const commandlet = new SfCommandlet(
sfProjectPreconditionChecker,
new GetDocumentQueryAndApiInputs(),
new GetDocumentQueryInputsForPlan(),
new QueryPlanExecutor()
);
yield* Effect.promise(() => commandlet.run());
Expand Down
170 changes: 67 additions & 103 deletions packages/salesforcedx-vscode-soql/src/commands/queryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,39 @@ export type QueryAndApiInputs = {
api: 'REST' | 'TOOLING';
};

export type QueryInputs = { query: string };

const API_ITEMS = [
{ api: 'REST' as const, label: nls.localize('REST_API'), description: nls.localize('REST_API_description') },
{ api: 'TOOLING' as const, label: nls.localize('tooling_API'), description: nls.localize('tooling_API_description') }
];

const INPUT_BOX_OPTIONS: vscode.InputBoxOptions = {
prompt: nls.localize('parameter_gatherer_enter_soql_query')
};

const normalizeQuery = (q: string): string =>
q.replace('[', '').replace(']', '').replaceAll(/(\r\n|\n)/g, ' ').trim();

const pickApiForQuery = async (
query: string
): Promise<CancelResponse | ContinueResponse<QueryAndApiInputs>> => {
const selection = await vscode.window.showQuickPick(API_ITEMS);
return selection ? { type: 'CONTINUE', data: { query, api: selection.api } } : { type: 'CANCEL' };
};

export class GetQueryAndApiInputs implements ParametersGatherer<QueryAndApiInputs> {
public async gather(): Promise<CancelResponse | ContinueResponse<QueryAndApiInputs>> {
const editor = vscode.window.activeTextEditor;

let query;

if (!editor) {
const userInputOptions: vscode.InputBoxOptions = {
prompt: nls.localize('parameter_gatherer_enter_soql_query')
};
query = await vscode.window.showInputBox(userInputOptions);
} else {
const document = editor.document;
if (editor.selection.isEmpty) {
const userInputOptions: vscode.InputBoxOptions = {
prompt: nls.localize('parameter_gatherer_enter_soql_query')
};
query = await vscode.window.showInputBox(userInputOptions);
} else {
query = document.getText(editor.selection);
}
}
const query = !editor
? await vscode.window.showInputBox(INPUT_BOX_OPTIONS)
: editor.selection.isEmpty
? await vscode.window.showInputBox(INPUT_BOX_OPTIONS)
: editor.document.getText(editor.selection);
if (!query) {
return { type: 'CANCEL' };
}

query = query
.replace('[', '')
.replace(']', '')
.replaceAll(/(\r\n|\n)/g, ' ');

const restApi = {
api: 'REST' as const,
label: nls.localize('REST_API'),
description: nls.localize('REST_API_description')
};

const toolingApi = {
api: 'TOOLING' as const,
label: nls.localize('tooling_API'),
description: nls.localize('tooling_API_description')
};

const apiItems = [restApi, toolingApi];
const selection = await vscode.window.showQuickPick(apiItems);

return selection ? { type: 'CONTINUE', data: { query, api: selection.api } } : { type: 'CANCEL' };
return pickApiForQuery(normalizeQuery(query));
}
}

Expand All @@ -69,75 +55,53 @@ export class GetDocumentQueryAndApiInputs implements ParametersGatherer<QueryAnd
if (!editor) {
return { type: 'CANCEL' };
}
const query = normalizeQuery(editor.document.getText());
return query ? pickApiForQuery(query) : { type: 'CANCEL' };
}
}

const query = editor.document.getText().replaceAll(/(\r\n|\n)/g, ' ').trim();
if (!query) {
export class GetQueryInputsForPlan implements ParametersGatherer<QueryInputs> {
public async gather(): Promise<CancelResponse | ContinueResponse<QueryInputs>> {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.selection.isEmpty) {
return { type: 'CANCEL' };
}

const restApi = {
api: 'REST' as const,
label: nls.localize('REST_API'),
description: nls.localize('REST_API_description')
};

const toolingApi = {
api: 'TOOLING' as const,
label: nls.localize('tooling_API'),
description: nls.localize('tooling_API_description')
};

const selection = await vscode.window.showQuickPick([restApi, toolingApi]);
return selection ? { type: 'CONTINUE', data: { query, api: selection.api } } : { type: 'CANCEL' };
const query = normalizeQuery(editor.document.getText(editor.selection));
return query ? { type: 'CONTINUE', data: { query } } : { type: 'CANCEL' };
}
}

/** Formats error messages for better user experience */
export const formatErrorMessage = (error: unknown): string => {
let errorString: string;
if (error instanceof Error) {
errorString = error.message;
} else if (error && typeof error === 'object' && 'message' in error) {
errorString = String(error.message);
} else {
errorString = String(error);
}

if (errorString.includes('HTTP response contains html content')) {
return nls.localize('data_query_error_org_expired');
}

if (errorString.includes('INVALID_SESSION_ID')) {
return nls.localize('data_query_error_session_expired');
}

if (errorString.includes('INVALID_LOGIN')) {
return nls.localize('data_query_error_invalid_login');
}

if (errorString.includes('INSUFFICIENT_ACCESS')) {
return nls.localize('data_query_error_insufficient_access');
}

if (errorString.includes('MALFORMED_QUERY')) {
return nls.localize('data_query_error_malformed_query');
}

if (errorString.includes('INVALID_FIELD')) {
return nls.localize('data_query_error_invalid_field');
}

if (errorString.includes('INVALID_TYPE')) {
return nls.localize('data_query_error_invalid_type');
}

if (errorString.includes('connection') || errorString.includes('network')) {
return nls.localize('data_query_error_connection');
export class GetDocumentQueryInputsForPlan implements ParametersGatherer<QueryInputs> {
public async gather(): Promise<CancelResponse | ContinueResponse<QueryInputs>> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return { type: 'CANCEL' };
}
const query = normalizeQuery(editor.document.getText());
return query ? { type: 'CONTINUE', data: { query } } : { type: 'CANCEL' };
}
}

if (errorString.includes('tooling') && errorString.includes('not found')) {
return nls.localize('data_query_error_tooling_not_found');
}
const ERROR_PATTERNS = [
{ match: (s: string) => s.includes('HTTP response contains html content'), key: 'data_query_error_org_expired' },
{ match: (s: string) => s.includes('INVALID_SESSION_ID'), key: 'data_query_error_session_expired' },
{ match: (s: string) => s.includes('INVALID_LOGIN'), key: 'data_query_error_invalid_login' },
{ match: (s: string) => s.includes('INSUFFICIENT_ACCESS'), key: 'data_query_error_insufficient_access' },
{ match: (s: string) => s.includes('MALFORMED_QUERY'), key: 'data_query_error_malformed_query' },
{ match: (s: string) => s.includes('INVALID_FIELD'), key: 'data_query_error_invalid_field' },
{ match: (s: string) => s.includes('INVALID_TYPE'), key: 'data_query_error_invalid_type' },
{ match: (s: string) => s.includes('connection') || s.includes('network'), key: 'data_query_error_connection' },
{ match: (s: string) => s.includes('tooling') && s.includes('not found'), key: 'data_query_error_tooling_not_found' }
] as const;

return nls.localize('data_query_error_message', errorString);
/** Formats error messages for better user experience */
export const formatErrorMessage = (error: unknown): string => {
const errorString =
error instanceof Error
? error.message
: error && typeof error === 'object' && 'message' in error
? String(error.message)
: String(error);
const matched = ERROR_PATTERNS.find(({ match }) => match(errorString));
return matched ? nls.localize(matched.key) : nls.localize('data_query_error_message', errorString);
};
8 changes: 1 addition & 7 deletions packages/salesforcedx-vscode-soql/src/commonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,5 @@

import * as vscode from 'vscode';
import { Utils } from 'vscode-uri';
import { channelService } from './services/channel';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was unused!

export const getDocumentName = (document: vscode.TextDocument): string =>
Utils.basename(document.uri) || '';

export const trackErrorWithTelemetry = (problemId: string, error: string): void => {
channelService.appendLine(`soql_error_${problemId.toLocaleLowerCase()}: ${error}`);
};
export const getDocumentName = (document: vscode.TextDocument): string => Utils.basename(document.uri) || '';
10 changes: 8 additions & 2 deletions packages/salesforcedx-vscode-soql/src/editor/queryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type { JsonMap } from '@salesforce/ts-types';
import * as vscode from 'vscode';
import { nls } from '../messages';

const hasMessage = (obj: unknown): obj is { message: unknown } =>
typeof obj === 'object' && obj !== null && 'message' in obj;

const getErrorMessage = (error: unknown): string =>
error instanceof Error ? error.message : hasMessage(error) ? String(error.message) : String(error);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type QueryResult<T> = Awaited<ReturnType<Connection['query']>>;

Expand All @@ -27,8 +33,8 @@ export const runQuery =
};
} catch (error) {
if (options.showErrors) {
const message = nls.localize('error_run_soql_query', error.message);
vscode.window.showErrorMessage(message);
const errorMsg = getErrorMessage(error);
vscode.window.showErrorMessage(nls.localize('error_run_soql_query', errorMsg));
}
throw error;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/salesforcedx-vscode-soql/src/messages/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const messages = {
data_query_error_tooling_not_found:
"The requested metadata was not found. This may be because it doesn't exist or you don't have access to it.",
data_query_open_file: 'Open File',
data_query_running_query: 'Running query...',
data_query_running_query: 'Running query with %s...',
data_query_warning_limit:
'Warning: The query result is missing %d records due to a %d record limit. Increase the number of records returned by setting the config value "org-max-query-limit" or the environment variable "SF_ORG_MAX_QUERY_LIMIT" to %d or greater than %d.',
data_query_complete: 'Query complete with %d records returned',
Expand All @@ -81,7 +81,7 @@ export const messages = {
tooling_API: 'Tooling API',
tooling_API_description: 'Use Tooling API to execute the query',
query_plan_selection_text: 'SFDX: Get SOQL Query Plan with Currently Selected Text',
query_plan_running: 'Fetching query plan...',
query_plan_running: 'Fetching query plan with %s...',
query_plan_complete: 'Query plan retrieved successfully'
} as const;

Expand Down
Loading
Loading