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