@devp0nt/error0 1.0.0-next.5 → 1.0.0-next.50
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 +614 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +336 -414
- package/dist/cjs/plugins/cause-serialize.cjs +41 -0
- package/dist/cjs/plugins/cause-serialize.cjs.map +1 -0
- package/dist/cjs/plugins/cause-serialize.d.cts +7 -0
- package/dist/cjs/plugins/code.cjs +38 -0
- package/dist/cjs/plugins/code.cjs.map +1 -0
- package/dist/cjs/plugins/code.d.cts +7 -0
- package/dist/cjs/plugins/expected.cjs +54 -0
- package/dist/cjs/plugins/expected.cjs.map +1 -0
- package/dist/cjs/plugins/expected.d.cts +36 -0
- package/dist/cjs/plugins/message-merge.cjs +39 -0
- package/dist/cjs/plugins/message-merge.cjs.map +1 -0
- package/dist/cjs/plugins/message-merge.d.cts +8 -0
- package/dist/cjs/plugins/meta.cjs +78 -0
- package/dist/cjs/plugins/meta.cjs.map +1 -0
- package/dist/cjs/plugins/meta.d.cts +7 -0
- package/dist/cjs/plugins/stack-merge.cjs +42 -0
- package/dist/cjs/plugins/stack-merge.cjs.map +1 -0
- package/dist/cjs/plugins/stack-merge.d.cts +8 -0
- package/dist/cjs/plugins/status.cjs +54 -0
- package/dist/cjs/plugins/status.cjs.map +1 -0
- package/dist/cjs/plugins/status.d.cts +8 -0
- package/dist/cjs/plugins/tags.cjs +73 -0
- package/dist/cjs/plugins/tags.cjs.map +1 -0
- package/dist/cjs/plugins/tags.d.cts +12 -0
- package/dist/esm/index.d.ts +336 -414
- package/dist/esm/index.js +530 -341
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/plugins/cause-serialize.d.ts +7 -0
- package/dist/esm/plugins/cause-serialize.js +17 -0
- package/dist/esm/plugins/cause-serialize.js.map +1 -0
- package/dist/esm/plugins/code.d.ts +7 -0
- package/dist/esm/plugins/code.js +14 -0
- package/dist/esm/plugins/code.js.map +1 -0
- package/dist/esm/plugins/expected.d.ts +36 -0
- package/dist/esm/plugins/expected.js +30 -0
- package/dist/esm/plugins/expected.js.map +1 -0
- package/dist/esm/plugins/message-merge.d.ts +8 -0
- package/dist/esm/plugins/message-merge.js +15 -0
- package/dist/esm/plugins/message-merge.js.map +1 -0
- package/dist/esm/plugins/meta.d.ts +7 -0
- package/dist/esm/plugins/meta.js +54 -0
- package/dist/esm/plugins/meta.js.map +1 -0
- package/dist/esm/plugins/stack-merge.d.ts +8 -0
- package/dist/esm/plugins/stack-merge.js +18 -0
- package/dist/esm/plugins/stack-merge.js.map +1 -0
- package/dist/esm/plugins/status.d.ts +8 -0
- package/dist/esm/plugins/status.js +30 -0
- package/dist/esm/plugins/status.js.map +1 -0
- package/dist/esm/plugins/tags.d.ts +12 -0
- package/dist/esm/plugins/tags.js +49 -0
- package/dist/esm/plugins/tags.js.map +1 -0
- package/package.json +53 -23
- package/src/index.test.ts +689 -452
- package/src/index.ts +1163 -502
- package/src/plugins/cause-serialize.test.ts +53 -0
- package/src/plugins/cause-serialize.ts +15 -0
- package/src/plugins/code.test.ts +27 -0
- package/src/plugins/code.ts +12 -0
- package/src/plugins/expected.test.ts +47 -0
- package/src/plugins/expected.ts +31 -0
- package/src/plugins/message-merge.test.ts +32 -0
- package/src/plugins/message-merge.ts +19 -0
- package/src/plugins/meta.test.ts +32 -0
- package/src/plugins/meta.ts +59 -0
- package/src/plugins/stack-merge.test.ts +64 -0
- package/src/plugins/stack-merge.ts +20 -0
- package/src/plugins/status.test.ts +54 -0
- package/src/plugins/status.ts +29 -0
- package/src/plugins/tags.test.ts +74 -0
- package/src/plugins/tags.ts +51 -0
- 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 * as assert from 'node:assert'
|
|
3
3
|
import z, { ZodError } from 'zod'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// TODO: test expected
|
|
4
|
+
import type { ClassError0 } from './index.js'
|
|
5
|
+
import { Error0 } from './index.js'
|
|
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,760 @@ const fixStack = (stack: string | undefined) => {
|
|
|
20
19
|
return fixedLines.join('\n')
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
22
|
+
describe('Error0', () => {
|
|
23
|
+
const statusPlugin = Error0.plugin()
|
|
24
|
+
.prop('status', {
|
|
25
|
+
init: (input: number) => input,
|
|
26
|
+
resolve: ({ flow }) => flow.find(Boolean),
|
|
27
|
+
serialize: ({ resolved }) => resolved,
|
|
28
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
29
|
+
})
|
|
30
|
+
.method('isStatus', (error, status: number) => error.status === status)
|
|
31
|
+
|
|
32
|
+
const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
|
|
33
|
+
type Code = (typeof codes)[number]
|
|
34
|
+
const codePlugin = Error0.plugin().use('prop', 'code', {
|
|
35
|
+
init: (input: Code) => input,
|
|
36
|
+
resolve: ({ flow }) => flow.find(Boolean),
|
|
37
|
+
serialize: ({ resolved, isPublic }) => (isPublic ? undefined : resolved),
|
|
38
|
+
deserialize: ({ value }) =>
|
|
39
|
+
typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
|
|
40
|
+
})
|
|
28
41
|
|
|
29
|
-
describe('error0', () => {
|
|
30
42
|
it('simple', () => {
|
|
31
|
-
const
|
|
32
|
-
expect(
|
|
33
|
-
expect(
|
|
34
|
-
expect(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"cause": undefined,
|
|
39
|
-
"clientMessage": undefined,
|
|
40
|
-
"code": undefined,
|
|
41
|
-
"expected": false,
|
|
42
|
-
"httpStatus": undefined,
|
|
43
|
-
"message": "test",
|
|
44
|
-
"meta": {},
|
|
45
|
-
"stack":
|
|
43
|
+
const error = new Error0('test')
|
|
44
|
+
expect(error).toBeInstanceOf(Error0)
|
|
45
|
+
expect(error).toBeInstanceOf(Error)
|
|
46
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
47
|
+
expect(error.message).toBe('test')
|
|
48
|
+
expect(error.stack).toBeDefined()
|
|
49
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
46
50
|
"Error0: test
|
|
47
51
|
at <anonymous> (...)"
|
|
48
|
-
,
|
|
49
|
-
"tag": undefined,
|
|
50
|
-
}
|
|
51
52
|
`)
|
|
52
53
|
})
|
|
53
54
|
|
|
54
|
-
it('
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
expect(toJSON(error1)).toMatchObject(toJSON(error2))
|
|
71
|
-
expect(toJSON(error1)).toMatchInlineSnapshot(`
|
|
72
|
-
{
|
|
73
|
-
"__I_AM_ERROR_0": true,
|
|
74
|
-
"anyMessage": undefined,
|
|
75
|
-
"cause": [Error: original message],
|
|
76
|
-
"clientMessage": "human message 1",
|
|
77
|
-
"code": "code1",
|
|
78
|
-
"expected": true,
|
|
79
|
-
"httpStatus": 400,
|
|
80
|
-
"message": "my message",
|
|
81
|
-
"meta": {
|
|
82
|
-
"reqDurationMs": 1,
|
|
83
|
-
"userId": "user1",
|
|
84
|
-
},
|
|
85
|
-
"stack":
|
|
86
|
-
"Error0: my message
|
|
87
|
-
at <anonymous> (...)
|
|
88
|
-
|
|
89
|
-
Error: original message
|
|
55
|
+
it('with direct prop plugin', () => {
|
|
56
|
+
const AppError = Error0.use('prop', 'status', {
|
|
57
|
+
init: (input: number) => input,
|
|
58
|
+
resolve: ({ flow }) => flow.find(Boolean),
|
|
59
|
+
serialize: ({ resolved }) => resolved,
|
|
60
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
61
|
+
})
|
|
62
|
+
const error = new AppError('test', { status: 400 })
|
|
63
|
+
expect(error).toBeInstanceOf(AppError)
|
|
64
|
+
expect(error).toBeInstanceOf(Error0)
|
|
65
|
+
expect(error).toBeInstanceOf(Error)
|
|
66
|
+
expect(error.status).toBe(400)
|
|
67
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
68
|
+
expect(error.stack).toBeDefined()
|
|
69
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
70
|
+
"Error0: test
|
|
90
71
|
at <anonymous> (...)"
|
|
91
|
-
,
|
|
92
|
-
"tag": "tag1",
|
|
93
|
-
}
|
|
94
72
|
`)
|
|
73
|
+
expectTypeOf<typeof AppError>().toExtend<ClassError0>()
|
|
95
74
|
})
|
|
96
75
|
|
|
97
|
-
it('
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
76
|
+
it('class helpers prop/method/adapt mirror use API', () => {
|
|
77
|
+
const AppError = Error0.use('prop', 'status', {
|
|
78
|
+
init: (value: number) => value,
|
|
79
|
+
resolve: ({ own, flow }) => {
|
|
80
|
+
return typeof own === 'number' ? own : undefined
|
|
81
|
+
},
|
|
82
|
+
serialize: ({ resolved }) => resolved,
|
|
83
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
84
|
+
})
|
|
85
|
+
.use('method', 'isStatus', (error, expectedStatus: number) => error.status === expectedStatus)
|
|
86
|
+
.use('adapt', (error) => {
|
|
87
|
+
if (error.cause instanceof Error && error.status === undefined) {
|
|
88
|
+
return { status: 500 }
|
|
89
|
+
}
|
|
90
|
+
return undefined
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const error = AppError.from(new Error('inner'))
|
|
94
|
+
expect(error.status).toBe(500)
|
|
95
|
+
expect(error.isStatus(500)).toBe(true)
|
|
96
|
+
expect(AppError.isStatus(error, 500)).toBe(true)
|
|
97
|
+
expectTypeOf<typeof AppError>().toExtend<ClassError0>()
|
|
98
|
+
})
|
|
116
99
|
|
|
117
|
-
|
|
100
|
+
it('with defined plugin', () => {
|
|
101
|
+
const AppError = Error0.use(statusPlugin)
|
|
102
|
+
const error = new AppError('test', { status: 400 })
|
|
103
|
+
expect(error).toBeInstanceOf(AppError)
|
|
104
|
+
expect(error).toBeInstanceOf(Error0)
|
|
105
|
+
expect(error).toBeInstanceOf(Error)
|
|
106
|
+
expect(error.status).toBe(400)
|
|
107
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
108
|
+
expect(error.stack).toBeDefined()
|
|
109
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
110
|
+
"Error0: test
|
|
118
111
|
at <anonymous> (...)"
|
|
119
|
-
,
|
|
120
|
-
"tag": undefined,
|
|
121
|
-
}
|
|
122
112
|
`)
|
|
123
113
|
})
|
|
124
114
|
|
|
125
|
-
it('
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
it('twice used Error0 extends previous by types', () => {
|
|
116
|
+
const AppError1 = Error0.use(statusPlugin)
|
|
117
|
+
const AppError2 = AppError1.use(codePlugin)
|
|
118
|
+
const error1 = new AppError1('test', { status: 400 })
|
|
119
|
+
const error2 = new AppError2('test', { status: 400, code: 'NOT_FOUND' })
|
|
120
|
+
expect(error1.status).toBe(400)
|
|
121
|
+
expect(error2.status).toBe(400)
|
|
122
|
+
expect(error2.code).toBe('NOT_FOUND')
|
|
123
|
+
expectTypeOf<typeof error2.status>().toEqualTypeOf<number | undefined>()
|
|
124
|
+
expectTypeOf<typeof error2.code>().toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
|
|
125
|
+
expectTypeOf<typeof AppError1>().toExtend<ClassError0>()
|
|
126
|
+
expectTypeOf<typeof AppError2>().toExtend<ClassError0>()
|
|
127
|
+
expectTypeOf<typeof AppError2>().toExtend<typeof AppError1>()
|
|
128
|
+
expectTypeOf<typeof AppError1>().not.toExtend<typeof AppError2>()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('can have cause', () => {
|
|
132
|
+
const AppError = Error0.use(statusPlugin)
|
|
133
|
+
const anotherError = new Error('another error')
|
|
134
|
+
const error = new AppError('test', { status: 400, cause: anotherError })
|
|
135
|
+
expect(error.status).toBe(400)
|
|
136
|
+
expect(error).toMatchInlineSnapshot(`[Error0: test]`)
|
|
137
|
+
expect(error.stack).toBeDefined()
|
|
138
|
+
expect(fixStack(error.stack)).toMatchInlineSnapshot(`
|
|
139
|
+
"Error0: test
|
|
141
140
|
at <anonymous> (...)"
|
|
142
|
-
,
|
|
143
|
-
"tag": undefined,
|
|
144
|
-
}
|
|
145
141
|
`)
|
|
142
|
+
expect(Error0.causes(error)).toEqual([error, anotherError])
|
|
146
143
|
})
|
|
147
144
|
|
|
148
|
-
it('
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
it('can have many causes', () => {
|
|
146
|
+
const AppError = Error0.use(statusPlugin)
|
|
147
|
+
const anotherError = new Error('another error')
|
|
148
|
+
const error1 = new AppError('test1', { status: 400, cause: anotherError })
|
|
149
|
+
const error2 = new AppError('test2', { status: 400, cause: error1 })
|
|
150
|
+
expect(error1.status).toBe(400)
|
|
151
|
+
expect(error2.status).toBe(400)
|
|
152
|
+
expect(Error0.causes(error2)).toEqual([error2, error1, anotherError])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('can limit causes depth via MAX_CAUSES_DEPTH on class', () => {
|
|
156
|
+
const AppError = Error0.use(statusPlugin)
|
|
157
|
+
const base = new AppError('base', { status: 400 })
|
|
158
|
+
const level1 = new AppError('level1', { status: 401, cause: base })
|
|
159
|
+
const level2 = new AppError('level2', { status: 402, cause: level1 })
|
|
160
|
+
|
|
161
|
+
AppError.MAX_CAUSES_DEPTH = 2
|
|
162
|
+
expect(AppError.causes(level2)).toEqual([level2, level1])
|
|
163
|
+
|
|
164
|
+
AppError.MAX_CAUSES_DEPTH = 999
|
|
165
|
+
expect(AppError.causes(level2)).toEqual([level2, level1, base])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('properties floating', () => {
|
|
169
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
170
|
+
const anotherError = new Error('another error')
|
|
171
|
+
const error1 = new AppError('test1', { status: 400, cause: anotherError })
|
|
172
|
+
const error2 = new AppError('test2', { code: 'NOT_FOUND', cause: error1 })
|
|
173
|
+
expect(error1.status).toBe(400)
|
|
174
|
+
expect(error1.code).toBe(undefined)
|
|
175
|
+
expect(error2.status).toBe(400)
|
|
176
|
+
expect(error2.code).toBe('NOT_FOUND')
|
|
177
|
+
expect(Error0.causes(error2)).toEqual([error2, error1, anotherError])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('property getter return resolved value, not own value', () => {
|
|
181
|
+
const AppError = Error0.use('prop', 'status', {
|
|
182
|
+
init: (input: number) => input,
|
|
183
|
+
resolve: () => 500,
|
|
184
|
+
serialize: ({ resolved }) => resolved,
|
|
185
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
156
186
|
})
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
const error = new AppError('another error', { status: 400 })
|
|
188
|
+
expect(error.status).toBe(500)
|
|
189
|
+
expect(error.own('status')).toBe(400)
|
|
190
|
+
expect(error.flow('status')).toEqual([400])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('serialize uses identity by default and skips undefined plugin values', () => {
|
|
194
|
+
const AppError = Error0.use(statusPlugin).use('prop', 'code', {
|
|
195
|
+
init: (input: string) => input,
|
|
196
|
+
resolve: ({ flow }) => flow.find(Boolean),
|
|
197
|
+
serialize: () => undefined,
|
|
198
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
168
199
|
})
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"cause": [Error0: first],
|
|
175
|
-
"clientMessage": "human message 1",
|
|
176
|
-
"code": "code2",
|
|
177
|
-
"expected": false,
|
|
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> (...)
|
|
200
|
+
const error = new AppError('test', { status: 401, code: 'secret' })
|
|
201
|
+
const json = AppError.serialize(error)
|
|
202
|
+
expect(json.status).toBe(401)
|
|
203
|
+
expect('code' in json).toBe(false)
|
|
204
|
+
})
|
|
191
205
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
`)
|
|
206
|
+
it('serialize keeps stack by default without stack plugin', () => {
|
|
207
|
+
const AppError = Error0.use(statusPlugin)
|
|
208
|
+
const error = new AppError('test', { status: 500 })
|
|
209
|
+
const json = AppError.serialize(error)
|
|
210
|
+
expect(json.stack).toBe(error.stack)
|
|
198
211
|
})
|
|
199
212
|
|
|
200
|
-
it('
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"clientMessage": undefined,
|
|
208
|
-
"code": undefined,
|
|
209
|
-
"expected": false,
|
|
210
|
-
"httpStatus": undefined,
|
|
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> (...)
|
|
213
|
+
it('stack plugin can customize stack serialization without defining prop plugin', () => {
|
|
214
|
+
const AppError = Error0.use('stack', { serialize: ({ value }) => (value ? `custom:${value}` : undefined) })
|
|
215
|
+
const error = new AppError('test')
|
|
216
|
+
const json = AppError.serialize(error)
|
|
217
|
+
expect(typeof json.stack).toBe('string')
|
|
218
|
+
expect((json.stack as string).startsWith('custom:')).toBe(true)
|
|
219
|
+
})
|
|
237
220
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
expect(fixStack(error2.stack)).toMatchInlineSnapshot(`
|
|
245
|
-
"Error0: Unknown error
|
|
246
|
-
at <anonymous> (...)
|
|
221
|
+
it('stack plugin can keep default stack via identity function', () => {
|
|
222
|
+
const AppError = Error0.use('stack', { serialize: ({ value }) => value })
|
|
223
|
+
const error = new AppError('test')
|
|
224
|
+
const json = AppError.serialize(error)
|
|
225
|
+
expect(json.stack).toBe(error.stack)
|
|
226
|
+
})
|
|
247
227
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
228
|
+
it('stack plugin can disable stack serialization via function', () => {
|
|
229
|
+
const AppError = Error0.use('stack', { serialize: () => undefined })
|
|
230
|
+
const error = new AppError('test')
|
|
231
|
+
const json = AppError.serialize(error)
|
|
232
|
+
expect('stack' in json).toBe(false)
|
|
251
233
|
})
|
|
252
234
|
|
|
253
|
-
it('
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
expect(toJSON(error0)).toMatchInlineSnapshot(`
|
|
257
|
-
{
|
|
258
|
-
"__I_AM_ERROR_0": true,
|
|
259
|
-
"anyMessage": undefined,
|
|
260
|
-
"cause": [Error: default error],
|
|
261
|
-
"clientMessage": undefined,
|
|
262
|
-
"code": undefined,
|
|
263
|
-
"expected": false,
|
|
264
|
-
"httpStatus": undefined,
|
|
265
|
-
"message": "Unknown error",
|
|
266
|
-
"meta": {},
|
|
267
|
-
"stack":
|
|
268
|
-
"Error0: Unknown error
|
|
269
|
-
at <anonymous> (...)
|
|
235
|
+
it('stack plugin rejects boolean config', () => {
|
|
236
|
+
expect(() => Error0.use('stack', true as any)).toThrow('expects { serialize: function }')
|
|
237
|
+
})
|
|
270
238
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"tag": undefined,
|
|
275
|
-
}
|
|
276
|
-
`)
|
|
277
|
-
expect(fixStack(error0.stack)).toMatchInlineSnapshot(`
|
|
278
|
-
"Error0: Unknown error
|
|
279
|
-
at <anonymous> (...)
|
|
239
|
+
it('message plugin rejects boolean config', () => {
|
|
240
|
+
expect(() => Error0.use('message', true as any)).toThrow('expects { serialize: function }')
|
|
241
|
+
})
|
|
280
242
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
243
|
+
it('prop("stack") throws and suggests using stack plugin', () => {
|
|
244
|
+
expect(() =>
|
|
245
|
+
Error0.use('prop', 'stack', {
|
|
246
|
+
init: (input: string) => input,
|
|
247
|
+
resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
|
|
248
|
+
serialize: ({ resolved }) => resolved,
|
|
249
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
250
|
+
}),
|
|
251
|
+
).toThrow('reserved prop key')
|
|
284
252
|
})
|
|
285
253
|
|
|
286
|
-
it('
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"expected": false,
|
|
297
|
-
"httpStatus": undefined,
|
|
298
|
-
"message": "Unknown error",
|
|
299
|
-
"meta": {},
|
|
300
|
-
"stack":
|
|
301
|
-
"Error0: Unknown error
|
|
302
|
-
at <anonymous> (...)
|
|
254
|
+
it('plugin builder also rejects prop("stack") as reserved key', () => {
|
|
255
|
+
expect(() =>
|
|
256
|
+
Error0.plugin().prop('stack', {
|
|
257
|
+
init: (input: string) => input,
|
|
258
|
+
resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
|
|
259
|
+
serialize: ({ resolved }) => resolved,
|
|
260
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
261
|
+
}),
|
|
262
|
+
).toThrow('reserved prop key')
|
|
263
|
+
})
|
|
303
264
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
265
|
+
it('prop("message") throws and suggests using message plugin', () => {
|
|
266
|
+
expect(() =>
|
|
267
|
+
Error0.use('prop', 'message', {
|
|
268
|
+
resolve: ({ own }) => own as string,
|
|
269
|
+
serialize: ({ resolved }) => resolved,
|
|
270
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
271
|
+
}),
|
|
272
|
+
).toThrow('reserved prop key')
|
|
273
|
+
})
|
|
313
274
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
275
|
+
it('plugin builder also rejects prop("message") as reserved key', () => {
|
|
276
|
+
expect(() =>
|
|
277
|
+
Error0.plugin().prop('message', {
|
|
278
|
+
resolve: ({ own }) => own as string,
|
|
279
|
+
serialize: ({ resolved }) => resolved,
|
|
280
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
281
|
+
}),
|
|
282
|
+
).toThrow('reserved prop key')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('.serialize() -> .from() roundtrip keeps plugin values', () => {
|
|
286
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
287
|
+
const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
|
|
288
|
+
const json = AppError.serialize(error, false)
|
|
289
|
+
const recreated = AppError.from(json)
|
|
290
|
+
expect(recreated).toBeInstanceOf(AppError)
|
|
291
|
+
expect(recreated.status).toBe(409)
|
|
292
|
+
expect(recreated.code).toBe('NOT_FOUND')
|
|
293
|
+
expect(AppError.serialize(recreated, false)).toEqual(json)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('.round() static and instance do serialize/from roundtrip', () => {
|
|
297
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
298
|
+
const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
|
|
299
|
+
const roundedStatic = AppError.round(error, false)
|
|
300
|
+
const roundedInstance = error.round(false)
|
|
301
|
+
|
|
302
|
+
expect(roundedStatic).toBeInstanceOf(AppError)
|
|
303
|
+
expect(roundedInstance).toBeInstanceOf(AppError)
|
|
304
|
+
expect(roundedStatic.status).toBe(409)
|
|
305
|
+
expect(roundedStatic.code).toBe('NOT_FOUND')
|
|
306
|
+
expect(roundedInstance.status).toBe(409)
|
|
307
|
+
expect(roundedInstance.code).toBe('NOT_FOUND')
|
|
308
|
+
expectTypeOf(roundedStatic.status).toEqualTypeOf<number | undefined>()
|
|
309
|
+
expectTypeOf(roundedStatic.code).toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
|
|
310
|
+
expectTypeOf(roundedInstance.status).toEqualTypeOf<number | undefined>()
|
|
311
|
+
expectTypeOf(roundedInstance.code).toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('.serialize() floated props and not serialize causes', () => {
|
|
315
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
316
|
+
const error1 = new AppError('test', { status: 409 })
|
|
317
|
+
const error2 = new AppError('test', { code: 'NOT_FOUND', cause: error1 })
|
|
318
|
+
const json = AppError.serialize(error2, false)
|
|
319
|
+
expect(json.status).toBe(409)
|
|
320
|
+
expect(json.code).toBe('NOT_FOUND')
|
|
321
|
+
expect('cause' in json).toBe(false)
|
|
317
322
|
})
|
|
318
323
|
|
|
319
|
-
it('
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
it('by default causes not serialized', () => {
|
|
325
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
326
|
+
const error = new AppError('test', { status: 400, code: 'NOT_FOUND' })
|
|
327
|
+
const json = AppError.serialize(error, false)
|
|
328
|
+
expect('cause' in json).toBe(false)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('serialize can hide props for public output', () => {
|
|
332
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
333
|
+
const error = new AppError('test', { status: 401, code: 'NOT_FOUND' })
|
|
334
|
+
const privateJson = AppError.serialize(error, false)
|
|
335
|
+
const publicJson = AppError.serialize(error, true)
|
|
336
|
+
expect(privateJson.code).toBe('NOT_FOUND')
|
|
337
|
+
expect('code' in publicJson).toBe(false)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('prop init without input arg infers undefined-only constructor input', () => {
|
|
341
|
+
const AppError = Error0.use('prop', 'computed', {
|
|
342
|
+
init: () => undefined as number | undefined,
|
|
343
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
344
|
+
serialize: ({ resolved }) => resolved,
|
|
345
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
325
346
|
})
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
347
|
+
|
|
348
|
+
const error = new AppError('test')
|
|
349
|
+
expect(error.computed).toBe(undefined)
|
|
350
|
+
expectTypeOf<typeof error.computed>().toEqualTypeOf<number | undefined>()
|
|
351
|
+
|
|
352
|
+
// @ts-expect-error - computed input is disallowed when init has no input arg
|
|
353
|
+
// eslint-disable-next-line no-new
|
|
354
|
+
new AppError('test', { computed: 123 })
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('prop without init omits constructor input and infers resolve output', () => {
|
|
358
|
+
const AppError = Error0.use('prop', 'statusCode', {
|
|
359
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
360
|
+
serialize: ({ resolved }) => resolved,
|
|
361
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
330
362
|
})
|
|
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
363
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
expect(fixStack(error02.stack)).toMatchInlineSnapshot(`
|
|
343
|
-
"Error0: second
|
|
344
|
-
at <anonymous> (...)
|
|
364
|
+
const error = new AppError('test')
|
|
365
|
+
expect(error.statusCode).toBe(undefined)
|
|
366
|
+
expectTypeOf<typeof error.statusCode>().toEqualTypeOf<number | undefined>()
|
|
345
367
|
|
|
346
|
-
|
|
347
|
-
|
|
368
|
+
// @ts-expect-error - statusCode input is disallowed when init is omitted
|
|
369
|
+
// eslint-disable-next-line no-new
|
|
370
|
+
new AppError('test', { statusCode: 123 })
|
|
371
|
+
})
|
|
348
372
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
373
|
+
it('prop output type is inferred from resolve type', () => {
|
|
374
|
+
const AppError = Error0.use('prop', 'x', {
|
|
375
|
+
init: (input: number) => input,
|
|
376
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
|
|
377
|
+
serialize: ({ resolved }) => resolved,
|
|
378
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const error = new AppError('test')
|
|
382
|
+
expect(error.x).toBe(500)
|
|
383
|
+
expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
|
|
384
|
+
expectTypeOf(AppError.own(error, 'x')).toEqualTypeOf<number | undefined>()
|
|
385
|
+
expectTypeOf(AppError.flow(error, 'x')).toEqualTypeOf<Array<number | undefined>>()
|
|
386
|
+
|
|
387
|
+
Error0.plugin().prop('x', {
|
|
388
|
+
init: (input: number) => input,
|
|
389
|
+
// @ts-expect-error - resolve type extends init type
|
|
390
|
+
resolve: ({ flow }) => 'string',
|
|
391
|
+
serialize: ({ resolved }) => resolved,
|
|
392
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
393
|
+
})
|
|
352
394
|
})
|
|
353
395
|
|
|
354
|
-
it('
|
|
355
|
-
const
|
|
356
|
-
|
|
396
|
+
it('own/flow are typed by output type, not resolve type', () => {
|
|
397
|
+
const AppError = Error0.use('prop', 'code', {
|
|
398
|
+
init: (input: number | 'fallback') => input,
|
|
399
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
|
|
400
|
+
serialize: ({ resolved }) => resolved,
|
|
401
|
+
deserialize: ({ value }) => (typeof value === 'number' || value === 'fallback' ? value : undefined),
|
|
357
402
|
})
|
|
358
|
-
|
|
403
|
+
const error = new AppError('test')
|
|
404
|
+
|
|
405
|
+
expect(error.code).toBe(500)
|
|
406
|
+
expect(AppError.own(error, 'code')).toBe(undefined)
|
|
407
|
+
expect(AppError.own(error)).toEqual({ code: undefined })
|
|
408
|
+
expect(error.own()).toEqual({ code: undefined })
|
|
409
|
+
expectTypeOf<typeof error.code>().toEqualTypeOf<number>()
|
|
410
|
+
expectTypeOf(AppError.own(error, 'code')).toEqualTypeOf<number | 'fallback' | undefined>()
|
|
411
|
+
expectTypeOf(AppError.own(error)).toEqualTypeOf<{ code: number | 'fallback' | undefined }>()
|
|
412
|
+
expectTypeOf(AppError.flow(error, 'code')).toEqualTypeOf<Array<number | 'fallback' | undefined>>()
|
|
413
|
+
})
|
|
359
414
|
|
|
360
|
-
|
|
361
|
-
|
|
415
|
+
it('own/flow runtime behavior across causes', () => {
|
|
416
|
+
type Code = 'A' | 'B'
|
|
417
|
+
const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
|
|
418
|
+
const AppError = Error0.use('prop', 'status', {
|
|
419
|
+
init: (input: number) => input,
|
|
420
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
421
|
+
serialize: ({ resolved }) => resolved,
|
|
422
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
423
|
+
}).use('prop', 'code', {
|
|
424
|
+
init: (input: Code) => input,
|
|
425
|
+
resolve: ({ flow }) => flow.find(isCode),
|
|
426
|
+
serialize: ({ resolved }) => resolved,
|
|
427
|
+
deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
|
|
362
428
|
})
|
|
363
|
-
expect(error1.expected).toBe(false)
|
|
364
429
|
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
430
|
+
const root = new AppError('root', { status: 400, code: 'A' })
|
|
431
|
+
const mid = new AppError('mid', { cause: root })
|
|
432
|
+
const leaf = new AppError('leaf', { status: 500, cause: mid })
|
|
433
|
+
|
|
434
|
+
expect(leaf.own()).toEqual({ status: 500, code: undefined })
|
|
435
|
+
expect(AppError.own(leaf)).toEqual({ status: 500, code: undefined })
|
|
436
|
+
expect(leaf.flow('status')).toEqual([500, undefined, 400])
|
|
437
|
+
expect(AppError.flow(leaf, 'status')).toEqual([500, undefined, 400])
|
|
438
|
+
expect(leaf.flow('code')).toEqual([undefined, undefined, 'A'])
|
|
439
|
+
expect(AppError.flow(leaf, 'code')).toEqual([undefined, undefined, 'A'])
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('own/flow have strong types for static and instance methods', () => {
|
|
443
|
+
type Code = 'A' | 'B'
|
|
444
|
+
const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
|
|
445
|
+
const AppError = Error0.use('prop', 'status', {
|
|
446
|
+
init: (input: number) => input,
|
|
447
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
448
|
+
serialize: ({ resolved }) => resolved,
|
|
449
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
450
|
+
}).use('prop', 'code', {
|
|
451
|
+
init: (input: Code) => input,
|
|
452
|
+
resolve: ({ flow }) => flow.find(isCode),
|
|
453
|
+
serialize: ({ resolved }) => resolved,
|
|
454
|
+
deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
|
|
368
455
|
})
|
|
369
|
-
expect(error3.expected).toBe(true)
|
|
370
456
|
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
457
|
+
const error = new AppError('test', { status: 400, code: 'A' })
|
|
458
|
+
|
|
459
|
+
expectTypeOf(error.own('status')).toEqualTypeOf<number | undefined>()
|
|
460
|
+
expectTypeOf(error.own('code')).toEqualTypeOf<Code | undefined>()
|
|
461
|
+
|
|
462
|
+
expectTypeOf(AppError.own(error, 'status')).toEqualTypeOf<number | undefined>()
|
|
463
|
+
expectTypeOf(AppError.own(error, 'code')).toEqualTypeOf<Code | undefined>()
|
|
464
|
+
expectTypeOf(AppError.own(error)).toEqualTypeOf<{ status: number | undefined; code: Code | undefined }>()
|
|
465
|
+
expectTypeOf(AppError.flow(error, 'status')).toEqualTypeOf<Array<number | undefined>>()
|
|
466
|
+
expectTypeOf(AppError.flow(error, 'code')).toEqualTypeOf<Array<Code | undefined>>()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('resolve returns plain resolved props object without methods', () => {
|
|
470
|
+
type Code = 'A' | 'B'
|
|
471
|
+
const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
|
|
472
|
+
const AppError = Error0.use('prop', 'status', {
|
|
473
|
+
init: (input: number) => input,
|
|
474
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
|
|
475
|
+
serialize: ({ resolved }) => resolved,
|
|
476
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
374
477
|
})
|
|
375
|
-
|
|
478
|
+
.use('prop', 'code', {
|
|
479
|
+
init: (input: Code) => input,
|
|
480
|
+
resolve: ({ flow }) => flow.find(isCode),
|
|
481
|
+
serialize: ({ resolved }) => resolved,
|
|
482
|
+
deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
|
|
483
|
+
})
|
|
484
|
+
.use('method', 'isStatus', (error, status: number) => error.status === status)
|
|
485
|
+
|
|
486
|
+
const root = new AppError('root', { status: 400, code: 'A' })
|
|
487
|
+
const leaf = new AppError('leaf', { cause: root })
|
|
488
|
+
|
|
489
|
+
const resolvedStatic = AppError.resolve(leaf)
|
|
490
|
+
const resolvedInstance = leaf.resolve()
|
|
491
|
+
expect(resolvedStatic).toEqual({ status: 400, code: 'A' })
|
|
492
|
+
expect(resolvedInstance).toEqual({ status: 400, code: 'A' })
|
|
493
|
+
expect('isStatus' in resolvedStatic).toBe(false)
|
|
494
|
+
expect(Object.keys(resolvedInstance)).toEqual(['status', 'code'])
|
|
495
|
+
|
|
496
|
+
expectTypeOf(resolvedStatic).toEqualTypeOf<{ status: number; code: Code | undefined }>()
|
|
497
|
+
})
|
|
376
498
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
499
|
+
it('prop resolved type can be not undefined with init not provided', () => {
|
|
500
|
+
const AppError = Error0.use('prop', 'x', {
|
|
501
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
|
|
502
|
+
serialize: ({ resolved }) => resolved,
|
|
503
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
380
504
|
})
|
|
381
|
-
expect(error5.expected).toBe(false)
|
|
382
505
|
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
506
|
+
const error = new AppError('test')
|
|
507
|
+
expect(error.x).toBe(500)
|
|
508
|
+
expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
|
|
509
|
+
|
|
510
|
+
Error0.plugin().prop('x', {
|
|
511
|
+
init: (input: number) => input,
|
|
512
|
+
// @ts-expect-error - resolve type extends init type
|
|
513
|
+
resolve: ({ flow }) => 'string',
|
|
514
|
+
serialize: ({ resolved }) => resolved,
|
|
515
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
386
516
|
})
|
|
387
|
-
expect(error6.expected).toBe(false)
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
it('extends self', () => {
|
|
391
|
-
const error7 = new e0s.Expected('expected error')
|
|
392
|
-
expect(e0s.Expected.defaultExpected).toBe(true)
|
|
393
|
-
expect(error7.expected).toBe(true)
|
|
394
|
-
expect(error7).toBeInstanceOf(e0s.Expected)
|
|
395
|
-
expect(error7).toBeInstanceOf(Error0)
|
|
396
|
-
expect(toJSON(error7)).toMatchInlineSnapshot(`
|
|
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
|
-
`)
|
|
414
517
|
})
|
|
415
518
|
|
|
416
|
-
it('
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
519
|
+
it('serialize/deserialize can be set to false to disable them', () => {
|
|
520
|
+
const AppError = Error0.use('prop', 'status', {
|
|
521
|
+
init: (input: number) => input,
|
|
522
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
523
|
+
serialize: false,
|
|
524
|
+
deserialize: false,
|
|
525
|
+
})
|
|
526
|
+
const error = new AppError('test', { status: 401 })
|
|
527
|
+
const json = AppError.serialize(error)
|
|
528
|
+
expect('status' in json).toBe(false)
|
|
529
|
+
|
|
530
|
+
const recreated = AppError.from({ ...json, status: 999 })
|
|
531
|
+
expect(recreated.status).toBe(undefined)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('by default error0 created from another error has same message', () => {
|
|
535
|
+
const schema = z.object({
|
|
536
|
+
x: z.string(),
|
|
537
|
+
})
|
|
538
|
+
const parseResult = schema.safeParse({ x: 123 })
|
|
539
|
+
const parsedError = parseResult.error
|
|
540
|
+
assert.ok(parsedError)
|
|
541
|
+
const error = Error0.from(parsedError)
|
|
542
|
+
expect(error.message).toBe(parsedError.message)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('adapt message and other props via direct transformations', () => {
|
|
546
|
+
const schema = z.object({
|
|
547
|
+
x: z.string(),
|
|
548
|
+
})
|
|
549
|
+
const parseResult = schema.safeParse({ x: 123 })
|
|
550
|
+
const parsedError = parseResult.error
|
|
551
|
+
assert.ok(parsedError)
|
|
552
|
+
const AppError = Error0.use(statusPlugin)
|
|
553
|
+
.use(codePlugin)
|
|
554
|
+
.use('adapt', (error) => {
|
|
555
|
+
if (error.cause instanceof ZodError) {
|
|
556
|
+
error.status = 422
|
|
557
|
+
error.code = 'NOT_FOUND'
|
|
558
|
+
error.message = `Validation Error: ${error.message}`
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
const error = AppError.from(parsedError)
|
|
562
|
+
expect(error.message).toBe(`Validation Error: ${parsedError.message}`)
|
|
563
|
+
expect(error.status).toBe(422)
|
|
564
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
565
|
+
const error1 = new AppError('test', { cause: parsedError })
|
|
566
|
+
expect(error1.message).toBe('test')
|
|
567
|
+
expect(error1.status).toBe(undefined)
|
|
568
|
+
expect(error1.code).toBe(undefined)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('adapt message and other props via return output values from plugin', () => {
|
|
572
|
+
const schema = z.object({
|
|
573
|
+
x: z.string(),
|
|
422
574
|
})
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
{
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
575
|
+
const parseResult = schema.safeParse({ x: 123 })
|
|
576
|
+
const parsedError = parseResult.error
|
|
577
|
+
assert.ok(parsedError)
|
|
578
|
+
const AppError = Error0.use(statusPlugin)
|
|
579
|
+
.use(codePlugin)
|
|
580
|
+
.use('adapt', (error) => {
|
|
581
|
+
if (error.cause instanceof ZodError) {
|
|
582
|
+
error.message = `Validation Error: ${error.message}`
|
|
583
|
+
return {
|
|
584
|
+
status: 422,
|
|
585
|
+
code: 'NOT_FOUND',
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return undefined
|
|
589
|
+
})
|
|
590
|
+
const error = AppError.from(parsedError)
|
|
591
|
+
expect(error.message).toBe(`Validation Error: ${parsedError.message}`)
|
|
592
|
+
expect(error.status).toBe(422)
|
|
593
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
594
|
+
const error1 = new AppError('test', { cause: parsedError })
|
|
595
|
+
expect(error1.message).toBe('test')
|
|
596
|
+
expect(error1.status).toBe(undefined)
|
|
597
|
+
expect(error1.code).toBe(undefined)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('messages can be combined on serialization', () => {
|
|
601
|
+
const AppError = Error0.use(statusPlugin)
|
|
602
|
+
.use(codePlugin)
|
|
603
|
+
.use('message', {
|
|
604
|
+
serialize: ({ error }) =>
|
|
605
|
+
error
|
|
606
|
+
.causes()
|
|
607
|
+
.map((cause) => {
|
|
608
|
+
return cause instanceof Error ? cause.message : undefined
|
|
609
|
+
})
|
|
610
|
+
.filter((value): value is string => typeof value === 'string')
|
|
611
|
+
.join(': '),
|
|
612
|
+
})
|
|
613
|
+
const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
|
|
614
|
+
const error2 = new AppError({ message: 'test2', status: 401, cause: error1 })
|
|
615
|
+
expect(error1.message).toEqual('test1')
|
|
616
|
+
expect(error2.message).toEqual('test2')
|
|
617
|
+
expect((error2.cause as any)?.message).toEqual('test1')
|
|
618
|
+
expect(error1.serialize().message).toEqual('test1')
|
|
619
|
+
expect(error2.serialize().message).toEqual('test2: test1')
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('stack plugin can merge stack across causes in one serialized value', () => {
|
|
623
|
+
const AppError = Error0.use(statusPlugin)
|
|
624
|
+
.use(codePlugin)
|
|
625
|
+
.use('stack', {
|
|
626
|
+
serialize: ({ error }) =>
|
|
627
|
+
error
|
|
628
|
+
.causes()
|
|
629
|
+
.map((cause) => {
|
|
630
|
+
return cause instanceof Error ? cause.stack : undefined
|
|
631
|
+
})
|
|
632
|
+
.filter((value): value is string => typeof value === 'string')
|
|
633
|
+
.join('\n'),
|
|
634
|
+
})
|
|
635
|
+
const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
|
|
636
|
+
const error2 = new AppError('test2', { status: 401, cause: error1 })
|
|
637
|
+
const mergedStack1 = error1.serialize().stack as string
|
|
638
|
+
const mergedStack2 = error2.serialize().stack as string
|
|
639
|
+
expect(mergedStack1).toContain('Error0: test1')
|
|
640
|
+
expect(mergedStack2).toContain('Error0: test2')
|
|
641
|
+
expect(mergedStack2).toContain('Error0: test1')
|
|
642
|
+
expect(fixStack(mergedStack1)).toMatchInlineSnapshot(`
|
|
643
|
+
"Error0: test1
|
|
442
644
|
at <anonymous> (...)"
|
|
443
|
-
,
|
|
444
|
-
"tag": "nested:nested",
|
|
445
|
-
}
|
|
446
645
|
`)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
646
|
+
expect(fixStack(mergedStack2)).toMatchInlineSnapshot(`
|
|
647
|
+
"Error0: test2
|
|
648
|
+
at <anonymous> (...)
|
|
649
|
+
Error0: test1
|
|
468
650
|
at <anonymous> (...)"
|
|
469
|
-
,
|
|
470
|
-
"tag": "nested:nested",
|
|
471
|
-
}
|
|
472
651
|
`)
|
|
473
652
|
})
|
|
474
653
|
|
|
475
|
-
it('
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
654
|
+
it('stress: resolve/serialize/flow stays within perf budget', () => {
|
|
655
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
656
|
+
|
|
657
|
+
let current: InstanceType<typeof AppError> = new AppError('root', {
|
|
658
|
+
status: 500,
|
|
659
|
+
code: 'BAD_REQUEST',
|
|
660
|
+
})
|
|
661
|
+
for (let i = 0; i < 300; i += 1) {
|
|
662
|
+
current = new AppError(`level-${i}`, {
|
|
663
|
+
status: 500,
|
|
664
|
+
code: i % 2 === 0 ? 'NOT_FOUND' : 'BAD_REQUEST',
|
|
665
|
+
cause: current,
|
|
666
|
+
})
|
|
479
667
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
668
|
+
|
|
669
|
+
let checksum = 0
|
|
670
|
+
const startedAt = performance.now()
|
|
671
|
+
for (let i = 0; i < 3000; i += 1) {
|
|
672
|
+
const resolved = AppError.resolve(current)
|
|
673
|
+
const serialized = AppError.serialize(current, false)
|
|
674
|
+
const flow = current.flow('status')
|
|
675
|
+
checksum += resolved.status ?? 0
|
|
676
|
+
checksum += (serialized.status as number | undefined) ?? 0
|
|
677
|
+
checksum += flow.length
|
|
678
|
+
}
|
|
679
|
+
const elapsedMs = performance.now() - startedAt
|
|
680
|
+
const budgetMs = process.env.CI ? 8000 : 4000
|
|
681
|
+
|
|
682
|
+
expect(checksum).toBeGreaterThan(0)
|
|
683
|
+
expect(elapsedMs).toBeLessThan(budgetMs)
|
|
484
684
|
})
|
|
485
685
|
|
|
486
|
-
it('from
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
686
|
+
it('class can be created extended from Error0', () => {
|
|
687
|
+
class MyError extends Error0.use(statusPlugin).use(codePlugin) {
|
|
688
|
+
isStatusAndCode(status: number, code: string): boolean {
|
|
689
|
+
return this.status === status && this.code === code
|
|
690
|
+
}
|
|
490
691
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
expect(
|
|
494
|
-
expect(
|
|
495
|
-
|
|
496
|
-
{
|
|
497
|
-
"expected": "object",
|
|
498
|
-
"code": "invalid_type",
|
|
499
|
-
"path": [],
|
|
500
|
-
"message": "Invalid input: expected object, received string"
|
|
501
|
-
}
|
|
502
|
-
]"
|
|
503
|
-
`)
|
|
692
|
+
const error = new MyError('test', { status: 400, code: 'NOT_FOUND' })
|
|
693
|
+
expect(error.isStatusAndCode(400, 'NOT_FOUND')).toBe(true)
|
|
694
|
+
expect(error.isStatusAndCode(400, 'BAD_REQUEST')).toBe(false)
|
|
695
|
+
expect(error.name).toBe('Error0')
|
|
696
|
+
expectTypeOf<typeof MyError>().toExtend<ClassError0>()
|
|
504
697
|
})
|
|
505
698
|
|
|
506
|
-
it('
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
message
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
699
|
+
it('Error0 assignable to LikeError0', () => {
|
|
700
|
+
class MyError extends Error {
|
|
701
|
+
status?: number
|
|
702
|
+
code?: string
|
|
703
|
+
constructor(message: string, options: { cause?: unknown; status?: number; code?: string }) {
|
|
704
|
+
super(message, { cause: options.cause })
|
|
705
|
+
this.status = options.status
|
|
706
|
+
this.code = options.code
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
static from(error: unknown): MyError {
|
|
710
|
+
if (error instanceof MyError) {
|
|
711
|
+
return error
|
|
712
|
+
}
|
|
713
|
+
const object = typeof error === 'object' && error !== null ? (error as Record<string, unknown>) : {}
|
|
714
|
+
const message =
|
|
715
|
+
typeof object.message === 'string' ? object.message : typeof error === 'string' ? error : 'Unknown error'
|
|
716
|
+
const status = typeof object.status === 'number' ? object.status : undefined
|
|
717
|
+
const code = typeof object.code === 'string' ? object.code : undefined
|
|
718
|
+
return new MyError(message, {
|
|
719
|
+
cause: error,
|
|
720
|
+
status,
|
|
721
|
+
code,
|
|
722
|
+
})
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
static serialize(error: MyError): Record<string, unknown> {
|
|
726
|
+
return {
|
|
727
|
+
message: error.message,
|
|
728
|
+
status: error.status,
|
|
729
|
+
code: error.code,
|
|
730
|
+
}
|
|
731
|
+
}
|
|
525
732
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
733
|
+
|
|
734
|
+
type LikeError0<TError> = {
|
|
735
|
+
new (message: string, options: { cause?: unknown; status?: number; code?: string }): TError
|
|
736
|
+
from: (error: unknown) => TError
|
|
737
|
+
serialize: (error: TError) => Record<string, unknown>
|
|
529
738
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
expect(error0.axiosError).toBe(axiosError)
|
|
533
|
-
expect(error0.message).toBe('Axios Error')
|
|
534
|
-
expect(error0.meta).toMatchInlineSnapshot(`
|
|
535
|
-
{
|
|
536
|
-
"axiosData": "{"error":"Invalid input","details":["Field X is required"]}",
|
|
537
|
-
"axiosStatus": 400,
|
|
538
|
-
}
|
|
539
|
-
`)
|
|
739
|
+
expectTypeOf<typeof MyError>().toExtend<LikeError0<MyError>>()
|
|
740
|
+
expectTypeOf<typeof Error0>().toExtend<typeof MyError>()
|
|
540
741
|
})
|
|
742
|
+
|
|
743
|
+
// we will have no variants
|
|
744
|
+
// becouse you can thorw any errorm and when you do AppError.from(yourError)
|
|
745
|
+
// can use adapt to assign desired props to error, it is enough for transport
|
|
746
|
+
// you even can create computed or method to retrieve your error, so no problems with variants
|
|
747
|
+
|
|
748
|
+
// it('can create and recongnize variant', () => {
|
|
749
|
+
// const AppError = Error0.use(statusPlugin)
|
|
750
|
+
// .use(codePlugin)
|
|
751
|
+
// .use('prop', 'userId', {
|
|
752
|
+
// input: (value: string) => value,
|
|
753
|
+
// output: (error) => {
|
|
754
|
+
// for (const value of error.flow('userId')) {
|
|
755
|
+
// if (typeof value === 'string') {
|
|
756
|
+
// return value
|
|
757
|
+
// }
|
|
758
|
+
// }
|
|
759
|
+
// return undefined
|
|
760
|
+
// },
|
|
761
|
+
// serialize: (value) => value,
|
|
762
|
+
// })
|
|
763
|
+
// const UserError = AppError.variant('UserError', {
|
|
764
|
+
// userId: true,
|
|
765
|
+
// })
|
|
766
|
+
// const error = new UserError('test', { userId: '123', status: 400 })
|
|
767
|
+
// expect(error).toBeInstanceOf(UserError)
|
|
768
|
+
// expect(error).toBeInstanceOf(AppError)
|
|
769
|
+
// expect(error).toBeInstanceOf(Error0)
|
|
770
|
+
// expect(error).toBeInstanceOf(Error)
|
|
771
|
+
// expect(error.userId).toBe('123')
|
|
772
|
+
// expect(error.status).toBe(400)
|
|
773
|
+
// expect(error.code).toBe(undefined)
|
|
774
|
+
// expectTypeOf<typeof error.userId>().toEqualTypeOf<string>()
|
|
775
|
+
// // @ts-expect-error
|
|
776
|
+
// new UserError('test')
|
|
777
|
+
// })
|
|
541
778
|
})
|