Skip to content

Commit 5d193cf

Browse files
fix: pretty-print MULTIPLE_API_ERROR data (#677)
* fix: add logic to handle multiple errors returned from api call * fix: reduce the scope to only jsforce error MULTIPLE_API_ERRORS * chore: typo * chore: remove TS assertions (#680) --------- Co-authored-by: Cristian Dominguez <6853656+cristiand391@users.noreply.github.com>
1 parent 4b066ca commit 5d193cf

File tree

4 files changed

+96
-46
lines changed

4 files changed

+96
-46
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545
"dependencies": {
4646
"@inquirer/confirm": "^3.1.22",
4747
"@inquirer/password": "^2.2.0",
48-
"@oclif/core": "^4.2.10",
48+
"@oclif/core": "^4.3.0",
4949
"@oclif/table": "^0.4.6",
50-
"@salesforce/core": "^8.8.5",
50+
"@salesforce/core": "^8.10.0",
5151
"@salesforce/kit": "^3.2.3",
5252
"@salesforce/ts-types": "^2.0.12",
5353
"ansis": "^3.3.2",

src/errorFormatting.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
import { inspect } from 'node:util';
99
import type { Ansis } from 'ansis';
10-
import { Mode, Messages, envVars } from '@salesforce/core';
10+
import { Mode, Messages, envVars, SfError } from '@salesforce/core';
11+
import { AnyJson, ensureString, isAnyJson } from '@salesforce/ts-types';
1112
import { StandardColors } from './ux/standardColors.js';
1213
import { SfCommandError } from './SfCommandError.js';
14+
import { Ux } from './ux/ux.js';
1315

1416
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1517
const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages');
@@ -44,6 +46,7 @@ export const formatActions = (
4446
export const formatError = (error: SfCommandError): string =>
4547
[
4648
`${formatErrorPrefix(error)} ${error.message}`,
49+
formatMultipleErrorMessages(error),
4750
...formatActions(error.actions ?? []),
4851
error.stack && envVars.getString('SF_ENV') === Mode.DEVELOPMENT
4952
? StandardColors.info(`\n*** Internal Diagnostic ***\n\n${inspect(error)}\n******\n`)
@@ -55,3 +58,55 @@ const formatErrorPrefix = (error: SfCommandError): string =>
5558

5659
const formatErrorCode = (error: SfCommandError): string =>
5760
typeof error.code === 'string' || typeof error.code === 'number' ? ` (${error.code})` : '';
61+
62+
type JsforceApiError = {
63+
errorCode: string;
64+
message?: AnyJson;
65+
};
66+
67+
const isJsforceApiError = (item: AnyJson): item is JsforceApiError =>
68+
typeof item === 'object' && item !== null && !Array.isArray(item) && ('errorCode' in item || 'message' in item);
69+
70+
const formatMultipleErrorMessages = (error: SfCommandError): string => {
71+
if (error.code === 'MULTIPLE_API_ERRORS' && error.cause) {
72+
const errorData = getErrorData(error.cause);
73+
if (errorData && Array.isArray(errorData) && errorData.length > 0) {
74+
const errors = errorData.filter(isJsforceApiError).map((d) => ({
75+
errorCode: d.errorCode,
76+
message: ensureString(d.message ?? ''),
77+
}));
78+
79+
const ux = new Ux();
80+
return ux.makeTable({
81+
data: errors,
82+
columns: [
83+
{ key: 'errorCode', name: 'Error Code' },
84+
{ key: 'message', name: 'Message' },
85+
],
86+
});
87+
}
88+
}
89+
return '';
90+
};
91+
92+
/**
93+
* Utility function to extract error data from an error object.
94+
* Recursively traverses the error chain to find the first error that contains data.
95+
* Returns undefined if no error in the chain contains data or if the input is not an Error/SfError.
96+
*
97+
* This is used in the top-level catch in sfCommand for deeply-nested error data.
98+
*
99+
* @param error - The error object to extract data from
100+
* @returns The error data if found, undefined otherwise
101+
*/
102+
const getErrorData = (error: unknown): AnyJson | undefined => {
103+
if (!(error instanceof Error || error instanceof SfError)) return undefined;
104+
105+
if ('data' in error && error.data && isAnyJson(error.data)) {
106+
return error.data;
107+
} else if (error.cause) {
108+
return getErrorData(error.cause);
109+
} else {
110+
return undefined;
111+
}
112+
};

test/unit/errorFormatting.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,28 @@ describe('errorFormatting.formatError()', () => {
7979
expect(errorOutput).to.contain('warnings: undefined');
8080
expect(errorOutput).to.contain('result: undefined');
8181
});
82+
83+
it('should have correct output for multiple errors in table format when errorCode is MULTIPLE_API_ERRORS', () => {
84+
const innerError = SfError.create({
85+
message: 'foo',
86+
data: [
87+
{ errorCode: 'ERROR_1', message: 'error 1' },
88+
{ errorCode: 'ERROR_2', message: 'error 2' },
89+
],
90+
});
91+
const sfError = SfError.create({
92+
name: 'myError',
93+
message: 'foo',
94+
actions: ['bar'],
95+
context: 'myContext',
96+
exitCode: 8,
97+
cause: innerError,
98+
});
99+
const err = SfCommandError.from(sfError, 'thecommand');
100+
err.code = 'MULTIPLE_API_ERRORS';
101+
const errorOutput = formatError(err);
102+
expect(errorOutput).to.match(/Error Code.+Message/);
103+
expect(errorOutput).to.match(/ERROR_1.+error 1/);
104+
expect(errorOutput).to.match(/ERROR_2.+error 2/);
105+
});
82106
});

yarn.lock

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -519,10 +519,10 @@
519519
"@jridgewell/resolve-uri" "^3.1.0"
520520
"@jridgewell/sourcemap-codec" "^1.4.14"
521521

522-
"@jsforce/jsforce-node@^3.6.5":
523-
version "3.6.6"
524-
resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.6.6.tgz#26fe2fc9f4f3671ca8bfb0c98a728cb8a0219265"
525-
integrity sha512-WdIo2lLbrz6nkfiaz2UynyaNiM8o+fEjaRev7zA4KKSaQYB1MJ66xHubeI5Iheq8WgkY9XGwWKAwPDhuV+GROQ==
522+
"@jsforce/jsforce-node@^3.6.5", "@jsforce/jsforce-node@^3.8.1":
523+
version "3.8.1"
524+
resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.8.1.tgz#482fcf2820b48a6b10930d33550eb4e4cbd1e480"
525+
integrity sha512-+IZZC7VfNjhkTyeAspBc4Z35Y5eAP0RIWQnyfKahsbY/aLjiFRIM9ejl1YbWbrbabf5ODFSUgBGmOiEYLW3f7Q==
526526
dependencies:
527527
"@sindresorhus/is" "^4"
528528
base64url "^3.0.1"
@@ -556,10 +556,10 @@
556556
"@nodelib/fs.scandir" "2.1.5"
557557
fastq "^1.6.0"
558558

559-
"@oclif/core@^4.2.10":
560-
version "4.2.10"
561-
resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.2.10.tgz#31dfb7481c79887c3e672e10c981fcc01fcbaeb3"
562-
integrity sha512-fAqcXgqkUm4v5FYy7qWP4w1HaOlVSVJveah+yVTo5Nm5kTiXhmD5mQQ7+knGeBaStyrtQy6WardoC2xSic9rlQ==
559+
"@oclif/core@^4.3.0":
560+
version "4.3.0"
561+
resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.3.0.tgz#9a2951f05f81a4c7ae5ffcc00b2d720cca0898e6"
562+
integrity sha512-lIzHY+JMP6evrS5E/sGijNnwrCoNtGy8703jWXcMuPOYKiFhWoAqnIm1BGgoRgmxczkbSfRsHUL/lwsSgh74Lw==
563563
dependencies:
564564
ansi-escapes "^4.3.2"
565565
ansis "^3.17.0"
@@ -608,36 +608,12 @@
608608
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
609609
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
610610

611-
"@salesforce/core@^8.8.3":
612-
version "8.8.4"
613-
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.8.4.tgz#1995d387a62c48810b4e0a66c68192ccdcf8001f"
614-
integrity sha512-TKioMWh/QWmXjnD0bMNvFHEqaH175TCYWNXUCO1CixdhhI0p1MQj4AnQsk8wuRJiH5kVhiYw6Z7NukcIxLtbUw==
611+
"@salesforce/core@^8.10.0", "@salesforce/core@^8.8.3":
612+
version "8.10.3"
613+
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.10.3.tgz#3cc2c99d097757cb4b08dab921254cfa3a00c7c1"
614+
integrity sha512-juqbU304TBrrjb8sZGw+QkeAJISKu4+v2XIMTCxGJoEjs4LLhsyI7/drxCUY+7FNye+veAGeJdn/PCxkKhSgcA==
615615
dependencies:
616-
"@jsforce/jsforce-node" "^3.6.5"
617-
"@salesforce/kit" "^3.2.2"
618-
"@salesforce/schemas" "^1.9.0"
619-
"@salesforce/ts-types" "^2.0.10"
620-
ajv "^8.17.1"
621-
change-case "^4.1.2"
622-
fast-levenshtein "^3.0.0"
623-
faye "^1.4.0"
624-
form-data "^4.0.0"
625-
js2xmlparser "^4.0.1"
626-
jsonwebtoken "9.0.2"
627-
jszip "3.10.1"
628-
pino "^9.4.0"
629-
pino-abstract-transport "^1.2.0"
630-
pino-pretty "^11.2.2"
631-
proper-lockfile "^4.1.2"
632-
semver "^7.6.3"
633-
ts-retry-promise "^0.8.1"
634-
635-
"@salesforce/core@^8.8.5":
636-
version "8.8.5"
637-
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.8.5.tgz#4004e22523e4e45b42631733d06dc8aec9fdc23d"
638-
integrity sha512-eCiiO4NptvKkz04A4ivBVLzEBy/6IIFmaXoZ4tnF1FcD5MESvC+Xuc+0RFSRiYmPi5oloKNl6njrfVCKAho2zQ==
639-
dependencies:
640-
"@jsforce/jsforce-node" "^3.6.5"
616+
"@jsforce/jsforce-node" "^3.8.1"
641617
"@salesforce/kit" "^3.2.2"
642618
"@salesforce/schemas" "^1.9.0"
643619
"@salesforce/ts-types" "^2.0.10"
@@ -1143,12 +1119,7 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1:
11431119
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
11441120
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
11451121

1146-
ansis@^3.10.0, ansis@^3.3.2:
1147-
version "3.10.0"
1148-
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.10.0.tgz#6886afb0f729b1fa865df6b710b97a9915b7d0d4"
1149-
integrity sha512-hxDKLYT7hy3Y4sF3HxI926A3urzPxi73mZBB629m9bCVF+NyKNxbwCqqm+C/YrGPtxLwnl6d8/ZASCsz6SyvJA==
1150-
1151-
ansis@^3.17.0:
1122+
ansis@^3.10.0, ansis@^3.17.0, ansis@^3.3.2:
11521123
version "3.17.0"
11531124
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
11541125
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==

0 commit comments

Comments
 (0)