Skip to content
Open
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
7 changes: 4 additions & 3 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
"alias": ["force:apex:execute"],
"command": "apex:run",
"flagAliases": ["apexcodefile", "apiversion", "targetusername", "u"],
"flagChars": ["f", "o"],
"flags": ["api-version", "file", "flags-dir", "json", "loglevel", "target-org"],
"flagChars": ["d", "f", "o"],
"flags": ["api-version", "category-level", "debug-level", "file", "flags-dir", "json", "loglevel", "target-org"],
"plugin": "@salesforce/plugin-apex"
},
{
Expand Down Expand Up @@ -78,8 +78,8 @@
"flags-dir",
"json",
"loglevel",
"poll-interval",
"output-dir",
"poll-interval",
"result-format",
"suite-names",
"synchronous",
Expand All @@ -99,6 +99,7 @@
"plugin": "@salesforce/plugin-apex"
},
{
"alias": [],
"command": "logic:get:test",
"flagAliases": [
"apiversion",
Expand Down
36 changes: 36 additions & 0 deletions messages/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ For more information, see "Anonymous Blocks" in the Apex Developer Guide.

Path to a local file that contains Apex code.

# flags.debug-level.summary

Debug level to use for the returned debug log.

# flags.debug-level.description

Sets the debug log level for the anonymous Apex execution. Valid values are NONE, DEBUGONLY, DB, PROFILING, CALLOUT, and DETAIL. Defaults to DEBUGONLY if not specified. Mutually exclusive with --category-level.

# flags.category-level.summary

Set the log level for a specific log category (format: Category=Level). Can be specified multiple times.

# flags.category-level.description

Set individual log category levels for fine-grained control over the debug log. Format: Category=Level (e.g., Apex_code=FINEST). Valid categories: Db, Workflow, Validation, Callout, Apex_code, Apex_profiling, Visualforce, System, Wave, Nba, All. Valid levels: NONE, ERROR, WARN, INFO, DEBUG, FINE, FINER, FINEST. Can be specified multiple times. Mutually exclusive with --debug-level.

# invalidCategoryLevel

Invalid --category-level format "%s". Use Category=Level (e.g., Apex_code=FINEST).

# invalidCategory

Invalid category "%s". Valid categories: %s

# invalidCategoryLevelValue

Invalid level "%s". Valid levels: %s

# examples

- Execute the Apex code that's in the ~/test.apex file in the org with the specified username:
Expand All @@ -27,6 +55,14 @@ Path to a local file that contains Apex code.

<%= config.bin %> <%= command.id %>

- Execute with maximum debug log detail:

<%= config.bin %> <%= command.id %> --file ~/test.apex --debug-level DETAIL

- Execute with fine-grained control over specific log categories:

<%= config.bin %> <%= command.id %> --file ~/test.apex --category-level Apex_code=FINEST --category-level Db=FINE

# executeCompileSuccess

Compiled successfully.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"enableO11y": true,
"o11yUploadEndpoint": "https://794testsite.my.site.com/byolwr/webruntime/log/metrics",
"dependencies": {
"@salesforce/apex-node": "^9.0.9",
"@salesforce/apex-node": "^9.1.0",
"@salesforce/core": "^9.1.10",
"@salesforce/kit": "^4.0.0",
"@salesforce/sf-plugins-core": "^13.0.4",
Expand Down
53 changes: 52 additions & 1 deletion src/commands/apex/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
* limitations under the License.
*/

import { ApexExecuteOptions, ExecuteService } from '@salesforce/apex-node';
import {
ApexExecuteOptions,
ExecuteService,
LOG_TYPES,
LOG_CATEGORIES,
LOG_CATEGORY_LEVELS,
type DebugCategory,
type LogType,
type LogCategory,
type LogCategoryLevel,
} from '@salesforce/apex-node';
import {
Flags,
loglevel,
Expand All @@ -28,6 +38,25 @@ import RunReporter from '../../reporters/runReporter.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-apex', 'run');

function parseCategoryLevel(input: string): DebugCategory {
const eqIndex = input.indexOf('=');
if (eqIndex === -1) {
throw new SfError(messages.getMessage('invalidCategoryLevel', [input]));
}
const cat = input.slice(0, eqIndex).trim();
const lvl = input.slice(eqIndex + 1).trim();
if (!cat || !lvl) {
throw new SfError(messages.getMessage('invalidCategoryLevel', [input]));
}
if (!LOG_CATEGORIES.includes(cat as LogCategory)) {
throw new SfError(messages.getMessage('invalidCategory', [cat, LOG_CATEGORIES.join(', ')]));
}
if (!LOG_CATEGORY_LEVELS.includes(lvl as LogCategoryLevel)) {
throw new SfError(messages.getMessage('invalidCategoryLevelValue', [lvl, LOG_CATEGORY_LEVELS.join(', ')]));
}
return { category: cat as LogCategory, level: lvl as LogCategoryLevel };
}

export type ExecuteResult = {
compiled: boolean;
success: boolean;
Expand Down Expand Up @@ -56,15 +85,37 @@ export default class Run extends SfCommand<ExecuteResult> {
char: 'f',
summary: messages.getMessage('flags.file.summary'),
}),
'debug-level': Flags.option({
char: 'd',
summary: messages.getMessage('flags.debug-level.summary'),
description: messages.getMessage('flags.debug-level.description'),
options: [...LOG_TYPES] as LogType[],
exclusive: ['category-level'],
})(),
'category-level': Flags.string({
summary: messages.getMessage('flags.category-level.summary'),
description: messages.getMessage('flags.category-level.description'),
multiple: true,
exclusive: ['debug-level'],
}),
};

public async run(): Promise<ExecuteResult> {
const { flags } = await this.parse(Run);
const conn = flags['target-org'].getConnection(flags['api-version']);
const exec = new ExecuteService(conn);

const debugCategories = flags['category-level']?.map(parseCategoryLevel).reduce<DebugCategory[]>((acc, entry) => {
const idx = acc.findIndex((e) => e.category === entry.category);
if (idx >= 0) acc[idx] = entry;
else acc.push(entry);
return acc;
}, []);

const execAnonOptions: ApexExecuteOptions = {
...(flags.file ? { apexFilePath: flags.file } : { userInput: true }),
...(flags['debug-level'] ? { debugLevel: flags['debug-level'] } : {}),
...(debugCategories?.length ? { debugCategories } : {}),
};

const result = await exec.executeAnonymous(execAnonOptions);
Expand Down
84 changes: 84 additions & 0 deletions test/commands/apex/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,90 @@ describe('apex:execute', () => {
]);
});

it('passes debug-level flag to executeAnonymous', async () => {
const file = join('Users', 'test', 'path', 'to', 'file');
const executeServiceStub = sandboxStub
.stub(ExecuteService.prototype, 'executeAnonymous')
.resolves({ compiled: true, success: true, logs: log });

await Run.run(['--file', file, '--debug-level', 'DETAIL']);

expect(executeServiceStub.args[0]).to.deep.equal([
{
apexFilePath: file,
debugLevel: 'DETAIL',
},
]);
});

it('passes category-level flags to executeAnonymous', async () => {
const file = join('Users', 'test', 'path', 'to', 'file');
const executeServiceStub = sandboxStub
.stub(ExecuteService.prototype, 'executeAnonymous')
.resolves({ compiled: true, success: true, logs: log });

await Run.run(['--file', file, '--category-level', 'Apex_code=FINEST', '--category-level', 'Db=FINE']);

expect(executeServiceStub.args[0]).to.deep.equal([
{
apexFilePath: file,
debugCategories: [
{ category: 'Apex_code', level: 'FINEST' },
{ category: 'Db', level: 'FINE' },
],
},
]);
});

it('throws on invalid category-level format', async () => {
sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: true, success: true });

try {
await Run.run(['--file', 'test.apex', '--category-level', 'bad-format']);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.include('Invalid --category-level format');
}
});

