Skip to content

Commit

Permalink
feat: improved sfError
Browse files Browse the repository at this point in the history
feat: improved sfError
  • Loading branch information
mshanemc authored Apr 8, 2024
2 parents 02896ec + bcdaf7f commit 9b4ac85
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 69 deletions.
6 changes: 3 additions & 3 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,14 @@ export class Messages<T extends string> {
}

if (!packageName) {
const errMessage = `Invalid or missing package.json file at '${moduleMessagesDirPath}'. If not using a package.json, pass in a packageName.`;
const message = `Invalid or missing package.json file at '${moduleMessagesDirPath}'. If not using a package.json, pass in a packageName.`;
try {
packageName = asString(ensureJsonMap(Messages.readFile(path.join(moduleMessagesDirPath, 'package.json'))).name);
if (!packageName) {
throw new NamedError('MissingPackageName', errMessage);
throw SfError.create({ message, name: 'MissingPackageName' });
}
} catch (err) {
throw new NamedError('MissingPackageName', errMessage, err as Error);
throw SfError.create({ message, name: 'MissingPackageName', cause: err });
}
}

Expand Down
76 changes: 50 additions & 26 deletions src/sfError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { NamedError } from '@salesforce/kit';
import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types';

export type SfErrorOptions<T extends ErrorDataProperties = ErrorDataProperties> = {
message: string;
exitCode?: number;
name?: string;
data?: T;
/** pass an Error. For convenience in catch blocks, code will check that it is, in fact, an Error */
cause?: unknown;
context?: string;
actions?: string[];
};

type ErrorDataProperties = AnyJson;

