/
sfError.ts
192 lines (172 loc) · 5.62 KB
/
sfError.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { AnyJson, hasString, isString } 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;
type SfErrorToObjectResult = {
name: string;
message: string;
exitCode: number;
actions?: string[];
context?: string;
data?: ErrorDataProperties;
};
/**
* A generalized sfdx error which also contains an action. The action is used in the
* CLI to help guide users past the error.
*
* To throw an error in a synchronous function you must either pass the error message and actions
* directly to the constructor, e.g.
*
* ```
* // To load a message bundle (Note that __dirname should contain a messages folder)
* Messages.importMessagesDirectory(__dirname);
* const messages = Messages.load('myPackageName', 'myBundleName');
*
* // To throw a non-bundle based error:
* throw new SfError(message.getMessage('myError'), 'MyErrorName');
* ```
*/
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.
*/
public actions?: string[];
/**
* SfdxCommand can return this process exit code.
*/
public exitCode: number;
/**
* The related context for this error.
*/
public context?: string;
// Additional data helpful for consumers of this error. E.g., API call result
public data?: T;
/**
* Some errors support `error.code` instead of `error.name`. This keeps backwards compatability.
*/
#code?: string;
/**
* Create an SfError.
*
* @param message The error message.
* @param name The error name. Defaults to 'SfError'.
* @param actions The action message(s).
* @param exitCodeOrCause The exit code which will be used by SfdxCommand or he underlying error that caused this error to be raised.
* @param cause The underlying error that caused this error to be raised.
*/
public constructor(
message: string,
name = 'SfError',
actions?: string[],
exitCodeOrCause?: number | Error,
cause?: unknown
) {
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;
if (actions?.length) {
this.actions = actions;
}
if (typeof exitCodeOrCause === 'number') {
this.exitCode = exitCodeOrCause;
} else {
this.exitCode = 1;
}
}
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);
if (inputs.data) {
error.data = inputs.data;
}
if (inputs.context) {
error.context = inputs.context;
}
return error;
}
/**
* Convert an Error to an SfError.
*
* @param err The error to convert.
*/
public static wrap<T extends ErrorDataProperties = ErrorDataProperties>(err: unknown): SfError<T> {
if (isString(err)) {
return new SfError<T>(err);
}
if (err instanceof SfError) {
return err as SfError<T>;
}
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;
}
/**
* Sets the context of the error. For convenience `this` object is returned.
*
* @param context The command name.
*/
public setContext(context: string): SfError {
this.context = context;
return this;
}
/**
* An additional payload for the error. For convenience `this` object is returned.
*
* @param data The payload data.
*/
public setData(data: T): SfError {
this.data = data;
return this;
}
/**
* Convert an {@link SfError} state to an object. Returns a plain object representing the state of this error.
*/
public toObject(): SfErrorToObjectResult {
return {
name: this.name,
message: this.message ?? this.name,
exitCode: this.exitCode,
...(this.actions?.length ? { actions: this.actions } : {}),
...(this.context ? { context: this.context } : {}),
...(this.data ? { data: this.data } : {}),
};
}
}