it('throws on invalid category name', async () => {
sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: true, success: true });

try {
await Run.run(['--file', 'test.apex', '--category-level', 'FakeCategory=FINEST']);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.include('Invalid category');
}
});

it('throws on invalid category level value', async () => {
sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({ compiled: true, success: true });

try {
await Run.run(['--file', 'test.apex', '--category-level', 'Apex_code=INVALID']);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.include('Invalid level');
}
});

it('deduplicates category-level entries with last-wins', async () => {
const file = join('Users', 'test', 'path', 'to', 'file');
const executeServiceStub = sandboxStub
.stub(ExecuteService.prototype, 'executeAnonymous')
.resolves({ compiled: true, success: true, logs: log });

await Run.run(['--file', file, '--category-level', 'Apex_code=DEBUG', '--category-level', 'Apex_code=FINEST']);

expect(executeServiceStub.args[0]).to.deep.equal([
{
apexFilePath: file,
debugCategories: [{ category: 'Apex_code', level: 'FINEST' }],
},
]);
});

it('throws an error when it fails to compile', async () => {
sandboxStub.stub(ExecuteService.prototype, 'executeAnonymous').resolves({
compiled: false,
Expand Down
25 changes: 10 additions & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1011,9 +1011,9 @@
"@jridgewell/sourcemap-codec" "^1.4.14"

"@jsforce/jsforce-node@^3.10.17":
version "3.10.19"
resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.10.19.tgz#ccbc539c12f4f7dff9cfdcc6cfb8f07bd840f731"
integrity sha512-k7i2Tntu1fLvkMtRcKDFU64/Fr2M692ECtbwIGX6hcOh5mj+jrMa1tlvcdwffxAMl+lPYCXnY2bjErxWmP84zA==
version "3.10.25"
resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.10.25.tgz#19a457a8d0b5217188576507139134cfae836eef"
integrity sha512-L4GfTOzmGBypjG1O7aTxwbk4DTKitU7yvnnqQE7wypFyEyHH9VR6cVZxi4+Gfk7cmItHRHAV7MyBTFfrP5DaHA==
dependencies:
"@sindresorhus/is" "^4"
base64url "^3.0.1"
Expand Down Expand Up @@ -1222,12 +1222,12 @@
"@pnpm/network.ca-file" "^1.0.1"
config-chain "^1.1.11"

"@salesforce/apex-node@^9.0.9":
version "9.0.9"
resolved "https://registry.yarnpkg.com/@salesforce/apex-node/-/apex-node-9.0.9.tgz#70b9d711aa77c5147cd31ab129509cc773b90175"
integrity sha512-rMLFlf/QQvKa6WBct7ClGmklMde2qt7Wv8AmpfkIMLD5p0F8ZIFYvPha7nnY1R2cO4FZHIO+q3a0qFaBsMZsVw==
"@salesforce/apex-node@^9.1.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@salesforce/apex-node/-/apex-node-9.1.0.tgz#419407f9b179251be81429bb5b8d629dc2dede1c"
integrity sha512-MY5iEWrWMJV7Rn5yxhYMFQFTsQ9uxEOSiU5RHO8aIWbEARkQ7aiev4Hy55nBxSkE9LuKQMHNZLFwiAvHfuj1Zw==
dependencies:
"@salesforce/core" "^9.1.7"
"@salesforce/core" "^9.1.10"
"@salesforce/kit" "^4.0.0"
"@types/istanbul-reports" "^3.0.4"
fast-glob "^3.3.2"
Expand Down Expand Up @@ -1278,7 +1278,7 @@
ts-retry-promise "^0.8.1"
zod "^4.1.12"

"@salesforce/core@^9.1.10", "@salesforce/core@^9.1.4", "@salesforce/core@^9.1.7", "@salesforce/core@^9.1.9":
"@salesforce/core@^9.1.10", "@salesforce/core@^9.1.4", "@salesforce/core@^9.1.9":
version "9.1.10"
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-9.1.10.tgz#a3a01455c33821f5f4af4f85ba09e4b18f55d194"
integrity sha512-0BRMIUHU21jOhK93tNEt9jtXaFnhwoNuRZynnVuuaNIogF5b2X8L6GYRXTqBHYPtX/MHqpKvAVMs6oMj3LcIpw==
Expand Down Expand Up @@ -1398,12 +1398,7 @@
resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-2.0.12.tgz#60420622812a7ec7e46d220667bc29b42dc247ff"
integrity sha512-BIJyduJC18Kc8z+arUm5AZ9VkPRyw1KKAm+Tk+9LT99eOzhNilyfKzhZ4t+tG2lIGgnJpmytZfVDZ0e2kFul8g==

"@salesforce/ts-types@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-3.0.0.tgz#375b13e92f594bc9d0a09716d77fc753bd31e6a6"
integrity sha512-lTCoI1NtQWiMDyn7B9N/CfWK9WmSpRnbYWqKTZYstn3mZ2DWSYwJXTh5Oh2wAAS9LAD/vaoU3F/TrV3q7wfnYw==

"@salesforce/ts-types@^3.2.0":
"@salesforce/ts-types@^3.0.0", "@salesforce/ts-types@^3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@salesforce/ts-types/-/ts-types-3.2.0.tgz#9e0393cab10d763b89562f0f94515e3cf1efbbe7"
integrity sha512-3xdKt4nlthDC8nCHVWpVDwM5mmPlk8kDyJ1E3dWMk7FLRdQqhLgaHZgS+AMqcowWiKUC1l1d5YIhGhQM7dw3Bw==
Expand Down
Loading