Skip to content
72 changes: 5 additions & 67 deletions packages/salesforcedx-vscode-soql/src/commands/dataQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,36 +139,6 @@ export const dataQuery = Effect.fn('sf.data.query')(function* () {
yield* Effect.promise(() => commandlet.run());
});

/**
* Retrieves the maximum fetch limit from user configuration.
* Checks SF CLI config first, then environment variable, then returns undefined if no limit is set.
*
* @returns Promise resolving to the configured limit number, or undefined if no limit is set.
*/
const getMaxFetch = async (): Promise<number | undefined> => {
try {
// Priority 1: Check SF CLI config value (org-max-query-limit)
const configAggregator = await getSoqlRuntime().runPromise(
Effect.gen(function* () {
const api = yield* getServicesApi;
return yield* api.services.ConfigService.getConfigAggregator();
})
);
const configValue = configAggregator.getPropertyValue<string>('org-max-query-limit');
if (configValue) {
const parsed = parseInt(configValue, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} catch {
// If config reading fails, fall back to no limit
}

// No limit configured - return undefined to allow default amount of queries
return undefined;
};

/** Generates table output from query records */
export const generateTableOutput = (records: QueryResult['records'], title: string): string => {
// Ensure the first record exists and is an object
Expand Down Expand Up @@ -366,23 +336,6 @@ export const formatFieldValueForDisplay = (value: unknown): string => {
return stringValue.length > 50 ? `${stringValue.substring(0, 47)}...` : stringValue;
};

/**
* Builds query options for the Salesforce connection query method.
* Supports optional maxFetch limit when user has configured query limits.
*
* @param maxFetch - Optional maximum number of records to fetch. If undefined, no limit is applied.
* @returns Query options object with autoFetch and scanAll settings, plus maxFetch if specified.
*/
export const buildQueryOptions = (maxFetch?: number) => {
const baseOptions = {
autoFetch: true,
scanAll: false
};

// Conditionally add maxFetch if user has configured a limit (including 0)
return maxFetch !== undefined ? { ...baseOptions, maxFetch } : baseOptions;
};

/** Displays query results in table format */
export const displayTableResults = (queryResult: QueryResult): void => {
if (!queryResult.records?.length) {
Expand Down Expand Up @@ -458,29 +411,14 @@ export const formatErrorMessage = (error: unknown): string => {
};

/**
* Executes a SOQL query using the provided connection (REST or Tooling API).
* Applies user-configured query limits if set, otherwise allows results under Salesforce limits.
* Executes a SOQL query, auto-fetching all pages of results up to the user-configured
* `org-max-query-limit` (default 10,000). Emits a lifecycle warning if results are truncated.
*
* @param connection - Salesforce connection (REST or Tooling API)
* @param connection - Salesforce connection
* @param query - SOQL query string to execute
* @returns Promise resolving to query results with records and metadata
* @param useTooling - Whether to use the Tooling API instead of REST
*/
const runSoqlQuery = async (connection: Connection, query: string, useTooling = false): Promise<QueryResult> => {
channelService.appendLine(nls.localize('data_query_running_query'));

// Get user-configured query limit (if any)
const maxFetch = await getMaxFetch();

// Execute query with appropriate options (with or without maxFetch limit)
const result = await (useTooling ? connection.tooling : connection).query(query, buildQueryOptions(maxFetch));

// Show warning if user-configured limit caused records to be truncated
if (maxFetch !== undefined && result.records.length > 0 && result.totalSize > result.records.length) {
const missingRecords = result.totalSize - result.records.length;
channelService.appendLine(
nls.localize('data_query_warning_limit', missingRecords, maxFetch, result.totalSize, maxFetch)
);
}

return result;
return connection.autoFetchQuery(query, { tooling: useTooling });
};
95 changes: 38 additions & 57 deletions packages/salesforcedx-vscode-soql/src/editor/htmlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,72 +9,53 @@ import * as vscode from 'vscode';
import { URI, Utils } from 'vscode-uri';

/**
* The index.html file in the dist folder of the @salesforce/soql-builder-ui
* is built by webpack to be displayed in a normal web context. however, vscode
* uses a custom protocol ( vscode-webview-resource instead of http ) to load resources
* so the html needs to be manipulated dynamcially to run inside of vscode.
* Matches webpack-generated script tags like:
* <script defer="defer" src="./0.app.js"></script>
* Captures the filename as group[1].
*/
export class HtmlUtils {
/**
* This regex will match tags in a string like this
* <script defer="defer" src="./0.app.js"></script><script defer="defer" src="./app.js"></script>
* And store just the filename section of the script tag as group[1]
*/
protected static readonly scriptRegex = /script defer="defer"\ssrc="\.\/(?<app>[^"]*app.js)"/g;
const scriptRegex = /script defer="defer"\ssrc="\.\/(?<app>[^"]*app.js)"/g;

/**
*
* @param html
* @param pathToLwcDist
* @param webview
*/
public static transformHtml(html: string, lwcDistUri: URI, webview: vscode.Webview): string {
return HtmlUtils.replaceCspMetaTag(HtmlUtils.transformScriptTags(html, lwcDistUri, webview), webview);
}
/**
* Transforms index.html from @salesforce/soql-builder-ui (built for normal web context)
* to work inside VS Code, which uses vscode-webview-resource instead of http to load resources.
*/
export const transformHtml = (html: string, lwcDistUri: URI, webview: vscode.Webview): string =>
replaceCspMetaTag(transformScriptTags(html, lwcDistUri, webview), webview);

/**
* This section replaces the relative file paths that are produced by
* webpack in the build in the dist folder with the protocol that
* vscode uses internally.
*
* Initial html script tags look like this
* <script defer="defer" src="./0.app.js"></script><script defer="defer" src="./app.js"></script>
*
* Each matched script tag gets transformed into into a vscode specific url
* <script src="vscode-webview-resource:0.app.js"><script src="vscode-webview-resource:app.js">
*
* Since we don't know how many bundles webpack will produce in the dist directory, we regex match and
* replace them in a while loop.
*
* @param html
* @param pathToLwcDist
* @param webview
*/
public static transformScriptTags(html: string, lwcDistUri: URI, webview: vscode.Webview): string {
let matches: string[] | null;
let newScriptSrc: URI;
while ((matches = HtmlUtils.scriptRegex.exec(html)) !== null) {
newScriptSrc = webview.asWebviewUri(Utils.joinPath(lwcDistUri, matches[1]));
// eslint-disable-next-line no-param-reassign
html = html.replace(`./${matches[1]}`, newScriptSrc.toString());
}
return html;
/**
* Replaces relative webpack script paths with vscode-webview-resource URIs.
*
* Initial html script tags look like this
* <script defer="defer" src="./0.app.js"></script><script defer="defer" src="./app.js"></script>
*
* Each matched script tag gets transformed into a vscode specific url
* <script src="vscode-webview-resource:0.app.js"><script src="vscode-webview-resource:app.js">
*
* Since we don't know how many bundles webpack will produce in the dist directory, we regex match and
* replace them in a while loop.
*/
const transformScriptTags = (html: string, lwcDistUri: URI, webview: vscode.Webview): string => {
let matches: string[] | null;
let newScriptSrc: URI;
while ((matches = scriptRegex.exec(html)) !== null) {
newScriptSrc = webview.asWebviewUri(Utils.joinPath(lwcDistUri, matches[1]));
// eslint-disable-next-line no-param-reassign
html = html.replace(`./${matches[1]}`, newScriptSrc.toString());
}
return html;
};

/**
* This method adds stricter CSP for displaying this webview inside of VSCode
* @param html
* @param webview
*/
public static replaceCspMetaTag(html: string, webview: vscode.Webview): string {
const cspMetaTag = `<meta
/**
* Adds stricter CSP for displaying this webview inside of VSCode.
*/
export const replaceCspMetaTag = (html: string, webview: vscode.Webview): string => {
const cspMetaTag = `<meta
http-equiv="Content-Security-Policy"
content="default-src 'self';
img-src ${webview.cspSource};
script-src ${webview.cspSource};
style-src 'unsafe-inline' ${webview.cspSource};"
/>`;

return html.replace('<!-- CSP TAG -->', cspMetaTag);
}
}
return html.replace('<!-- CSP TAG -->', cspMetaTag);
};
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class SOQLEditorInstance {
protected sendMessageToUi(
type: MessageType,
payload?: string | string[] | DescribeSObjectResult
): Effect.Effect<void> {
) {
return Effect.promise<boolean>(
() => this.webviewPanel.webview.postMessage({ type, payload })
).pipe(
Expand All @@ -149,7 +149,7 @@ export class SOQLEditorInstance {
);
}

protected updateWebview(document: vscode.TextDocument): Effect.Effect<void> {
protected updateWebview(document: vscode.TextDocument) {
const self = this;
return Effect.gen(function* () {
if (self.pendingWebviewUpdate) {
Expand All @@ -160,11 +160,11 @@ export class SOQLEditorInstance {
});
}

protected updateSObjects(sobjectNames: string[]): Effect.Effect<void> {
protected updateSObjects(sobjectNames: string[]) {
return this.sendMessageToUi('sobjects_response', sobjectNames);
}

protected updateSObjectMetadata(sobject: DescribeSObjectResult): Effect.Effect<void> {
protected updateSObjectMetadata(sobject: DescribeSObjectResult) {
return this.sendMessageToUi('sobject_metadata_response', sobject);
}

Expand Down Expand Up @@ -266,7 +266,7 @@ export class SOQLEditorInstance {
}
};

protected runQueryDone(): Effect.Effect<void> {
protected runQueryDone() {
return Effect.promise<boolean>(() =>
this.webviewPanel.webview.postMessage({ type: 'run_query_done' satisfies MessageType })
).pipe(Effect.asVoid);
Expand Down Expand Up @@ -294,5 +294,5 @@ export class SOQLEditorInstance {
this.disposedCallback = callback;
}

public onConnectionChanged = (): Effect.Effect<void> => this.sendMessageToUi('connection_changed');
public onConnectionChanged = () => this.sendMessageToUi('connection_changed');
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { nls } from '../messages';
import { channelService } from '../services/channel';
import { getSoqlRuntime } from '../services/extensionProvider';
import { isDefaultOrgSet } from '../services/org';
import { HtmlUtils } from './htmlUtils';
import { transformHtml } from './htmlUtils';
import { SOQLEditorInstance } from './soqlEditorInstance';

export class SOQLEditorProvider implements vscode.CustomTextEditorProvider {
Expand Down Expand Up @@ -60,7 +60,7 @@ export class SOQLEditorProvider implements vscode.CustomTextEditorProvider {
return yield* api.services.FsService.readFile(Utils.joinPath(soqlBuilderUri, HTML_FILE));
})
);
return HtmlUtils.transformHtml(htmlContent, soqlBuilderUri, webview);
return transformHtml(htmlContent, soqlBuilderUri, webview);
}

private disposeInstance(instance: SOQLEditorInstance) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as Effect from 'effect/Effect';
import * as vscode from 'vscode';
import { Utils } from 'vscode-uri';
import { DATA_VIEW_PATH, HTML_FILE } from '../constants';
import { HtmlUtils } from '../editor/htmlUtils';
import { replaceCspMetaTag } from '../editor/htmlUtils';
import { getSoqlRuntime } from '../services/extensionProvider';

export const getHtml = async (
Expand All @@ -31,7 +31,7 @@ export const getHtml = async (
We need to replace the hrefs with webviewUris,
this will need to change once we need a standalone data view.
*/
html = HtmlUtils.replaceCspMetaTag(html, webview);
html = replaceCspMetaTag(html, webview);
html = html.replace('${tabulatorStyleUri}', tabulatorStyleUri.toString());
html = html.replace('${baseStyleUri}', baseStyleUri.toString());
html = html.replace('${tabulatorUri}', tabulatorUri.toString());
Expand Down
Loading
Loading