@devp0nt/error0 1.0.0-next.4 → 1.0.0-next.40
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/dist/cjs/index.cjs +379 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +177 -415
- package/dist/esm/index.d.ts +177 -415
- package/dist/esm/index.js +301 -347
- package/dist/esm/index.js.map +1 -1
- package/package.json +45 -22
- package/src/index.test.ts +315 -478
- package/src/index.ts +637 -506
- package/dist/cjs/index.js +0 -435
- package/dist/cjs/index.js.map +0 -1
package/src/index.test.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'bun:test'
|
|
2
|
+
import type { ClassError0 } from './index.js'
|
|
3
|
+
import { Error0 } from './index.js'
|
|
3
4
|
import z, { ZodError } from 'zod'
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
// TODO: test expected
|
|
5
|
+
import * as assert from 'node:assert'
|
|
7
6
|
|
|
8
7
|
const fixStack = (stack: string | undefined) => {
|
|
9
8
|
if (!stack) {
|
|
10
9
|
return stack
|
|
11
10
|
}
|
|
12
|
-
// at <anonymous> (/Users/
|
|
13
|
-
//
|
|
11
|
+
// at <anonymous> (/Users/x/error0.test.ts:103:25)
|
|
12
|
+
// ↓
|
|
14
13
|
// at <anonymous> (...)
|
|
15
14
|
const lines = stack.split('\n')
|
|
16
15
|
const fixedLines = lines.map((line) => {
|
|
@@ -20,522 +19,360 @@ const fixStack = (stack: string | undefined) => {
|
|
|
20
19
|
return fixedLines.join('\n')
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
"__I_AM_ERROR_0": true,
|
|
37
|
-
"anyMessage": undefined,
|
|
38
|
-
"cause": undefined,
|
|
39
|
-
"clientMessage": undefined,
|
|
40
|
-
"code": undefined,
|
|
41
|
-
"expected": false,
|
|
42
|
-
"httpStatus": undefined,
|
|
43
|
-
"message": "test",
|
|
44
|
-
"meta": {},
|
|
45
|
-
"stack":
|
|
46
|
-
"Error0: test
|
|
47
|
-
at <anonymous> (...)"
|
|
48
|
-
,
|
|
49
|
-
"tag": undefined,
|
|
50
|
-
}
|
|
51
|
-
`)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('full', () => {
|
|
55
|
-
const input = {
|
|
56
|
-
message: 'my message',
|
|
57
|
-
tag: 'tag1',
|
|
58
|
-
code: 'code1',
|
|
59
|
-
httpStatus: 400,
|
|
60
|
-
expected: true,
|
|
61
|
-
clientMessage: 'human message 1',
|
|
62
|
-
cause: new Error('original message'),
|
|
63
|
-
meta: {
|
|
64
|
-
reqDurationMs: 1,
|
|
65
|
-
userId: 'user1',
|
|
22
|
+
describe('Error0', () => {
|
|
23
|
+
const statusExtension = Error0.extension()
|
|
24
|
+
.prop('status', {
|
|
25
|
+
input: (value: number) => value,
|
|
26
|
+
output: (error) => {
|
|
27
|
+
for (const value of error.flow('status')) {
|
|
28
|
+
const status = Number(value)
|
|
29
|
+
if (!Number.isNaN(status)) {
|
|
30
|
+
return status
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
66
34
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"reqDurationMs": 1,
|
|
83
|
-
"userId": "user1",
|
|
84
|
-
},
|
|
85
|
-
"stack":
|
|
86
|
-
"Error0: my message
|
|
87
|
-
at <anonymous> (...)
|
|
88
|
-
|
|
89
|
-
Error: original message
|
|
90
|
-
at <anonymous> (...)"
|
|
91
|
-
,
|
|
92
|
-
"tag": "tag1",
|
|
93
|
-
}
|
|
94
|
-
`)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('cause error default', () => {
|
|
98
|
-
const errorDefault = new Error('original message')
|
|
99
|
-
const error0 = new Error0('my message', { cause: errorDefault })
|
|
100
|
-
expect(error0).toBeInstanceOf(Error0)
|
|
101
|
-
expect(error0).toMatchInlineSnapshot(`[Error0: my message]`)
|
|
102
|
-
expect(toJSON(error0)).toMatchInlineSnapshot(`
|
|
103
|
-
{
|
|
104
|
-
"__I_AM_ERROR_0": true,
|
|
105
|
-
"anyMessage": undefined,
|
|
106
|
-
"cause": [Error: original message],
|
|
107
|
-
"clientMessage": undefined,
|
|
108
|
-
"code": undefined,
|
|
109
|
-
"expected": false,
|
|
110
|
-
"httpStatus": undefined,
|
|
111
|
-
"message": "my message",
|
|
112
|
-
"meta": {},
|
|
113
|
-
"stack":
|
|
114
|
-
"Error0: my message
|
|
115
|
-
at <anonymous> (...)
|
|
116
|
-
|
|
117
|
-
Error: original message
|
|
118
|
-
at <anonymous> (...)"
|
|
119
|
-
,
|
|
120
|
-
"tag": undefined,
|
|
35
|
+
serialize: (value) => value,
|
|
36
|
+
})
|
|
37
|
+
.method('isStatus', (error, status: number) => error.status === status)
|
|
38
|
+
|
|
39
|
+
const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
|
|
40
|
+
type Code = (typeof codes)[number]
|
|
41
|
+
const codeExtension = Error0.extension().extend('prop', 'code', {
|
|
42
|
+
input: (value: Code) => value,
|
|
43
|
+
output: (error) => {
|
|
44
|
+
for (const value of error.flow('code')) {
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
if (codes.includes(value as Code)) {
|
|
47
|
+
return value as Code
|
|
48
|
+
}
|
|
49
|
+
}
|
|
121
50
|
}
|
|
122
|
-
|
|
51
|
+
return undefined
|
|
52
|
+
},
|
|
53
|
+
serialize: (value, error, isPublic) => (isPublic ? undefined : value),
|
|
123
54
|
})
|
|
124
55
|
|
|
125
|
-
it('
|
|
126
|
-
const
|
|
127
|
-
expect(
|
|
128
|
-
expect(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"code": undefined,
|
|
135
|
-
"expected": false,
|
|
136
|
-
"httpStatus": undefined,
|
|
137
|
-
"message": "my message",
|
|
138
|
-
"meta": {},
|
|
139
|
-
"stack":
|
|
140
|
-
"Error0: my message
|
|
56
|
+
it('simple', () => {
|
|
57
|
+
const error = new Error0('test')
|
|
58
|
+
expect(error).toBeInstanceOf(Error0)
|
|
59
|
+
expect(error).toBeInstanceOf(Error)
|
|
60
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
61
|
+
expect(error.message).toBe('test')
|
|
62
|
+
expect(error.stack).toBeDefined()
|
|
63
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
64
|
+
"Error0: test
|
|
141
65
|
at <anonymous> (...)"
|
|
142
|
-
,
|
|
143
|
-
"tag": undefined,
|
|
144
|
-
}
|
|
145
66
|
`)
|
|
146
67
|
})
|
|
147
68
|
|
|
148
|
-
it('
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
code: 'code2',
|
|
160
|
-
cause: error01,
|
|
161
|
-
meta: {
|
|
162
|
-
reqDurationMs: 1,
|
|
163
|
-
ideaId: 'idea1',
|
|
164
|
-
other: {
|
|
165
|
-
x: 1,
|
|
166
|
-
},
|
|
69
|
+
it('with direct prop extension', () => {
|
|
70
|
+
const AppError = Error0.extend('prop', 'status', {
|
|
71
|
+
input: (value: number) => value,
|
|
72
|
+
output: (error: Error0) => {
|
|
73
|
+
for (const value of error.flow('status')) {
|
|
74
|
+
const status = Number(value)
|
|
75
|
+
if (!Number.isNaN(status)) {
|
|
76
|
+
return status
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined
|
|
167
80
|
},
|
|
81
|
+
serialize: (value: number | undefined) => value,
|
|
168
82
|
})
|
|
169
|
-
|
|
170
|
-
expect(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"httpStatus": undefined,
|
|
179
|
-
"message": "second",
|
|
180
|
-
"meta": {
|
|
181
|
-
"ideaId": "idea1",
|
|
182
|
-
"other": {
|
|
183
|
-
"x": 1,
|
|
184
|
-
},
|
|
185
|
-
"reqDurationMs": 1,
|
|
186
|
-
"userId": "user1",
|
|
187
|
-
},
|
|
188
|
-
"stack":
|
|
189
|
-
"Error0: second
|
|
190
|
-
at <anonymous> (...)
|
|
191
|
-
|
|
192
|
-
Error0: first
|
|
83
|
+
const error = new AppError('test', { status: 400 })
|
|
84
|
+
expect(error).toBeInstanceOf(AppError)
|
|
85
|
+
expect(error).toBeInstanceOf(Error0)
|
|
86
|
+
expect(error).toBeInstanceOf(Error)
|
|
87
|
+
expect(error.status).toBe(400)
|
|
88
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
89
|
+
expect(error.stack).toBeDefined()
|
|
90
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
91
|
+
"Error0: test
|
|
193
92
|
at <anonymous> (...)"
|
|
194
|
-
,
|
|
195
|
-
"tag": "tag2",
|
|
196
|
-
}
|
|
197
93
|
`)
|
|
94
|
+
expectTypeOf<typeof AppError>().toExtend<ClassError0>()
|
|
198
95
|
})
|
|
199
96
|
|
|
200
|
-
it('
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"message": "Unknown error",
|
|
212
|
-
"meta": {},
|
|
213
|
-
"stack":
|
|
214
|
-
"Error0: Unknown error
|
|
215
|
-
at <anonymous> (...)"
|
|
216
|
-
,
|
|
217
|
-
"tag": undefined,
|
|
218
|
-
}
|
|
219
|
-
`)
|
|
220
|
-
const error1 = new Error0('test')
|
|
221
|
-
expect(error1.message).toBe('test')
|
|
222
|
-
const error2 = new Error0({ cause: error1 })
|
|
223
|
-
expect(toJSON(error2)).toMatchInlineSnapshot(`
|
|
224
|
-
{
|
|
225
|
-
"__I_AM_ERROR_0": true,
|
|
226
|
-
"anyMessage": undefined,
|
|
227
|
-
"cause": [Error0: test],
|
|
228
|
-
"clientMessage": undefined,
|
|
229
|
-
"code": undefined,
|
|
230
|
-
"expected": false,
|
|
231
|
-
"httpStatus": undefined,
|
|
232
|
-
"message": "Unknown error",
|
|
233
|
-
"meta": {},
|
|
234
|
-
"stack":
|
|
235
|
-
"Error0: Unknown error
|
|
236
|
-
at <anonymous> (...)
|
|
237
|
-
|
|
238
|
-
Error0: test
|
|
239
|
-
at <anonymous> (...)"
|
|
240
|
-
,
|
|
241
|
-
"tag": undefined,
|
|
242
|
-
}
|
|
243
|
-
`)
|
|
244
|
-
expect(fixStack(error2.stack)).toMatchInlineSnapshot(`
|
|
245
|
-
"Error0: Unknown error
|
|
246
|
-
at <anonymous> (...)
|
|
247
|
-
|
|
248
|
-
Error0: test
|
|
97
|
+
it('with defined extension', () => {
|
|
98
|
+
const AppError = Error0.extend(statusExtension)
|
|
99
|
+
const error = new AppError('test', { status: 400 })
|
|
100
|
+
expect(error).toBeInstanceOf(AppError)
|
|
101
|
+
expect(error).toBeInstanceOf(Error0)
|
|
102
|
+
expect(error).toBeInstanceOf(Error)
|
|
103
|
+
expect(error.status).toBe(400)
|
|
104
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
105
|
+
expect(error.stack).toBeDefined()
|
|
106
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
107
|
+
"Error0: test
|
|
249
108
|
at <anonymous> (...)"
|
|
250
109
|
`)
|
|
251
110
|
})
|
|
252
111
|
|
|
253
|
-
it('
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"stack":
|
|
268
|
-
"Error0: Unknown error
|
|
269
|
-
at <anonymous> (...)
|
|
270
|
-
|
|
271
|
-
Error: default error
|
|
272
|
-
at <anonymous> (...)"
|
|
273
|
-
,
|
|
274
|
-
"tag": undefined,
|
|
275
|
-
}
|
|
276
|
-
`)
|
|
277
|
-
expect(fixStack(error0.stack)).toMatchInlineSnapshot(`
|
|
278
|
-
"Error0: Unknown error
|
|
279
|
-
at <anonymous> (...)
|
|
280
|
-
|
|
281
|
-
Error: default error
|
|
282
|
-
at <anonymous> (...)"
|
|
283
|
-
`)
|
|
112
|
+
it('twice extended Error0 extends previous by types', () => {
|
|
113
|
+
const AppError1 = Error0.extend(statusExtension)
|
|
114
|
+
const AppError2 = AppError1.extend(codeExtension)
|
|
115
|
+
const error1 = new AppError1('test', { status: 400 })
|
|
116
|
+
const error2 = new AppError2('test', { status: 400, code: 'NOT_FOUND' })
|
|
117
|
+
expect(error1.status).toBe(400)
|
|
118
|
+
expect(error2.status).toBe(400)
|
|
119
|
+
expect(error2.code).toBe('NOT_FOUND')
|
|
120
|
+
expectTypeOf<typeof error2.status>().toEqualTypeOf<number | undefined>()
|
|
121
|
+
expectTypeOf<typeof error2.code>().toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
|
|
122
|
+
expectTypeOf<typeof AppError1>().toExtend<ClassError0>()
|
|
123
|
+
expectTypeOf<typeof AppError2>().toExtend<ClassError0>()
|
|
124
|
+
expectTypeOf<typeof AppError2>().toExtend<typeof AppError1>()
|
|
125
|
+
expectTypeOf<typeof AppError1>().not.toExtend<typeof AppError2>()
|
|
284
126
|
})
|
|
285
127
|
|
|
286
|
-
it('
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"code": undefined,
|
|
296
|
-
"expected": false,
|
|
297
|
-
"httpStatus": undefined,
|
|
298
|
-
"message": "Unknown error",
|
|
299
|
-
"meta": {},
|
|
300
|
-
"stack":
|
|
301
|
-
"Error0: Unknown error
|
|
302
|
-
at <anonymous> (...)
|
|
303
|
-
|
|
304
|
-
Error0: error0 error
|
|
305
|
-
at <anonymous> (...)"
|
|
306
|
-
,
|
|
307
|
-
"tag": undefined,
|
|
308
|
-
}
|
|
309
|
-
`)
|
|
310
|
-
expect(fixStack(error0.stack)).toMatchInlineSnapshot(`
|
|
311
|
-
"Error0: Unknown error
|
|
312
|
-
at <anonymous> (...)
|
|
313
|
-
|
|
314
|
-
Error0: error0 error
|
|
128
|
+
it('can have cause', () => {
|
|
129
|
+
const AppError = Error0.extend(statusExtension)
|
|
130
|
+
const anotherError = new Error('another error')
|
|
131
|
+
const error = new AppError('test', { status: 400, cause: anotherError })
|
|
132
|
+
expect(error.status).toBe(400)
|
|
133
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
134
|
+
expect(error.stack).toBeDefined()
|
|
135
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
136
|
+
"Error0: test
|
|
315
137
|
at <anonymous> (...)"
|
|
316
138
|
`)
|
|
139
|
+
expect(Error0.causes(error)).toEqual([error, anotherError])
|
|
317
140
|
})
|
|
318
141
|
|
|
319
|
-
it('
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
tag: 'tag2',
|
|
328
|
-
code: 'code2',
|
|
329
|
-
cause: error01,
|
|
330
|
-
})
|
|
331
|
-
expect(fixStack(errorDefault.stack)).toMatchInlineSnapshot(`
|
|
332
|
-
"Error: default error
|
|
333
|
-
at <anonymous> (...)"
|
|
334
|
-
`)
|
|
335
|
-
expect(fixStack(error01.stack)).toMatchInlineSnapshot(`
|
|
336
|
-
"Error0: first
|
|
337
|
-
at <anonymous> (...)
|
|
338
|
-
|
|
339
|
-
Error: default error
|
|
340
|
-
at <anonymous> (...)"
|
|
341
|
-
`)
|
|
342
|
-
expect(fixStack(error02.stack)).toMatchInlineSnapshot(`
|
|
343
|
-
"Error0: second
|
|
344
|
-
at <anonymous> (...)
|
|
345
|
-
|
|
346
|
-
Error0: first
|
|
347
|
-
at <anonymous> (...)
|
|
348
|
-
|
|
349
|
-
Error: default error
|
|
350
|
-
at <anonymous> (...)"
|
|
351
|
-
`)
|
|
142
|
+
it('can have many causes', () => {
|
|
143
|
+
const AppError = Error0.extend(statusExtension)
|
|
144
|
+
const anotherError = new Error('another error')
|
|
145
|
+
const error1 = new AppError('test1', { status: 400, cause: anotherError })
|
|
146
|
+
const error2 = new AppError('test2', { status: 400, cause: error1 })
|
|
147
|
+
expect(error1.status).toBe(400)
|
|
148
|
+
expect(error2.status).toBe(400)
|
|
149
|
+
expect(Error0.causes(error2)).toEqual([error2, error1, anotherError])
|
|
352
150
|
})
|
|
353
151
|
|
|
354
|
-
it('
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
})
|
|
358
|
-
|
|
152
|
+
it('properties floating', () => {
|
|
153
|
+
const AppError = Error0.extend(statusExtension).extend(codeExtension)
|
|
154
|
+
const anotherError = new Error('another error')
|
|
155
|
+
const error1 = new AppError('test1', { status: 400, cause: anotherError })
|
|
156
|
+
const error2 = new AppError('test2', { code: 'NOT_FOUND', cause: error1 })
|
|
157
|
+
expect(error1.status).toBe(400)
|
|
158
|
+
expect(error1.code).toBe(undefined)
|
|
159
|
+
expect(error2.status).toBe(400)
|
|
160
|
+
expect(error2.code).toBe('NOT_FOUND')
|
|
161
|
+
expect(Error0.causes(error2)).toEqual([error2, error1, anotherError])
|
|
162
|
+
})
|
|
359
163
|
|
|
360
|
-
|
|
361
|
-
|
|
164
|
+
it('serialize uses identity by default and skips undefined extension values', () => {
|
|
165
|
+
const AppError = Error0.extend(statusExtension).extend('prop', 'code', {
|
|
166
|
+
input: (value: string) => value,
|
|
167
|
+
output: (error) => {
|
|
168
|
+
for (const value of error.flow('code')) {
|
|
169
|
+
if (typeof value === 'string') {
|
|
170
|
+
return value
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined
|
|
174
|
+
},
|
|
175
|
+
serialize: () => undefined,
|
|
362
176
|
})
|
|
363
|
-
|
|
177
|
+
const error = new AppError('test', { status: 401, code: 'secret' })
|
|
178
|
+
const json = AppError.serialize(error) as Record<string, unknown>
|
|
179
|
+
expect(json.status).toBe(401)
|
|
180
|
+
expect('code' in json).toBe(false)
|
|
181
|
+
})
|
|
364
182
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
expect(
|
|
183
|
+
it('serialize keeps stack by default without stack extension', () => {
|
|
184
|
+
const AppError = Error0.extend(statusExtension)
|
|
185
|
+
const error = new AppError('test', { status: 500 })
|
|
186
|
+
const json = AppError.serialize(error) as Record<string, unknown>
|
|
187
|
+
expect(json.stack).toBe(error.stack)
|
|
188
|
+
})
|
|
370
189
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
190
|
+
it('stack extension can customize serialization of stack prop', () => {
|
|
191
|
+
const AppError = Error0.extend('prop', 'stack', {
|
|
192
|
+
input: (value: string) => value,
|
|
193
|
+
output: (error: Error0) => {
|
|
194
|
+
const stack = error.own('stack')
|
|
195
|
+
if (typeof stack === 'string') {
|
|
196
|
+
return stack
|
|
197
|
+
}
|
|
198
|
+
return undefined
|
|
199
|
+
},
|
|
200
|
+
serialize: () => undefined,
|
|
374
201
|
})
|
|
375
|
-
|
|
202
|
+
const error = new AppError('test')
|
|
203
|
+
const json = AppError.serialize(error) as Record<string, unknown>
|
|
204
|
+
expect('stack' in json).toBe(false)
|
|
205
|
+
})
|
|
376
206
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
207
|
+
it('.serialize() -> .from() roundtrip keeps extension values', () => {
|
|
208
|
+
const AppError = Error0.extend(statusExtension).extend(codeExtension)
|
|
209
|
+
const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
|
|
210
|
+
const json = AppError.serialize(error, false)
|
|
211
|
+
const recreated = AppError.from(json)
|
|
212
|
+
expect(recreated).toBeInstanceOf(AppError)
|
|
213
|
+
expect(recreated.status).toBe(409)
|
|
214
|
+
expect(recreated.code).toBe('NOT_FOUND')
|
|
215
|
+
expect(AppError.serialize(recreated, false)).toEqual(json)
|
|
216
|
+
})
|
|
382
217
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
218
|
+
it('computed values and rich methods work in static/instance modes', () => {
|
|
219
|
+
const AppError = Error0.extend(statusExtension)
|
|
220
|
+
.extend(codeExtension)
|
|
221
|
+
.extend('computed', 'summary', (error) => {
|
|
222
|
+
return `${error.message}:${error.code ?? 'none'}`
|
|
223
|
+
})
|
|
224
|
+
.extend('method', 'hasCode', (error, expectedCode: string) => error.code === expectedCode)
|
|
225
|
+
|
|
226
|
+
const error = new AppError('test', { status: 400, code: 'NOT_FOUND' })
|
|
227
|
+
expect(error.summary).toBe('test:NOT_FOUND')
|
|
228
|
+
expect(error.hasCode('NOT_FOUND')).toBe(true)
|
|
229
|
+
expect(AppError.hasCode(error, 'NOT_FOUND')).toBe(true)
|
|
230
|
+
expect(AppError.hasCode('just string', 'NOT_FOUND')).toBe(false)
|
|
231
|
+
expect('summary' in AppError.serialize(error)).toBe(false)
|
|
388
232
|
})
|
|
389
233
|
|
|
390
|
-
it('
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
expect(
|
|
396
|
-
expect(
|
|
397
|
-
{
|
|
398
|
-
"__I_AM_ERROR_0": true,
|
|
399
|
-
"anyMessage": undefined,
|
|
400
|
-
"cause": undefined,
|
|
401
|
-
"clientMessage": undefined,
|
|
402
|
-
"code": undefined,
|
|
403
|
-
"expected": true,
|
|
404
|
-
"httpStatus": undefined,
|
|
405
|
-
"message": "expected error",
|
|
406
|
-
"meta": {},
|
|
407
|
-
"stack":
|
|
408
|
-
"Error0: expected error
|
|
409
|
-
at <anonymous> (...)"
|
|
410
|
-
,
|
|
411
|
-
"tag": undefined,
|
|
412
|
-
}
|
|
413
|
-
`)
|
|
234
|
+
it('serialize can hide props for public output', () => {
|
|
235
|
+
const AppError = Error0.extend(statusExtension).extend(codeExtension)
|
|
236
|
+
const error = new AppError('test', { status: 401, code: 'NOT_FOUND' })
|
|
237
|
+
const privateJson = AppError.serialize(error, false) as Record<string, unknown>
|
|
238
|
+
const publicJson = AppError.serialize(error, true) as Record<string, unknown>
|
|
239
|
+
expect(privateJson.code).toBe('NOT_FOUND')
|
|
240
|
+
expect('code' in publicJson).toBe(false)
|
|
414
241
|
})
|
|
415
242
|
|
|
416
|
-
it('
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
defaultMeta: {
|
|
420
|
-
tagPrefix: 'nested',
|
|
421
|
-
},
|
|
243
|
+
it('by default error0 created from another error has same message', () => {
|
|
244
|
+
const schema = z.object({
|
|
245
|
+
x: z.string(),
|
|
422
246
|
})
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
expect(
|
|
428
|
-
{
|
|
429
|
-
"__I_AM_ERROR_0": true,
|
|
430
|
-
"anyMessage": undefined,
|
|
431
|
-
"cause": undefined,
|
|
432
|
-
"clientMessage": undefined,
|
|
433
|
-
"code": undefined,
|
|
434
|
-
"expected": false,
|
|
435
|
-
"httpStatus": undefined,
|
|
436
|
-
"message": "nested error",
|
|
437
|
-
"meta": {
|
|
438
|
-
"tagPrefix": "nested",
|
|
439
|
-
},
|
|
440
|
-
"stack":
|
|
441
|
-
"Error0: nested error
|
|
442
|
-
at <anonymous> (...)"
|
|
443
|
-
,
|
|
444
|
-
"tag": "nested:nested",
|
|
445
|
-
}
|
|
446
|
-
`)
|
|
447
|
-
|
|
448
|
-
const error02 = new e0s1.Expected('nested error 1')
|
|
449
|
-
expect(error02).toBeInstanceOf(e0s1.Expected)
|
|
450
|
-
expect(error02).toBeInstanceOf(e0s.Expected)
|
|
451
|
-
expect(error02).toBeInstanceOf(Error0)
|
|
452
|
-
expect(error02.expected).toBe(true)
|
|
453
|
-
expect(toJSON(error02)).toMatchInlineSnapshot(`
|
|
454
|
-
{
|
|
455
|
-
"__I_AM_ERROR_0": true,
|
|
456
|
-
"anyMessage": undefined,
|
|
457
|
-
"cause": undefined,
|
|
458
|
-
"clientMessage": undefined,
|
|
459
|
-
"code": undefined,
|
|
460
|
-
"expected": true,
|
|
461
|
-
"httpStatus": undefined,
|
|
462
|
-
"message": "nested error 1",
|
|
463
|
-
"meta": {
|
|
464
|
-
"tagPrefix": "nested",
|
|
465
|
-
},
|
|
466
|
-
"stack":
|
|
467
|
-
"Error0: nested error 1
|
|
468
|
-
at <anonymous> (...)"
|
|
469
|
-
,
|
|
470
|
-
"tag": "nested:nested",
|
|
471
|
-
}
|
|
472
|
-
`)
|
|
247
|
+
const parseResult = schema.safeParse({ x: 123 })
|
|
248
|
+
const parsedError = parseResult.error
|
|
249
|
+
assert.ok(parsedError)
|
|
250
|
+
const error = Error0.from(parsedError)
|
|
251
|
+
expect(error.message).toBe(parsedError.message)
|
|
473
252
|
})
|
|
474
253
|
|
|
475
|
-
it('
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
254
|
+
it('refine message and other props via direct transformations', () => {
|
|
255
|
+
const schema = z.object({
|
|
256
|
+
x: z.string(),
|
|
257
|
+
})
|
|
258
|
+
const parseResult = schema.safeParse({ x: 123 })
|
|
259
|
+
const parsedError = parseResult.error
|
|
260
|
+
assert.ok(parsedError)
|
|
261
|
+
const AppError = Error0.extend(statusExtension)
|
|
262
|
+
.extend(codeExtension)
|
|
263
|
+
.extend('refine', (error) => {
|
|
264
|
+
if (error.cause instanceof ZodError) {
|
|
265
|
+
error.status = 422
|
|
266
|
+
error.code = 'NOT_FOUND'
|
|
267
|
+
error.message = `Validation Error: ${error.message}`
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
const error = AppError.from(parsedError)
|
|
271
|
+
expect(error.message).toBe(`Validation Error: ${parsedError.message}`)
|
|
272
|
+
expect(error.status).toBe(422)
|
|
273
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
274
|
+
const error1 = new AppError('test', { cause: parsedError })
|
|
275
|
+
expect(error1.message).toBe('test')
|
|
276
|
+
expect(error1.status).toBe(undefined)
|
|
277
|
+
expect(error1.code).toBe(undefined)
|
|
484
278
|
})
|
|
485
279
|
|
|
486
|
-
it('from
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
{
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
280
|
+
it('refine message and other props via return output values from extension', () => {
|
|
281
|
+
const schema = z.object({
|
|
282
|
+
x: z.string(),
|
|
283
|
+
})
|
|
284
|
+
const parseResult = schema.safeParse({ x: 123 })
|
|
285
|
+
const parsedError = parseResult.error
|
|
286
|
+
assert.ok(parsedError)
|
|
287
|
+
const AppError = Error0.extend(statusExtension)
|
|
288
|
+
.extend(codeExtension)
|
|
289
|
+
.extend('refine', (error) => {
|
|
290
|
+
if (error.cause instanceof ZodError) {
|
|
291
|
+
error.message = `Validation Error: ${error.message}`
|
|
292
|
+
return {
|
|
293
|
+
status: 422,
|
|
294
|
+
code: 'NOT_FOUND',
|
|
295
|
+
}
|
|
501
296
|
}
|
|
502
|
-
|
|
503
|
-
|
|
297
|
+
return undefined
|
|
298
|
+
})
|
|
299
|
+
const error = AppError.from(parsedError)
|
|
300
|
+
expect(error.message).toBe(`Validation Error: ${parsedError.message}`)
|
|
301
|
+
expect(error.status).toBe(422)
|
|
302
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
303
|
+
const error1 = new AppError('test', { cause: parsedError })
|
|
304
|
+
expect(error1.message).toBe('test')
|
|
305
|
+
expect(error1.status).toBe(undefined)
|
|
306
|
+
expect(error1.code).toBe(undefined)
|
|
504
307
|
})
|
|
505
308
|
|
|
506
|
-
it('
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
headers: {},
|
|
518
|
-
config: {},
|
|
519
|
-
data: {
|
|
520
|
-
error: 'Invalid input',
|
|
521
|
-
details: ['Field X is required'],
|
|
522
|
-
},
|
|
309
|
+
it('expected prop can be realized to send or not to send error to your error tracker', () => {
|
|
310
|
+
const AppError = Error0.extend(statusExtension)
|
|
311
|
+
.extend('prop', 'expected', {
|
|
312
|
+
input: (value: boolean) => value,
|
|
313
|
+
output: (error) => {
|
|
314
|
+
for (const value of error.flow('expected')) {
|
|
315
|
+
if (typeof value === 'boolean') {
|
|
316
|
+
return value
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return undefined
|
|
523
320
|
},
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
321
|
+
serialize: (value) => value,
|
|
322
|
+
})
|
|
323
|
+
.extend('method', 'isExpected', (error) => {
|
|
324
|
+
return error.expected || false
|
|
325
|
+
})
|
|
326
|
+
const errorExpected = new AppError('test', { status: 400, expected: true })
|
|
327
|
+
const errorUnexpected = new AppError('test', { status: 400, expected: false })
|
|
328
|
+
const usualError = new Error('test')
|
|
329
|
+
const errorFromUsualError = AppError.from(usualError)
|
|
330
|
+
const errorWithExpectedErrorAsCause = new AppError('test', { status: 400, cause: errorExpected })
|
|
331
|
+
const errorWithUnexpectedErrorAsCause = new AppError('test', { status: 400, cause: errorUnexpected })
|
|
332
|
+
expect(errorExpected.expected).toBe(true)
|
|
333
|
+
expect(errorUnexpected.expected).toBe(false)
|
|
334
|
+
expect(AppError.isExpected(usualError)).toBe(false)
|
|
335
|
+
expect(errorFromUsualError.expected).toBe(undefined)
|
|
336
|
+
expect(errorFromUsualError.isExpected()).toBe(false)
|
|
337
|
+
expect(errorWithExpectedErrorAsCause.expected).toBe(true)
|
|
338
|
+
expect(errorWithExpectedErrorAsCause.isExpected()).toBe(true)
|
|
339
|
+
expect(errorWithUnexpectedErrorAsCause.expected).toBe(false)
|
|
340
|
+
expect(errorWithUnexpectedErrorAsCause.isExpected()).toBe(false)
|
|
540
341
|
})
|
|
342
|
+
|
|
343
|
+
// we will have no variants
|
|
344
|
+
// becouse you can thorw any errorm and when you do AppError.from(yourError)
|
|
345
|
+
// can use refine to assign desired props to error, it is enough for transport
|
|
346
|
+
// you even can create computed or method to retrieve your error, so no problems with variants
|
|
347
|
+
|
|
348
|
+
// it('can create and recongnize variant', () => {
|
|
349
|
+
// const AppError = Error0.extend(statusExtension)
|
|
350
|
+
// .extend(codeExtension)
|
|
351
|
+
// .extend('prop', 'userId', {
|
|
352
|
+
// input: (value: string) => value,
|
|
353
|
+
// output: (error) => {
|
|
354
|
+
// for (const value of error.flow('userId')) {
|
|
355
|
+
// if (typeof value === 'string') {
|
|
356
|
+
// return value
|
|
357
|
+
// }
|
|
358
|
+
// }
|
|
359
|
+
// return undefined
|
|
360
|
+
// },
|
|
361
|
+
// serialize: (value) => value,
|
|
362
|
+
// })
|
|
363
|
+
// const UserError = AppError.variant('UserError', {
|
|
364
|
+
// userId: true,
|
|
365
|
+
// })
|
|
366
|
+
// const error = new UserError('test', { userId: '123', status: 400 })
|
|
367
|
+
// expect(error).toBeInstanceOf(UserError)
|
|
368
|
+
// expect(error).toBeInstanceOf(AppError)
|
|
369
|
+
// expect(error).toBeInstanceOf(Error0)
|
|
370
|
+
// expect(error).toBeInstanceOf(Error)
|
|
371
|
+
// expect(error.userId).toBe('123')
|
|
372
|
+
// expect(error.status).toBe(400)
|
|
373
|
+
// expect(error.code).toBe(undefined)
|
|
374
|
+
// expectTypeOf<typeof error.userId>().toEqualTypeOf<string>()
|
|
375
|
+
// // @ts-expect-error
|
|
376
|
+
// new UserError('test')
|
|
377
|
+
// })
|
|
541
378
|
})
|