Skip to content

Commit 985e5e7

Browse files
fix: add logic to handle multiple errors returned from api call
1 parent 4b066ca commit 985e5e7

File tree

5 files changed

+119
-3
lines changed

5 files changed

+119
-3
lines changed

src/SfCommandError.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { inspect } from 'node:util';
88
import { SfError, StructuredMessage } from '@salesforce/core';
99
import { AnyJson } from '@salesforce/ts-types';
10-
import { computeErrorCode } from './errorHandling.js';
10+
import { computeErrorCode, computeErrorData } from './errorHandling.js';
1111

1212
// These types are 90% the same as SfErrorOptions (but they aren't exported to extend)
1313
type ErrorDataProperties = AnyJson;
@@ -75,7 +75,7 @@ export class SfCommandError extends SfError {
7575
code: 'code' in err && err.code ? err.code : exitCode.toString(10),
7676
cause: sfError.cause,
7777
commandName: 'commandName' in err ? err.commandName : commandName,
78-
data: 'data' in err ? err.data : undefined,
78+
data: computeErrorData(err),
7979
result: 'result' in err ? err.result : undefined,
8080
context: 'context' in err ? err.context : commandName,
8181
warnings,

src/errorFormatting.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Ansis } from 'ansis';
1010
import { Mode, Messages, envVars } from '@salesforce/core';
1111
import { StandardColors } from './ux/standardColors.js';
1212
import { SfCommandError } from './SfCommandError.js';
13+
import { Ux } from './ux/ux.js';
1314

1415
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1516
const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages');
@@ -44,6 +45,7 @@ export const formatActions = (
4445
export const formatError = (error: SfCommandError): string =>
4546
[
4647
`${formatErrorPrefix(error)} ${error.message}`,
48+
...formatMultipleErrorMessages(error),
4749
...formatActions(error.actions ?? []),
4850
error.stack && envVars.getString('SF_ENV') === Mode.DEVELOPMENT
4951
? StandardColors.info(`\n*** Internal Diagnostic ***\n\n${inspect(error)}\n******\n`)
@@ -55,3 +57,25 @@ const formatErrorPrefix = (error: SfCommandError): string =>
5557

5658
const formatErrorCode = (error: SfCommandError): string =>
5759
typeof error.code === 'string' || typeof error.code === 'number' ? ` (${error.code})` : '';
60+
61+
const formatMultipleErrorMessages = (error: SfCommandError): string[] => {
62+
if (!error.data || !Array.isArray(error.data) || error.data.length === 0) {
63+
return [];
64+
}
65+
66+
const errorData = error.data.map((d) => ({
67+
errorCode: (d as { errorCode: string }).errorCode || '',
68+
message: (d as { message: string }).message || '',
69+
}));
70+
71+
const ux = new Ux();
72+
return [
73+
ux.makeTable({
74+
data: errorData,
75+
columns: [
76+
{ key: 'errorCode', name: 'Error Code' },
77+
{ key: 'message', name: 'Message' },
78+
],
79+
}),
80+
];
81+
};

src/errorHandling.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { SfError } from '@salesforce/core/sfError';
99
import { Errors } from '@oclif/core';
10+
import { AnyJson } from '@salesforce/ts-types';
1011

1112
/**
1213
*
@@ -44,6 +45,27 @@ export const computeErrorCode = (e: Error | SfError | Errors.CLIError): number =
4445
return typeof process.exitCode === 'number' ? process.exitCode : 1;
4546
};
4647

48+
/**
49+
* Computes and extracts error data from different error types.
50+
*
51+
* 1. If the error has a 'data' property with a value, returns that data
52+
* 2. If the error has a 'cause' property with a value:
53+
* - If the cause has a 'data' property, returns cause.data
54+
* - If not, returns undefined
55+
* 3. If none of the above conditions are met, returns undefined
56+
*
57+
* @param e - The error object to extract data from. Can be a standard Error, SfError, or CLIError
58+
* @returns The extracted data as AnyJson or undefined if no data is found
59+
*/
60+
export const computeErrorData = (e: Error | SfError | Errors.CLIError): AnyJson | undefined =>
61+
'data' in e && e.data
62+
? e.data
63+
: 'cause' in e && e.cause
64+
? 'data' in (e.cause as { data: AnyJson | undefined })
65+
? (e.cause as { data: AnyJson | undefined }).data
66+
: undefined
67+
: undefined;
68+
4769
/** identifies gacks via regex. Searches the error message, stack, and recursively checks the cause chain */
4870
export const errorIsGack = (error: Error | SfError): boolean => {
4971
/** see test for samples */

test/unit/errorFormatting.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,23 @@ 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 ', () => {
84+
const sfError = SfError.create({
85+
name: 'myError',
86+
message: 'foo',
87+
actions: ['bar'],
88+
context: 'myContext',
89+
exitCode: 8,
90+
data: [
91+
{ errorCode: 'ERROR_1', message: 'error 1' },
92+
{ errorCode: 'ERROR_2', message: 'error 2' },
93+
],
94+
});
95+
const err = SfCommandError.from(sfError, 'thecommand');
96+
const errorOutput = formatError(err);
97+
expect(errorOutput).to.match(/Error Code.+Message/);
98+
expect(errorOutput).to.match(/ERROR_1.+error 1/);
99+
expect(errorOutput).to.match(/ERROR_2.+error 2/);
100+
});
82101
});