/**
* A generalized sfdx error which also contains an action. The action is used in the
* CLI to help guide users past the error.
Expand All @@ -24,7 +36,8 @@ import { AnyJson, hasString, isString, JsonMap } from '@salesforce/ts-types';
* throw new SfError(message.getMessage('myError'), 'MyErrorName');
* ```
*/
export class SfError<T = unknown> extends NamedError {
export class SfError<T extends ErrorDataProperties = ErrorDataProperties> extends Error {
public readonly name: string;
/**
* Action messages. Hints to the users regarding what can be done to fix related issues.
*/
Expand Down Expand Up @@ -59,13 +72,17 @@ export class SfError<T = unknown> extends NamedError {
*/
public constructor(
message: string,
name?: string,
name = 'SfError',
actions?: string[],
exitCodeOrCause?: number | Error,
cause?: Error
cause?: unknown
) {
cause = exitCodeOrCause instanceof Error ? exitCodeOrCause : cause;
super(name ?? 'SfError', message || name, cause);
if (typeof cause !== 'undefined' && !(cause instanceof Error)) {
throw new TypeError(`The cause, if provided, must be an instance of Error. Received: ${typeof cause}`);
}
super(message);
this.name = name;
this.cause = exitCodeOrCause instanceof Error ? exitCodeOrCause : cause;
this.actions = actions;
if (typeof exitCodeOrCause === 'number') {
this.exitCode = exitCodeOrCause;
Expand All @@ -74,35 +91,53 @@ export class SfError<T = unknown> extends NamedError {
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
public get code(): any {
public get code(): string {
return this.#code ?? this.name;
}

public set code(code: string) {
this.#code = code;
}

/** like the constructor, but takes an typed object and let you also set context and data properties */
public static create<T extends ErrorDataProperties = ErrorDataProperties>(inputs: SfErrorOptions<T>): SfError<T> {
const error = new SfError<T>(inputs.message, inputs.name, inputs.actions, inputs.exitCode, inputs.cause);
error.data = inputs.data;
error.context = inputs.context;
return error;
}
/**
* Convert an Error to an SfError.
*
* @param err The error to convert.
*/
public static wrap(err: Error | string): SfError {
public static wrap<T extends ErrorDataProperties = ErrorDataProperties>(err: unknown): SfError<T> {
if (isString(err)) {
return new SfError(err);
return new SfError<T>(err);
}

if (err instanceof SfError) {
return err;
return err as SfError<T>;
}

const sfError = new SfError(err.message, err.name, undefined, err);
const sfError =
err instanceof Error
? // a basic error with message and name. We make it the cause to preserve any other properties
SfError.create<T>({
message: err.message,
name: err.name,
cause: err,
})
: // ok, something was throws that wasn't error or string. Convert it to an Error that preserves the information as the cause and wrap that.
SfError.wrap<T>(
new Error(`SfError.wrap received type ${typeof err} but expects type Error or string`, { cause: err })
);

// If the original error has a code, use that instead of name.
if (hasString(err, 'code')) {
sfError.code = err.code;
}

return sfError;
}

Expand Down Expand Up @@ -130,24 +165,13 @@ export class SfError<T = unknown> extends NamedError {
* Convert an {@link SfError} state to an object. Returns a plain object representing the state of this error.
*/
public toObject(): JsonMap {
const obj: JsonMap = {
return {
name: this.name,
message: this.message ?? this.name,
exitCode: this.exitCode,
actions: this.actions,
...(this.context ? { context: this.context } : {}),
...(this.data ? { data: this.data } : {}),
};

if (this.context) {
obj.context = this.context;
}

if (this.data) {
// DANGER: data was previously typed as `unknown` and this assertion was here on the toObject.
// TODO in next major release: put proper type constraint on SfError.data to something that can serialize
// while we're making breaking changes, provide a more definite type for toObject
obj.data = this.data as AnyJson;
}

return obj;
}
}
6 changes: 5 additions & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"noEmit": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true
"esModuleInterop": true,
"lib": ["ES2022"],
"module": "Node16",
"moduleResolution": "Node16",
"target": "ES2022"
}
}
4 changes: 2 additions & 2 deletions test/unit/org/scratchOrgInfoApiTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe('requestScratchOrgCreation', () => {
expect.fail('should have thrown SfError');
}
expect(error).to.exist;
expect(error).to.have.keys(['cause', 'name', 'actions', 'exitCode']);
expect(error).to.include.keys(['cause', 'name', 'actions', 'exitCode']);
expect((error as Error).toString()).to.include('SignupDuplicateSettingsSpecifiedError');
}
});
Expand All @@ -216,7 +216,7 @@ describe('requestScratchOrgCreation', () => {
expect.fail('should have thrown SfError');
}
expect(error).to.exist;
expect(error).to.have.keys(['cause', 'name', 'actions', 'exitCode']);
expect(error).to.include.keys(['cause', 'name', 'actions', 'exitCode']);
expect((error as Error).toString()).to.include(messages.getMessage('DeprecatedPrefFormat'));
}
});
Expand Down
Loading

2 comments on commit 9b4ac85

@svc-cli-bot
Copy link
Contributor

Choose a reason for hiding this comment

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

Logger Benchmarks - ubuntu-latest

Benchmark suite Current: 9b4ac85 Previous: bcdaf7f Ratio
Child logger creation 494230 ops/sec (±0.53%) 492289 ops/sec (±0.56%) 1.00
Logging a string on root logger 789451 ops/sec (±8.26%) 816996 ops/sec (±8.51%) 1.03
Logging an object on root logger 631796 ops/sec (±7.76%) 666577 ops/sec (±5.78%) 1.06
Logging an object with a message on root logger 6408 ops/sec (±212.55%) 3198 ops/sec (±243.04%) 0.50
Logging an object with a redacted prop on root logger 534718 ops/sec (±7.99%) 451213 ops/sec (±7.97%) 0.84
Logging a nested 3-level object on root logger 441250 ops/sec (±5.69%) 395124 ops/sec (±7.56%) 0.90

This comment was automatically generated by workflow using github-action-benchmark.

@svc-cli-bot
Copy link
Contributor

Choose a reason for hiding this comment

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

Logger Benchmarks - windows-latest

Benchmark suite Current: 9b4ac85 Previous: bcdaf7f Ratio
Child logger creation 314216 ops/sec (±0.63%) 318422 ops/sec (±0.82%) 1.01
Logging a string on root logger 744861 ops/sec (±6.74%) 781695 ops/sec (±6.74%) 1.05
Logging an object on root logger 577009 ops/sec (±4.69%) 500200 ops/sec (±8.62%) 0.87
Logging an object with a message on root logger 9255 ops/sec (±198.42%) 11076 ops/sec (±189.18%) 1.20
Logging an object with a redacted prop on root logger 514069 ops/sec (±5.33%) 429626 ops/sec (±5.41%) 0.84
Logging a nested 3-level object on root logger 340272 ops/sec (±6.48%) 295788 ops/sec (±6.91%) 0.87

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.