@devp0nt/error0 1.0.0-next.55 → 1.0.0-next.57

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/src/index.test.ts DELETED
@@ -1,967 +0,0 @@
1
- import { describe, expect, expectTypeOf, it } from 'bun:test'
2
- import * as assert from 'node:assert'
3
- import z, { ZodError } from 'zod'
4
- import type { ClassError0 } from './index.js'
5
- import { Error0 } from './index.js'
6
-
7
- const fixStack = (stack: string | undefined) => {
8
- if (!stack) {
9
- return stack
10
- }
11
- // at <anonymous> (/Users/x/error0.test.ts:103:25)
12
- // ↓
13
- // at <anonymous> (...)
14
- const lines = stack.split('\n')
15
- const fixedLines = lines.map((line) => {
16
- const withoutPath = line.replace(/\(.*\)$/, '(...)')
17
- return withoutPath
18
- })
19
- return fixedLines.join('\n')
20
- }
21
-
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
- })
41
-
42
- it('simple', () => {
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(`
50
- "Error0: test
51
- at <anonymous> (...)"
52
- `)
53
- })
54
-
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
71
- at <anonymous> (...)"
72
- `)
73
- expectTypeOf<typeof AppError>().toExtend<ClassError0>()
74
- })
75
-
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
- })
99
-
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
111
- at <anonymous> (...)"
112
- `)
113
- })
114
-
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
140
- at <anonymous> (...)"
141
- `)
142
- expect(Error0.causes(error)).toEqual([error, anotherError])
143
- })
144
-
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),
186
- })
187
- const error = new AppError('another error', { status: 400 })
188
- expect(error.status).toBe(500)
189
- expectTypeOf<typeof error.status>().toEqualTypeOf<number>()
190
- expect(error.own?.status).toBe(400)
191
- expect(error.flow('status')).toEqual([400])
192
- })
193
-
194
- it('without resolve, getter return own value, also resolved value eq to own value', () => {
195
- const AppError = Error0.use('prop', 'status', {
196
- init: (input: number) => input,
197
- serialize: ({ resolved }) => resolved,
198
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
199
- })
200
- const error = new AppError('another error', { status: 400 })
201
- expect(error.status).toBe(400)
202
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
203
- expect(error.own?.status).toBe(400)
204
- expect(error.flow('status')).toEqual([400])
205
- expect(error.serialize().status).toEqual(400)
206
- })
207
-
208
- it('without resolve (as false), getter return own value, also resolved value eq to own value', () => {
209
- const AppError = Error0.use('prop', 'status', {
210
- init: (input: number) => input,
211
- resolve: false,
212
- serialize: ({ resolved }) => resolved,
213
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
214
- })
215
- const error = new AppError('another error', { status: 400 })
216
- expect(error.status).toBe(400)
217
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
218
- expect(error.own?.status).toBe(400)
219
- expect(error.flow('status')).toEqual([400])
220
- expect(error.serialize().status).toEqual(400)
221
- })
222
-
223
- it('with resolve true, getter return first not undefined in flow', () => {
224
- const AppError = Error0.use('prop', 'status', {
225
- init: (input: number) => input,
226
- resolve: true,
227
- serialize: ({ resolved }) => resolved,
228
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
229
- })
230
- const cause = new AppError('another error', { status: 400 })
231
- const error = new AppError('test', { cause })
232
- expect(error.status).toBe(400)
233
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
234
- expect(error.own?.status).toBe(undefined)
235
- expect(error.flow('status')).toEqual([undefined, 400])
236
- expect(error.serialize().status).toEqual(400)
237
- })
238
-
239
- it('(plugin) without resolve, getter return own value, also resolved value eq to own value', () => {
240
- const statusPlugin = Error0.plugin().prop('status', {
241
- init: (input: number) => input,
242
- serialize: ({ resolved }) => resolved,
243
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
244
- })
245
- const AppError = Error0.use(statusPlugin)
246
- const error = new AppError('another error', { status: 400 })
247
- expect(error.status).toBe(400)
248
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
249
- expect(error.own?.status).toBe(400)
250
- expect(error.flow('status')).toEqual([400])
251
- expect(error.serialize().status).toEqual(400)
252
- })
253
-
254
- it('(plugin) without resolve (as false), getter return own value, also resolved value eq to own value', () => {
255
- const statusPlugin = Error0.plugin().prop('status', {
256
- init: (input: number) => input,
257
- resolve: false,
258
- serialize: ({ resolved }) => resolved,
259
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
260
- })
261
- const AppError = Error0.use(statusPlugin)
262
- const error = new AppError('another error', { status: 400 })
263
- expect(error.status).toBe(400)
264
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
265
- expect(error.own?.status).toBe(400)
266
- expect(error.flow('status')).toEqual([400])
267
- expect(error.serialize().status).toEqual(400)
268
- })
269
-
270
- it('without serailize and deserialize value will not be serialized', () => {
271
- const AppError = Error0.use('prop', 'status', {
272
- init: (input: number) => input,
273
- resolve: true,
274
- })
275
- const cause = new AppError('another error', { status: 400 })
276
- const error = new AppError('test', { cause })
277
- expect(error.status).toBe(400)
278
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
279
- expect(error.own?.status).toBe(undefined)
280
- expect(error.flow('status')).toEqual([undefined, 400])
281
- expect(error.serialize().status).toEqual(undefined)
282
- })
283
-
284
- it('(plugin) with resolve true, getter return first not undefined in flow', () => {
285
- const statusPlugin = Error0.plugin().prop('status', {
286
- init: (input: number) => input,
287
- resolve: true,
288
- serialize: ({ resolved }) => resolved,
289
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
290
- })
291
- const AppError = Error0.use(statusPlugin)
292
- const cause = new AppError('another error', { status: 400 })
293
- const error = new AppError('test', { cause })
294
- expect(error.status).toBe(400)
295
- expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
296
- expect(error.own?.status).toBe(undefined)
297
- expect(error.flow('status')).toEqual([undefined, 400])
298
- expect(error.serialize().status).toEqual(400)
299
- })
300
-
301
- it('assign props check types', () => {
302
- const AppError = Error0.use(statusPlugin).use(codePlugin)
303
- const error = new AppError('test', { status: 401 })
304
- // @ts-expect-error - code is not match desirecd type
305
- error.assign({ code: 'NOT_VALID_CODE' })
306
- })
307
-
308
- it('assign props', () => {
309
- const AppError = Error0.use(statusPlugin).use(codePlugin)
310
- const error = new AppError('test', { status: 401 })
311
- const error2 = error.assign({ code: 'NOT_FOUND' })
312
- expect(error).toBe(error2)
313
- expect(error2.status).toBe(401)
314
- expect(error2.code).toBe('NOT_FOUND')
315
- })
316
-
317
- it('static assign props check types', () => {
318
- const AppError = Error0.use(statusPlugin).use(codePlugin)
319
- const error = new AppError('test', { status: 401 })
320
- AppError.assign(
321
- error,
322
- // @ts-expect-error - code is not match desirecd type
323
- { code: 'NOT_VALID_CODE' },
324
- )
325
- })
326
-
327
- it('static assign props', () => {
328
- const AppError = Error0.use(statusPlugin).use(codePlugin)
329
- const error = new AppError('test', { status: 401 })
330
- const error2 = AppError.assign(error, { code: 'NOT_FOUND' })
331
- expect(error).toBe(error2)
332
- expect(error2.status).toBe(401)
333
- expect(error2.code).toBe('NOT_FOUND')
334
- })
335
-
336
- // it('form, assing, round, flat, and new, return same type and instance', () => {
337
- it('form, assing, round, and new, return same type and instance', () => {
338
- const AppError = Error0.use(statusPlugin).use(codePlugin)
339
- const errorNew = new AppError('test', { status: 401 })
340
- const errorAssign = AppError.assign({}, { status: 401 })
341
- const errorAssign1 = errorNew.assign({})
342
- const errorFrom = AppError.from({})
343
- const errorRound = AppError.round({})
344
- const errorRound1 = errorNew.round()
345
- // const errorFlat = AppError.flat({})
346
- // const errorFlat1 = errorNew.flat()
347
- expect(errorNew).toBeInstanceOf(AppError)
348
- expect(errorAssign).toBeInstanceOf(AppError)
349
- expect(errorAssign1).toBeInstanceOf(AppError)
350
- expect(errorFrom).toBeInstanceOf(AppError)
351
- expect(errorRound).toBeInstanceOf(AppError)
352
- expect(errorRound1).toBeInstanceOf(AppError)
353
- // expect(errorFlat).toBeInstanceOf(AppError)
354
- // expect(errorFlat1).toBeInstanceOf(AppError)
355
- expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorAssign>()
356
- expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorAssign1>()
357
- expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorFrom>()
358
- expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorRound>()
359
- expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorRound1>()
360
- // expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorFlat>()
361
- // expectTypeOf<typeof errorNew>().toEqualTypeOf<typeof errorFlat1>()
362
- })
363
-
364
- // it.only('flat merge all resolved values to own and remove causes', () => {
365
- // const AppError = Error0.use(statusPlugin).use(codePlugin)
366
- // const errorDeepDeep = new AppError('test deep deep', { status: 401 })
367
- // const errorDeep = new AppError('test deep', { code: 'NOT_FOUND', cause: errorDeepDeep })
368
- // const error = new AppError('test', { status: 401, cause: errorDeep })
369
- // const flat = error.flat()
370
- // expect(flat.own).toEqual({ status: 401, code: 'NOT_FOUND' })
371
- // expect(flat.cause).toBeUndefined()
372
- // console.error(flat)
373
- // })
374
-
375
- it('serialize uses identity by default and skips undefined plugin values', () => {
376
- const AppError = Error0.use(statusPlugin).use('prop', 'code', {
377
- init: (input: string) => input,
378
- resolve: ({ flow }) => flow.find(Boolean),
379
- serialize: () => undefined,
380
- deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
381
- })
382
- const error = new AppError('test', { status: 401, code: 'secret' })
383
- const json = AppError.serialize(error)
384
- expect(json.status).toBe(401)
385
- expect('code' in json).toBe(false)
386
- })
387
-
388
- it('serialize keeps stack by default without stack plugin when not public', () => {
389
- const AppError = Error0.use(statusPlugin)
390
- const error = new AppError('test', { status: 500 })
391
- const json = AppError.serialize(error, false)
392
- expect(json.stack).toBe(error.stack)
393
- })
394
-
395
- it('serialize does not keep stack when public', () => {
396
- const AppError = Error0.use(statusPlugin)
397
- const error = new AppError('test', { status: 500 })
398
- const json = AppError.serialize(error, true)
399
- expect('stack' in json).toBe(false)
400
- })
401
-
402
- it('stack plugin can customize stack serialization without defining prop plugin', () => {
403
- const AppError = Error0.use('stack', { serialize: ({ value }) => (value ? `custom:${value}` : undefined) })
404
- const error = new AppError('test')
405
- const json = AppError.serialize(error)
406
- expect(typeof json.stack).toBe('string')
407
- expect((json.stack as string).startsWith('custom:')).toBe(true)
408
- })
409
-
410
- it('stack plugin can keep default stack via identity function', () => {
411
- const AppError = Error0.use('stack', { serialize: ({ value }) => value })
412
- const error = new AppError('test')
413
- const json = AppError.serialize(error)
414
- expect(json.stack).toBe(error.stack)
415
- })
416
-
417
- it('stack plugin can disable stack serialization via function', () => {
418
- const AppError = Error0.use('stack', { serialize: () => undefined })
419
- const error = new AppError('test')
420
- const json = AppError.serialize(error)
421
- expect('stack' in json).toBe(false)
422
- })
423
-
424
- it('stack plugin rejects boolean config', () => {
425
- expect(() => Error0.use('stack', true as any)).toThrow('expects { serialize: function }')
426
- })
427
-
428
- it('message plugin rejects boolean config', () => {
429
- expect(() => Error0.use('message', true as any)).toThrow('expects { serialize: function }')
430
- })
431
-
432
- it('prop("stack") throws and suggests using stack plugin', () => {
433
- expect(() =>
434
- Error0.use('prop', 'stack', {
435
- init: (input: string) => input,
436
- resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
437
- serialize: ({ resolved }) => resolved,
438
- deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
439
- }),
440
- ).toThrow('reserved prop key')
441
- })
442
-
443
- it('plugin builder also rejects prop("stack") as reserved key', () => {
444
- expect(() =>
445
- Error0.plugin().prop('stack', {
446
- init: (input: string) => input,
447
- resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
448
- serialize: ({ resolved }) => resolved,
449
- deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
450
- }),
451
- ).toThrow('reserved prop key')
452
- })
453
-
454
- it('prop("message") throws and suggests using message plugin', () => {
455
- expect(() =>
456
- Error0.use('prop', 'message', {
457
- resolve: ({ own }) => own as string,
458
- serialize: ({ resolved }) => resolved,
459
- deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
460
- }),
461
- ).toThrow('reserved prop key')
462
- })
463
-
464
- it('plugin builder also rejects prop("message") as reserved key', () => {
465
- expect(() =>
466
- Error0.plugin().prop('message', {
467
- resolve: ({ own }) => own as string,
468
- serialize: ({ resolved }) => resolved,
469
- deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
470
- }),
471
- ).toThrow('reserved prop key')
472
- })
473
-
474
- it('.serialize() -> .from() roundtrip keeps plugin values', () => {
475
- const AppError = Error0.use(statusPlugin).use(codePlugin)
476
- const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
477
- const json = AppError.serialize(error, false)
478
- const recreated = AppError.from(json)
479
- expect(recreated).toBeInstanceOf(AppError)
480
- expect(recreated.status).toBe(409)
481
- expect(recreated.code).toBe('NOT_FOUND')
482
- expect(AppError.serialize(recreated, false)).toEqual(json)
483
- })
484
-
485
- it('.round() static and instance do serialize/from roundtrip', () => {
486
- const AppError = Error0.use(statusPlugin).use(codePlugin)
487
- const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
488
- const roundedStatic = AppError.round(error, false)
489
- const roundedInstance = error.round(false)
490
-
491
- expect(roundedStatic).toBeInstanceOf(AppError)
492
- expect(roundedInstance).toBeInstanceOf(AppError)
493
- expect(roundedStatic.status).toBe(409)
494
- expect(roundedStatic.code).toBe('NOT_FOUND')
495
- expect(roundedInstance.status).toBe(409)
496
- expect(roundedInstance.code).toBe('NOT_FOUND')
497
- expectTypeOf(roundedStatic.status).toEqualTypeOf<number | undefined>()
498
- expectTypeOf(roundedStatic.code).toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
499
- expectTypeOf(roundedInstance.status).toEqualTypeOf<number | undefined>()
500
- expectTypeOf(roundedInstance.code).toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
501
- })
502
-
503
- it('.serialize() floated props and not serialize causes', () => {
504
- const AppError = Error0.use(statusPlugin).use(codePlugin)
505
- const error1 = new AppError('test', { status: 409 })
506
- const error2 = new AppError('test', { code: 'NOT_FOUND', cause: error1 })
507
- const json = AppError.serialize(error2, false)
508
- expect(json.status).toBe(409)
509
- expect(json.code).toBe('NOT_FOUND')
510
- expect('cause' in json).toBe(false)
511
- })
512
-
513
- it('by default causes not serialized', () => {
514
- const AppError = Error0.use(statusPlugin).use(codePlugin)
515
- const error = new AppError('test', { status: 400, code: 'NOT_FOUND' })
516
- const json = AppError.serialize(error, false)
517
- expect('cause' in json).toBe(false)
518
- })
519
-
520
- it('serialize can hide props for public output', () => {
521
- const AppError = Error0.use(statusPlugin).use(codePlugin)
522
- const error = new AppError('test', { status: 401, code: 'NOT_FOUND' })
523
- const privateJson = AppError.serialize(error, false)
524
- const publicJson = AppError.serialize(error, true)
525
- expect(privateJson.code).toBe('NOT_FOUND')
526
- expect('code' in publicJson).toBe(false)
527
- })
528
-
529
- it('prop init without input arg infers undefined-only constructor input', () => {
530
- const AppError = Error0.use('prop', 'computed', {
531
- init: () => undefined as number | undefined,
532
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
533
- serialize: ({ resolved }) => resolved,
534
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
535
- })
536
-
537
- const error = new AppError('test')
538
- expect(error.computed).toBe(undefined)
539
- expectTypeOf<typeof error.computed>().toEqualTypeOf<number | undefined>()
540
-
541
- // @ts-expect-error - computed input is disallowed when init has no input arg
542
- // eslint-disable-next-line no-new
543
- new AppError('test', { computed: 123 })
544
- })
545
-
546
- it('prop without init omits constructor input and infers resolve output', () => {
547
- const AppError = Error0.use('prop', 'statusCode', {
548
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
549
- serialize: ({ resolved }) => resolved,
550
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
551
- })
552
-
553
- const error = new AppError('test')
554
- expect(error.statusCode).toBe(undefined)
555
- expectTypeOf<typeof error.statusCode>().toEqualTypeOf<number | undefined>()
556
-
557
- // @ts-expect-error - statusCode input is disallowed when init is omitted
558
- // eslint-disable-next-line no-new
559
- new AppError('test', { statusCode: 123 })
560
- })
561
-
562
- it('prop output type is inferred from resolve type', () => {
563
- const AppError = Error0.use('prop', 'x', {
564
- init: (input: number) => input,
565
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
566
- serialize: ({ resolved }) => resolved,
567
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
568
- })
569
-
570
- const error = new AppError('test')
571
- expect(error.x).toBe(500)
572
- expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
573
- expectTypeOf(AppError.own(error, 'x')).toEqualTypeOf<number | undefined>()
574
- expectTypeOf(AppError.flow(error, 'x')).toEqualTypeOf<Array<number | undefined>>()
575
-
576
- Error0.plugin().prop('x', {
577
- init: (input: number) => input,
578
- // @ts-expect-error - resolve type extends init type
579
- resolve: ({ flow }) => 'string',
580
- serialize: ({ resolved }) => resolved,
581
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
582
- })
583
- })
584
-
585
- it('own/flow are typed by output type, not resolve type', () => {
586
- const AppError = Error0.use('prop', 'code', {
587
- init: (input: number | 'fallback') => input,
588
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
589
- serialize: ({ resolved }) => resolved,
590
- deserialize: ({ value }) => (typeof value === 'number' || value === 'fallback' ? value : undefined),
591
- })
592
- const error = new AppError('test')
593
-
594
- expect(error.code).toBe(500)
595
- expect(AppError.own(error, 'code')).toBe(undefined)
596
- expect(AppError.own(error)).toEqual({ code: undefined })
597
- expect(error.own).toEqual(undefined)
598
- expectTypeOf<typeof error.code>().toEqualTypeOf<number>()
599
- expectTypeOf(AppError.own(error, 'code')).toEqualTypeOf<number | 'fallback' | undefined>()
600
- expectTypeOf(AppError.own(error)).toEqualTypeOf<{ code: number | 'fallback' | undefined }>()
601
- expectTypeOf(AppError.flow(error, 'code')).toEqualTypeOf<Array<number | 'fallback' | undefined>>()
602
- })
603
-
604
- it('own/flow runtime behavior across causes', () => {
605
- type Code = 'A' | 'B'
606
- const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
607
- const AppError = Error0.use('prop', 'status', {
608
- init: (input: number) => input,
609
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
610
- serialize: ({ resolved }) => resolved,
611
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
612
- }).use('prop', 'code', {
613
- init: (input: Code) => input,
614
- resolve: ({ flow }) => flow.find(isCode),
615
- serialize: ({ resolved }) => resolved,
616
- deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
617
- })
618
-
619
- const root = new AppError('root', { status: 400, code: 'A' })
620
- const mid = new AppError('mid', { cause: root })
621
- const leaf = new AppError('leaf', { status: 500, cause: mid })
622
-
623
- expect(leaf.own).toEqual({ status: 500, code: undefined })
624
- expect(AppError.own(leaf)).toEqual({ status: 500, code: undefined })
625
- expect(leaf.flow('status')).toEqual([500, undefined, 400])
626
- expect(AppError.flow(leaf, 'status')).toEqual([500, undefined, 400])
627
- expect(leaf.flow('code')).toEqual([undefined, undefined, 'A'])
628
- expect(AppError.flow(leaf, 'code')).toEqual([undefined, undefined, 'A'])
629
- })
630
-
631
- it('own/flow have strong types for static and instance methods', () => {
632
- type Code = 'A' | 'B'
633
- const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
634
- const AppError = Error0.use('prop', 'status', {
635
- init: (input: number) => input,
636
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
637
- serialize: ({ resolved }) => resolved,
638
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
639
- }).use('prop', 'code', {
640
- init: (input: Code) => input,
641
- resolve: ({ flow }) => flow.find(isCode),
642
- serialize: ({ resolved }) => resolved,
643
- deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
644
- })
645
-
646
- const error = new AppError('test', { status: 400, code: 'A' })
647
-
648
- expectTypeOf(error.own?.status).toEqualTypeOf<number | undefined>()
649
- expectTypeOf(error.own?.code).toEqualTypeOf<Code | undefined>()
650
-
651
- expectTypeOf(AppError.own(error, 'status')).toEqualTypeOf<number | undefined>()
652
- expectTypeOf(AppError.own(error, 'code')).toEqualTypeOf<Code | undefined>()
653
- expectTypeOf(AppError.own(error)).toEqualTypeOf<{ status: number | undefined; code: Code | undefined }>()
654
- expectTypeOf(AppError.flow(error, 'status')).toEqualTypeOf<Array<number | undefined>>()
655
- expectTypeOf(AppError.flow(error, 'code')).toEqualTypeOf<Array<Code | undefined>>()
656
- })
657
-
658
- it('resolve returns plain resolved props object without methods', () => {
659
- type Code = 'A' | 'B'
660
- const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
661
- const AppError = Error0.use('prop', 'status', {
662
- init: (input: number) => input,
663
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
664
- serialize: ({ resolved }) => resolved,
665
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
666
- })
667
- .use('prop', 'code', {
668
- init: (input: Code) => input,
669
- resolve: ({ flow }) => flow.find(isCode),
670
- serialize: ({ resolved }) => resolved,
671
- deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
672
- })
673
- .use('method', 'isStatus', (error, status: number) => error.status === status)
674
-
675
- const root = new AppError('root', { status: 400, code: 'A' })
676
- const leaf = new AppError('leaf', { cause: root })
677
-
678
- const resolvedStatic = AppError.resolve(leaf)
679
- const resolvedInstance = leaf.resolve()
680
- expect(resolvedStatic).toEqual({ status: 400, code: 'A' })
681
- expect(resolvedInstance).toEqual({ status: 400, code: 'A' })
682
- expect('isStatus' in resolvedStatic).toBe(false)
683
- expect(Object.keys(resolvedInstance)).toEqual(['status', 'code'])
684
-
685
- expectTypeOf(resolvedStatic).toEqualTypeOf<{ status: number; code: Code | undefined }>()
686
- })
687
-
688
- it('prop resolved type can be not undefined with init not provided', () => {
689
- const AppError = Error0.use('prop', 'x', {
690
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
691
- serialize: ({ resolved }) => resolved,
692
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
693
- })
694
-
695
- const error = new AppError('test')
696
- expect(error.x).toBe(500)
697
- expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
698
-
699
- Error0.plugin().prop('x', {
700
- init: (input: number) => input,
701
- // @ts-expect-error - resolve type extends init type
702
- resolve: ({ flow }) => 'string',
703
- serialize: ({ resolved }) => resolved,
704
- deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
705
- })
706
- })
707
-
708
- it('serialize/deserialize can be set to false to disable them', () => {
709
- const AppError = Error0.use('prop', 'status', {
710
- init: (input: number) => input,
711
- resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
712
- serialize: false,
713
- deserialize: false,
714
- })
715
- const error = new AppError('test', { status: 401 })
716
- const json = AppError.serialize(error)
717
- expect('status' in json).toBe(false)
718
-
719
- const recreated = AppError.from({ ...json, status: 999 })
720
- expect(recreated.status).toBe(undefined)
721
- })
722
-
723
- it('by default error0 created from another error has same message', () => {
724
- const schema = z.object({
725
- x: z.string(),
726
- })
727
- const parseResult = schema.safeParse({ x: 123 })
728
- const parsedError = parseResult.error
729
- assert.ok(parsedError)
730
- const error = Error0.from(parsedError)
731
- expect(error.message).toBe(parsedError.message)
732
- })
733
-
734
- it('adapt message and other props via direct transformations', () => {
735
- const schema = z.object({
736
- x: z.string(),
737
- })
738
- const parseResult = schema.safeParse({ x: 123 })
739
- const parsedError = parseResult.error
740
- assert.ok(parsedError)
741
- const AppError = Error0.use(statusPlugin)
742
- .use(codePlugin)
743
- .use('adapt', (error) => {
744
- if (error.cause instanceof ZodError) {
745
- error.status = 422
746
- error.code = 'NOT_FOUND'
747
- error.message = `Validation Error: ${error.message}`
748
- }
749
- })
750
- const error = AppError.from(parsedError)
751
- expect(error.message).toBe(`Validation Error: ${parsedError.message}`)
752
- expect(error.status).toBe(422)
753
- expect(error.code).toBe('NOT_FOUND')
754
- const error1 = new AppError('test', { cause: parsedError })
755
- expect(error1.message).toBe('test')
756
- expect(error1.status).toBe(undefined)
757
- expect(error1.code).toBe(undefined)
758
- })
759
-
760
- it('adapt message and other props via return output values from plugin', () => {
761
- const schema = z.object({
762
- x: z.string(),
763
- })
764
- const parseResult = schema.safeParse({ x: 123 })
765
- const parsedError = parseResult.error
766
- assert.ok(parsedError)
767
- const AppError = Error0.use(statusPlugin)
768
- .use(codePlugin)
769
- .use('adapt', (error) => {
770
- if (error.cause instanceof ZodError) {
771
- error.message = `Validation Error: ${error.message}`
772
- return {
773
- status: 422,
774
- code: 'NOT_FOUND',
775
- }
776
- }
777
- return undefined
778
- })
779
- const error = AppError.from(parsedError)
780
- expect(error.message).toBe(`Validation Error: ${parsedError.message}`)
781
- expect(error.status).toBe(422)
782
- expect(error.code).toBe('NOT_FOUND')
783
- const error1 = new AppError('test', { cause: parsedError })
784
- expect(error1.message).toBe('test')
785
- expect(error1.status).toBe(undefined)
786
- expect(error1.code).toBe(undefined)
787
- })
788
-
789
- it('messages can be combined on serialization', () => {
790
- const AppError = Error0.use(statusPlugin)
791
- .use(codePlugin)
792
- .use('message', {
793
- serialize: ({ error }) =>
794
- error
795
- .causes()
796
- .map((cause) => {
797
- return cause instanceof Error ? cause.message : undefined
798
- })
799
- .filter((value): value is string => typeof value === 'string')
800
- .join(': '),
801
- })
802
- const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
803
- const error2 = new AppError({ message: 'test2', status: 401, cause: error1 })
804
- expect(error1.message).toEqual('test1')
805
- expect(error2.message).toEqual('test2')
806
- expect((error2.cause as any)?.message).toEqual('test1')
807
- expect(error1.serialize().message).toEqual('test1')
808
- expect(error2.serialize().message).toEqual('test2: test1')
809
- })
810
-
811
- it('stack plugin can merge stack across causes in one serialized value', () => {
812
- const AppError = Error0.use(statusPlugin)
813
- .use(codePlugin)
814
- .use('stack', {
815
- serialize: ({ error }) =>
816
- error
817
- .causes()
818
- .map((cause) => {
819
- return cause instanceof Error ? cause.stack : undefined
820
- })
821
- .filter((value): value is string => typeof value === 'string')
822
- .join('\n'),
823
- })
824
- const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
825
- const error2 = new AppError('test2', { status: 401, cause: error1 })
826
- const mergedStack1 = error1.serialize().stack as string
827
- const mergedStack2 = error2.serialize().stack as string
828
- expect(mergedStack1).toContain('Error0: test1')
829
- expect(mergedStack2).toContain('Error0: test2')
830
- expect(mergedStack2).toContain('Error0: test1')
831
- expect(fixStack(mergedStack1)).toMatchInlineSnapshot(`
832
- "Error0: test1
833
- at <anonymous> (...)"
834
- `)
835
- expect(fixStack(mergedStack2)).toMatchInlineSnapshot(`
836
- "Error0: test2
837
- at <anonymous> (...)
838
- Error0: test1
839
- at <anonymous> (...)"
840
- `)
841
- })
842
-
843
- it('stress: resolve/serialize/flow stays within perf budget', () => {
844
- const AppError = Error0.use(statusPlugin).use(codePlugin)
845
-
846
- let current: InstanceType<typeof AppError> = new AppError('root', {
847
- status: 500,
848
- code: 'BAD_REQUEST',
849
- })
850
- for (let i = 0; i < 300; i += 1) {
851
- current = new AppError(`level-${i}`, {
852
- status: 500,
853
- code: i % 2 === 0 ? 'NOT_FOUND' : 'BAD_REQUEST',
854
- cause: current,
855
- })
856
- }
857
-
858
- let checksum = 0
859
- const startedAt = performance.now()
860
- for (let i = 0; i < 3000; i += 1) {
861
- const resolved = AppError.resolve(current)
862
- const serialized = AppError.serialize(current, false)
863
- const flow = current.flow('status')
864
- checksum += resolved.status ?? 0
865
- checksum += (serialized.status as number | undefined) ?? 0
866
- checksum += flow.length
867
- }
868
- const elapsedMs = performance.now() - startedAt
869
- const budgetMs = process.env.CI ? 8000 : 4000
870
-
871
- expect(checksum).toBeGreaterThan(0)
872
- expect(elapsedMs).toBeLessThan(budgetMs)
873
- })
874
-
875
- it('class can be created extended from Error0', () => {
876
- class MyError extends Error0.use(statusPlugin).use(codePlugin) {
877
- isStatusAndCode(status: number, code: string): boolean {
878
- return this.status === status && this.code === code
879
- }
880
- }
881
- const error = new MyError('test', { status: 400, code: 'NOT_FOUND' })
882
- expect(error.isStatusAndCode(400, 'NOT_FOUND')).toBe(true)
883
- expect(error.isStatusAndCode(400, 'BAD_REQUEST')).toBe(false)
884
- expect(error.name).toBe('Error0')
885
- expectTypeOf<typeof MyError>().toExtend<ClassError0>()
886
- })
887
-
888
- it('Error0 assignable to LikeError0', () => {
889
- class MyError extends Error {
890
- status?: number
891
- code?: string
892
- constructor(message: string, options: { cause?: unknown; status?: number; code?: string }) {
893
- super(message, { cause: options.cause })
894
- this.status = options.status
895
- this.code = options.code
896
- }
897
-
898
- static from(error: unknown): MyError {
899
- if (error instanceof MyError) {
900
- return error
901
- }
902
- const object = typeof error === 'object' && error !== null ? (error as Record<string, unknown>) : {}
903
- const message =
904
- typeof object.message === 'string' ? object.message : typeof error === 'string' ? error : 'Unknown error'
905
- const status = typeof object.status === 'number' ? object.status : undefined
906
- const code = typeof object.code === 'string' ? object.code : undefined
907
- return new MyError(message, {
908
- cause: error,
909
- status,
910
- code,
911
- })
912
- }
913
-
914
- static serialize(error: MyError): Record<string, unknown> {
915
- return {
916
- message: error.message,
917
- status: error.status,
918
- code: error.code,
919
- }
920
- }
921
- }
922
-
923
- type LikeError0<TError> = {
924
- new (message: string, options: { cause?: unknown; status?: number; code?: string }): TError
925
- from: (error: unknown) => TError
926
- serialize: (error: TError) => Record<string, unknown>
927
- }
928
- expectTypeOf<typeof MyError>().toExtend<LikeError0<MyError>>()
929
- expectTypeOf<typeof Error0>().toExtend<typeof MyError>()
930
- })
931
-
932
- // we will have no variants
933
- // becouse you can thorw any errorm and when you do AppError.from(yourError)
934
- // can use adapt to assign desired props to error, it is enough for transport
935
- // you even can create computed or method to retrieve your error, so no problems with variants
936
-
937
- // it('can create and recongnize variant', () => {
938
- // const AppError = Error0.use(statusPlugin)
939
- // .use(codePlugin)
940
- // .use('prop', 'userId', {
941
- // input: (value: string) => value,
942
- // output: (error) => {
943
- // for (const value of error.flow('userId')) {
944
- // if (typeof value === 'string') {
945
- // return value
946
- // }
947
- // }
948
- // return undefined
949
- // },
950
- // serialize: (value) => value,
951
- // })
952
- // const UserError = AppError.variant('UserError', {
953
- // userId: true,
954
- // })
955
- // const error = new UserError('test', { userId: '123', status: 400 })
956
- // expect(error).toBeInstanceOf(UserError)
957
- // expect(error).toBeInstanceOf(AppError)
958
- // expect(error).toBeInstanceOf(Error0)
959
- // expect(error).toBeInstanceOf(Error)
960
- // expect(error.userId).toBe('123')
961
- // expect(error.status).toBe(400)
962
- // expect(error.code).toBe(undefined)
963
- // expectTypeOf<typeof error.userId>().toEqualTypeOf<string>()
964
- // // @ts-expect-error
965
- // new UserError('test')
966
- // })
967
- })