@api-client/core 0.19.16 → 0.19.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/exceptions/exception.d.ts +56 -0
- package/build/src/exceptions/exception.d.ts.map +1 -1
- package/build/src/exceptions/exception.js +111 -14
- package/build/src/exceptions/exception.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/exceptions/exception.ts +119 -15
- package/tests/unit/exceptions/exception.spec.ts +261 -0
package/package.json
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A serialized representation of an Exception.
|
|
3
|
+
* Provides a standardized structure for pure data objects,
|
|
4
|
+
* typically used when converting errors for JSON responses.
|
|
5
|
+
*/
|
|
1
6
|
export interface ExceptionSchema {
|
|
2
7
|
name: string
|
|
3
8
|
message: string
|
|
@@ -75,39 +80,49 @@ export class Exception extends Error {
|
|
|
75
80
|
*/
|
|
76
81
|
static fromRawException(init: object, defaultMessage: string): Exception {
|
|
77
82
|
const typed = init as Record<string, string | number>
|
|
78
|
-
const options: ErrorOptions & { code?: string; status?: number } = {}
|
|
79
|
-
if (typed.code) {
|
|
80
|
-
options.code = typed.code
|
|
83
|
+
const options: ErrorOptions & { code?: string; status?: number; help?: string } = {}
|
|
84
|
+
if (typed.code !== undefined) {
|
|
85
|
+
options.code = String(typed.code)
|
|
81
86
|
}
|
|
82
|
-
if (typed.status) {
|
|
83
|
-
|
|
87
|
+
if (typed.status !== undefined) {
|
|
88
|
+
const parsedStatus = Number(typed.status)
|
|
89
|
+
if (!isNaN(parsedStatus)) {
|
|
90
|
+
options.status = parsedStatus
|
|
91
|
+
}
|
|
84
92
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (typed.help) {
|
|
88
|
-
result.setHelp(typed.help as string)
|
|
93
|
+
if (typed.help !== undefined) {
|
|
94
|
+
options.help = String(typed.help)
|
|
89
95
|
}
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
const message = typed.message !== undefined ? String(typed.message) : defaultMessage
|
|
97
|
+
const result = new this(message, options)
|
|
98
|
+
if (typed.name !== undefined) {
|
|
99
|
+
result.name = String(typed.name)
|
|
92
100
|
}
|
|
93
101
|
return result
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Initializes a new Exception instance.
|
|
106
|
+
*
|
|
107
|
+
* @param message - The primary human-readable error message.
|
|
108
|
+
* @param options - Additional properties configuration. Accepts standard `ErrorOptions`
|
|
109
|
+
* (like `cause`), plus custom `code`, `status`, and `help`.
|
|
110
|
+
*/
|
|
96
111
|
constructor(message?: string, options?: ErrorOptions & { code?: string; status?: number; help?: string }) {
|
|
97
112
|
super(message, options)
|
|
98
113
|
|
|
99
114
|
const ErrorConstructor = this.constructor as typeof Exception
|
|
100
115
|
|
|
101
116
|
this.name = ErrorConstructor.name
|
|
102
|
-
this.message = message
|
|
103
|
-
this.status = options?.status
|
|
117
|
+
this.message = message ?? ErrorConstructor.message ?? ''
|
|
118
|
+
this.status = options?.status ?? ErrorConstructor.status ?? 500
|
|
104
119
|
|
|
105
|
-
const code = options?.code
|
|
120
|
+
const code = options?.code ?? ErrorConstructor.code
|
|
106
121
|
if (code !== undefined) {
|
|
107
122
|
this.code = code
|
|
108
123
|
}
|
|
109
124
|
|
|
110
|
-
const help = options?.help
|
|
125
|
+
const help = options?.help ?? ErrorConstructor.help
|
|
111
126
|
if (help !== undefined) {
|
|
112
127
|
this.help = help
|
|
113
128
|
}
|
|
@@ -115,25 +130,53 @@ export class Exception extends Error {
|
|
|
115
130
|
Error.captureStackTrace(this, ErrorConstructor)
|
|
116
131
|
}
|
|
117
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Assigns a human-readable help description for troubleshooting and returns
|
|
135
|
+
* the exception instance to allow for method chaining.
|
|
136
|
+
*
|
|
137
|
+
* @param help - The detailed help informational text.
|
|
138
|
+
* @returns The current exception instance.
|
|
139
|
+
*/
|
|
118
140
|
setHelp(help: string): this {
|
|
119
141
|
this.help = help
|
|
120
142
|
return this
|
|
121
143
|
}
|
|
122
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Assigns a programmatic error code and returns the exception instance
|
|
147
|
+
* to allow for method chaining.
|
|
148
|
+
*
|
|
149
|
+
* @param code - The string error code (e.g., 'E_FILE_NOT_FOUND').
|
|
150
|
+
* @returns The current exception instance.
|
|
151
|
+
*/
|
|
123
152
|
setCode(code: string): this {
|
|
124
153
|
this.code = code
|
|
125
154
|
return this
|
|
126
155
|
}
|
|
127
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Assigns an HTTP-style status code and returns the exception instance
|
|
159
|
+
* to allow for method chaining.
|
|
160
|
+
*
|
|
161
|
+
* @param status - The numeric status code (e.g., 404, 500).
|
|
162
|
+
* @returns The current exception instance.
|
|
163
|
+
*/
|
|
128
164
|
setStatus(status: number): this {
|
|
129
165
|
this.status = status
|
|
130
166
|
return this
|
|
131
167
|
}
|
|
132
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Provides the string tag used by `Object.prototype.toString`.
|
|
171
|
+
*/
|
|
133
172
|
get [Symbol.toStringTag]() {
|
|
134
173
|
return this.constructor.name
|
|
135
174
|
}
|
|
136
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Computes a string representation of the exception, including the
|
|
178
|
+
* programmatic code if one is defined.
|
|
179
|
+
*/
|
|
137
180
|
override toString(): string {
|
|
138
181
|
if (this.code) {
|
|
139
182
|
return `${this.name} [${this.code}]: ${this.message}`
|
|
@@ -141,6 +184,12 @@ export class Exception extends Error {
|
|
|
141
184
|
return `${this.name}: ${this.message}`
|
|
142
185
|
}
|
|
143
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Serializes the exception into a plain JavaScript object.
|
|
189
|
+
* This ensures safe passing across boundaries and correct format in JSON responses.
|
|
190
|
+
*
|
|
191
|
+
* @returns The serialized exception object matching `ExceptionSchema`.
|
|
192
|
+
*/
|
|
144
193
|
toJSON(): ExceptionSchema {
|
|
145
194
|
const result: ExceptionSchema = {
|
|
146
195
|
name: this.name,
|
|
@@ -158,3 +207,58 @@ export class Exception extends Error {
|
|
|
158
207
|
return result
|
|
159
208
|
}
|
|
160
209
|
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Converts a standard Error object (or thrown primitive) into an Exception instance.
|
|
213
|
+
* This function extracts relevant properties from the raw error
|
|
214
|
+
* and sets them on the Exception instance, allowing for a more
|
|
215
|
+
* consistent error handling experience across the application.
|
|
216
|
+
* @param error The unknown error value to convert.
|
|
217
|
+
* @param defaults Optional default properties to apply if they are missing on the Error object.
|
|
218
|
+
* @returns An Exception instance with properties set from the Error object.
|
|
219
|
+
*/
|
|
220
|
+
export function fromError(error: unknown, defaults: Partial<ExceptionSchema> = {}): Exception {
|
|
221
|
+
let message: string | undefined
|
|
222
|
+
let typed: Record<string, string | number> = {}
|
|
223
|
+
|
|
224
|
+
if (error instanceof Error) {
|
|
225
|
+
message = error.message
|
|
226
|
+
typed = error as unknown as Record<string, string | number>
|
|
227
|
+
} else if (typeof error === 'string') {
|
|
228
|
+
message = error
|
|
229
|
+
} else if (error !== null && typeof error === 'object') {
|
|
230
|
+
typed = error as Record<string, string | number>
|
|
231
|
+
if (typeof typed.message === 'string') {
|
|
232
|
+
message = typed.message
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const e = new Exception(message || defaults.message || 'An error occurred')
|
|
237
|
+
|
|
238
|
+
if (typed.code !== undefined) {
|
|
239
|
+
e.code = String(typed.code)
|
|
240
|
+
} else if (defaults.code !== undefined) {
|
|
241
|
+
e.code = defaults.code
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (typed.status !== undefined) {
|
|
245
|
+
const status = Number(typed.status)
|
|
246
|
+
e.status = isNaN(status) ? (defaults.status ?? 500) : status
|
|
247
|
+
} else if (defaults.status !== undefined) {
|
|
248
|
+
e.status = defaults.status
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (typed.help !== undefined) {
|
|
252
|
+
e.setHelp(String(typed.help))
|
|
253
|
+
} else if (defaults.help !== undefined) {
|
|
254
|
+
e.setHelp(defaults.help)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typed.name !== undefined) {
|
|
258
|
+
e.name = String(typed.name)
|
|
259
|
+
} else if (defaults.name !== undefined) {
|
|
260
|
+
e.name = defaults.name
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return e
|
|
264
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { Exception, fromError } from '../../../src/exceptions/exception.js'
|
|
3
|
+
|
|
4
|
+
test.group('Exceptions > Exception', () => {
|
|
5
|
+
test('constructor sets basic properties', ({ assert }) => {
|
|
6
|
+
const ex = new Exception('Test message', { code: 'E_TEST', status: 400, help: 'Help me' })
|
|
7
|
+
assert.equal(ex.message, 'Test message')
|
|
8
|
+
assert.equal(ex.code, 'E_TEST')
|
|
9
|
+
assert.equal(ex.status, 400)
|
|
10
|
+
assert.equal(ex.help, 'Help me')
|
|
11
|
+
assert.equal(ex.name, 'Exception')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('constructor uses static defaults', ({ assert }) => {
|
|
15
|
+
class CustomException extends Exception {
|
|
16
|
+
static override message = 'Static message'
|
|
17
|
+
static override code = 'E_STATIC'
|
|
18
|
+
static override status = 404
|
|
19
|
+
static override help = 'Static help'
|
|
20
|
+
}
|
|
21
|
+
const ex = new CustomException()
|
|
22
|
+
assert.equal(ex.message, 'Static message')
|
|
23
|
+
assert.equal(ex.code, 'E_STATIC')
|
|
24
|
+
assert.equal(ex.status, 404)
|
|
25
|
+
assert.equal(ex.help, 'Static help')
|
|
26
|
+
assert.equal(ex.name, 'CustomException')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('constructor defaults status to 500', ({ assert }) => {
|
|
30
|
+
const ex = new Exception('Test')
|
|
31
|
+
assert.equal(ex.status, 500)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('constructor accepts status 0', ({ assert }) => {
|
|
35
|
+
const ex = new Exception('Test', { status: 0 })
|
|
36
|
+
assert.equal(ex.status, 0)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('setters allow chaining and update properties', ({ assert }) => {
|
|
40
|
+
const ex = new Exception('Test')
|
|
41
|
+
const result = ex.setCode('E_CHAIN').setStatus(401).setHelp('Chained')
|
|
42
|
+
|
|
43
|
+
assert.strictEqual(result, ex)
|
|
44
|
+
assert.equal(ex.code, 'E_CHAIN')
|
|
45
|
+
assert.equal(ex.status, 401)
|
|
46
|
+
assert.equal(ex.help, 'Chained')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('toString returns properly formatted string', ({ assert }) => {
|
|
50
|
+
const ex1 = new Exception('Error without code')
|
|
51
|
+
assert.equal(ex1.toString(), 'Exception: Error without code')
|
|
52
|
+
|
|
53
|
+
const ex2 = new Exception('Error with code', { code: 'E_CODE' })
|
|
54
|
+
assert.equal(ex2.toString(), 'Exception [E_CODE]: Error with code')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('toStringTag returns class name', ({ assert }) => {
|
|
58
|
+
const ex = new Exception('Test')
|
|
59
|
+
assert.equal(Object.prototype.toString.call(ex), '[object Exception]')
|
|
60
|
+
|
|
61
|
+
class CustomTagError extends Exception {}
|
|
62
|
+
const ex2 = new CustomTagError('Test')
|
|
63
|
+
assert.equal(Object.prototype.toString.call(ex2), '[object CustomTagError]')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('toJSON serializes properties', ({ assert }) => {
|
|
67
|
+
const ex = new Exception('Test message', { code: 'E_TEST', status: 400, help: 'Help me' })
|
|
68
|
+
const json = ex.toJSON()
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(json, {
|
|
71
|
+
name: 'Exception',
|
|
72
|
+
message: 'Test message',
|
|
73
|
+
code: 'E_TEST',
|
|
74
|
+
status: 400,
|
|
75
|
+
help: 'Help me',
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('toJSON does not output unset optional properties', ({ assert }) => {
|
|
80
|
+
const ex = new Exception('Basic')
|
|
81
|
+
const json = ex.toJSON()
|
|
82
|
+
|
|
83
|
+
assert.deepEqual(json, {
|
|
84
|
+
name: 'Exception',
|
|
85
|
+
message: 'Basic',
|
|
86
|
+
status: 500,
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('fromRawException reconstructs exception', ({ assert }) => {
|
|
91
|
+
const raw = {
|
|
92
|
+
message: 'Raw message',
|
|
93
|
+
code: 'E_RAW',
|
|
94
|
+
status: '403',
|
|
95
|
+
help: 'Raw help',
|
|
96
|
+
name: 'RawName',
|
|
97
|
+
}
|
|
98
|
+
const ex = Exception.fromRawException(raw, 'Default')
|
|
99
|
+
|
|
100
|
+
assert.instanceOf(ex, Exception)
|
|
101
|
+
assert.equal(ex.message, 'Raw message')
|
|
102
|
+
assert.equal(ex.code, 'E_RAW')
|
|
103
|
+
assert.equal(ex.status, 403)
|
|
104
|
+
assert.equal(ex.help, 'Raw help')
|
|
105
|
+
assert.equal(ex.name, 'RawName')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('fromRawException uses default message if missing', ({ assert }) => {
|
|
109
|
+
const ex = Exception.fromRawException({}, 'Fallback message')
|
|
110
|
+
assert.equal(ex.message, 'Fallback message')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test.group('Exceptions > Exception > fromError()', () => {
|
|
115
|
+
test('creates Exception from standard Error', ({ assert }) => {
|
|
116
|
+
const err = new Error('standard error message')
|
|
117
|
+
const ex = fromError(err)
|
|
118
|
+
|
|
119
|
+
assert.instanceOf(ex, Exception)
|
|
120
|
+
assert.equal(ex.message, 'standard error message')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('creates Exception from Error with no message', ({ assert }) => {
|
|
124
|
+
const err = new Error()
|
|
125
|
+
const ex = fromError(err)
|
|
126
|
+
|
|
127
|
+
assert.instanceOf(ex, Exception)
|
|
128
|
+
assert.equal(ex.message, 'An error occurred')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('copies code property', ({ assert }) => {
|
|
132
|
+
const err = new Error('test') as unknown as Exception
|
|
133
|
+
err.code = 'TEST_CODE'
|
|
134
|
+
const ex = fromError(err)
|
|
135
|
+
|
|
136
|
+
assert.equal(ex.code, 'TEST_CODE')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('copies status property', ({ assert }) => {
|
|
140
|
+
const err = new Error('test') as unknown as Exception
|
|
141
|
+
err.status = 404
|
|
142
|
+
const ex = fromError(err)
|
|
143
|
+
|
|
144
|
+
assert.equal(ex.status, 404)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('copies help property', ({ assert }) => {
|
|
148
|
+
const err = new Error('test') as unknown as Exception
|
|
149
|
+
err.help = 'Help text'
|
|
150
|
+
const ex = fromError(err)
|
|
151
|
+
|
|
152
|
+
assert.equal(ex.help, 'Help text')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('copies name property', ({ assert }) => {
|
|
156
|
+
const err = new Error('test')
|
|
157
|
+
err.name = 'CustomErrorName'
|
|
158
|
+
const ex = fromError(err)
|
|
159
|
+
|
|
160
|
+
assert.equal(ex.name, 'CustomErrorName')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('handles when object is already an Exception instance', ({ assert }) => {
|
|
164
|
+
const originalEx = new Exception('Original exception')
|
|
165
|
+
originalEx.code = 'ORIGINAL_CODE'
|
|
166
|
+
originalEx.status = 400
|
|
167
|
+
originalEx.setHelp('Original help')
|
|
168
|
+
|
|
169
|
+
const newEx = fromError(originalEx)
|
|
170
|
+
|
|
171
|
+
assert.instanceOf(newEx, Exception)
|
|
172
|
+
assert.equal(newEx.message, 'Original exception')
|
|
173
|
+
assert.equal(newEx.code, 'ORIGINAL_CODE')
|
|
174
|
+
assert.equal(newEx.status, 400)
|
|
175
|
+
assert.equal(newEx.help, 'Original help')
|
|
176
|
+
assert.equal(newEx.name, 'Exception')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('uses default message when error has no message', ({ assert }) => {
|
|
180
|
+
const err = new Error()
|
|
181
|
+
err.message = ''
|
|
182
|
+
const ex = fromError(err, { message: 'Default message' })
|
|
183
|
+
|
|
184
|
+
assert.equal(ex.message, 'Default message')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('error message takes precedence over default message', ({ assert }) => {
|
|
188
|
+
const err = new Error('Original message')
|
|
189
|
+
const ex = fromError(err, { message: 'Default message' })
|
|
190
|
+
|
|
191
|
+
assert.equal(ex.message, 'Original message')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('uses default code when error has no code', ({ assert }) => {
|
|
195
|
+
const err = new Error('test')
|
|
196
|
+
const ex = fromError(err, { code: 'DEFAULT_CODE' })
|
|
197
|
+
|
|
198
|
+
assert.equal(ex.code, 'DEFAULT_CODE')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('error code takes precedence over default code', ({ assert }) => {
|
|
202
|
+
const err = new Error('test') as unknown as Exception
|
|
203
|
+
err.code = 'ORIGINAL_CODE'
|
|
204
|
+
const ex = fromError(err, { code: 'DEFAULT_CODE' })
|
|
205
|
+
|
|
206
|
+
assert.equal(ex.code, 'ORIGINAL_CODE')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('uses default status when error has no status', ({ assert }) => {
|
|
210
|
+
const err = new Error('test')
|
|
211
|
+
const ex = fromError(err, { status: 401 })
|
|
212
|
+
|
|
213
|
+
assert.equal(ex.status, 401)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('uses default status when error status is invalid', ({ assert }) => {
|
|
217
|
+
const err = new Error('test') as unknown as Exception
|
|
218
|
+
err.status = 'invalid' as unknown as number
|
|
219
|
+
const ex = fromError(err, { status: 403 })
|
|
220
|
+
|
|
221
|
+
assert.equal(ex.status, 403)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('error status takes precedence over default status', ({ assert }) => {
|
|
225
|
+
const err = new Error('test') as unknown as Exception
|
|
226
|
+
err.status = 404
|
|
227
|
+
const ex = fromError(err, { status: 401 })
|
|
228
|
+
|
|
229
|
+
assert.equal(ex.status, 404)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('uses default help when error has no help', ({ assert }) => {
|
|
233
|
+
const err = new Error('test')
|
|
234
|
+
const ex = fromError(err, { help: 'Default help text' })
|
|
235
|
+
|
|
236
|
+
assert.equal(ex.help, 'Default help text')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('error help takes precedence over default help', ({ assert }) => {
|
|
240
|
+
const err = new Error('test') as unknown as Exception
|
|
241
|
+
err.help = 'Original help text'
|
|
242
|
+
const ex = fromError(err, { help: 'Default help text' })
|
|
243
|
+
|
|
244
|
+
assert.equal(ex.help, 'Original help text')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('uses default name when error has no name', ({ assert }) => {
|
|
248
|
+
const err = { message: 'test' } as unknown as Error
|
|
249
|
+
const ex = fromError(err, { name: 'DefaultName' })
|
|
250
|
+
|
|
251
|
+
assert.equal(ex.name, 'DefaultName')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('error name takes precedence over default name', ({ assert }) => {
|
|
255
|
+
const err = new Error('test')
|
|
256
|
+
err.name = 'OriginalName'
|
|
257
|
+
const ex = fromError(err, { name: 'DefaultName' })
|
|
258
|
+
|
|
259
|
+
assert.equal(ex.name, 'OriginalName')
|
|
260
|
+
})
|
|
261
|
+
})
|