test/unit/errorHandling.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
*/
77
import { expect } from 'chai';
88
import { SfError } from '@salesforce/core/sfError';
9-
import { computeErrorCode, errorIsGack, errorIsTypeError } from '../../src/errorHandling.js';
9+
import { AnyJson } from '@salesforce/ts-types';
10+
import { Errors } from '@oclif/core';
11+
import { computeErrorCode, computeErrorData, errorIsGack, errorIsTypeError } from '../../src/errorHandling.js';
1012
import { SfCommandError } from '../../src/SfCommandError.js';
1113

1214
describe('typeErrors', () => {
@@ -197,3 +199,52 @@ describe('SfCommandError.toJson()', () => {
197199
});
198200
});
199201
});
202+
203+
describe('computeErrorData', () => {
204+
interface ErrorWithData extends Error {
205+
data?: AnyJson;
206+
}
207+
208+
it('should return data from error.data when present', () => {
209+
const sfError = SfError.create({
210+
name: 'myError',
211+
message: 'foo',
212+
actions: ['bar'],
213+
context: 'myContext',
214+
exitCode: 8,
215+
data: { foo: 'bar' },
216+
});
217+
expect(computeErrorData(sfError)).to.deep.equal({ foo: 'bar' });
218+
});
219+
220+
it('should return cause.data when error.data is not present but cause.data is', () => {
221+
const sfError = SfError.create({
222+
name: 'myError',
223+
message: 'foo',
224+
actions: ['bar'],
225+
context: 'myContext',
226+
exitCode: 8,
227+
});
228+
const err: ErrorWithData = { name: 'testError', message: 'baz', data: { foo: 'baz' } };
229+
sfError.cause = err;
230+
expect(computeErrorData(sfError)).to.deep.equal({ foo: 'baz' });
231+
});
232+
233+
it('should return undefined when no data or cause is present', () => {
234+
const error = new Error('test error') as ErrorWithData;
235+
expect(computeErrorData(error)).to.be.undefined;
236+
});
237+
238+
it('should handle SfError with data', () => {
239+
const error = new SfError('test error', 'TestError', [], 1, undefined);
240+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
241+
(error as any).data = { foo: 'bar' };
242+
expect(computeErrorData(error)).to.deep.equal({ foo: 'bar' });
243+
});
244+
245+
it('should handle CLIError with data', () => {
246+
const err = new Errors.CLIError('Nonexistent flag: --INVALID\nSee more help with --help') as ErrorWithData;
247+
err.data = { foo: 'bar' };
248+
expect(computeErrorData(err)).to.deep.equal({ foo: 'bar' });
249+
});
250+
});

0 commit comments

Comments
 (0)