@devp0nt/error0 1.0.0-next.43 → 1.0.0-next.45
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 +197 -19
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +75 -12
- package/dist/esm/index.d.ts +75 -12
- package/dist/esm/index.js +197 -19
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.test.ts +247 -12
- package/src/index.ts +327 -36
package/src/index.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'bun:test'
|
|
2
|
+
import * as assert from 'node:assert'
|
|
3
|
+
import z, { ZodError } from 'zod'
|
|
2
4
|
import type { ClassError0 } from './index.js'
|
|
3
5
|
import { Error0 } from './index.js'
|
|
4
|
-
import z, { ZodError } from 'zod'
|
|
5
|
-
import * as assert from 'node:assert'
|
|
6
6
|
|
|
7
7
|
const fixStack = (stack: string | undefined) => {
|
|
8
8
|
if (!stack) {
|
|
@@ -76,8 +76,8 @@ describe('Error0', () => {
|
|
|
76
76
|
it('class helpers prop/method/adapt mirror use API', () => {
|
|
77
77
|
const AppError = Error0.prop('status', {
|
|
78
78
|
init: (value: number) => value,
|
|
79
|
-
resolve: ({
|
|
80
|
-
return typeof
|
|
79
|
+
resolve: ({ own, flow }) => {
|
|
80
|
+
return typeof own === 'number' ? own : undefined
|
|
81
81
|
},
|
|
82
82
|
serialize: ({ value }) => value,
|
|
83
83
|
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
@@ -184,18 +184,50 @@ describe('Error0', () => {
|
|
|
184
184
|
expect(json.stack).toBe(error.stack)
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
it('stack plugin can customize serialization
|
|
188
|
-
const AppError = Error0.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
it('stack plugin can customize stack serialization without defining prop plugin', () => {
|
|
188
|
+
const AppError = Error0.stack(({ value }) => (value ? `custom:${value}` : undefined))
|
|
189
|
+
const error = new AppError('test')
|
|
190
|
+
const json = AppError.serialize(error)
|
|
191
|
+
expect(typeof json.stack).toBe('string')
|
|
192
|
+
expect((json.stack as string).startsWith('custom:')).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('stack plugin serialize true keeps default stack', () => {
|
|
196
|
+
const AppError = Error0.stack(true)
|
|
197
|
+
const error = new AppError('test')
|
|
198
|
+
const json = AppError.serialize(error)
|
|
199
|
+
expect(json.stack).toBe(error.stack)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('stack plugin serialize false disables stack serialization', () => {
|
|
203
|
+
const AppError = Error0.stack(false)
|
|
194
204
|
const error = new AppError('test')
|
|
195
205
|
const json = AppError.serialize(error)
|
|
196
206
|
expect('stack' in json).toBe(false)
|
|
197
207
|
})
|
|
198
208
|
|
|
209
|
+
it('prop("stack") throws and suggests using stack plugin', () => {
|
|
210
|
+
expect(() =>
|
|
211
|
+
Error0.prop('stack', {
|
|
212
|
+
init: (input: string) => input,
|
|
213
|
+
resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
|
|
214
|
+
serialize: ({ value }) => value,
|
|
215
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
216
|
+
}),
|
|
217
|
+
).toThrow('reserved prop key')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('plugin builder also rejects prop("stack") as reserved key', () => {
|
|
221
|
+
expect(() =>
|
|
222
|
+
Error0.plugin().prop('stack', {
|
|
223
|
+
init: (input: string) => input,
|
|
224
|
+
resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
|
|
225
|
+
serialize: ({ value }) => value,
|
|
226
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
227
|
+
}),
|
|
228
|
+
).toThrow('reserved prop key')
|
|
229
|
+
})
|
|
230
|
+
|
|
199
231
|
it('.serialize() -> .from() roundtrip keeps plugin values', () => {
|
|
200
232
|
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
201
233
|
const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
|
|
@@ -217,6 +249,24 @@ describe('Error0', () => {
|
|
|
217
249
|
expect('cause' in json).toBe(false)
|
|
218
250
|
})
|
|
219
251
|
|
|
252
|
+
it('cause plugin true serializes and deserializes nested Error0 causes', () => {
|
|
253
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin).cause(true)
|
|
254
|
+
const causeError = new AppError('cause', { status: 409, code: 'NOT_FOUND' })
|
|
255
|
+
const error = new AppError('root', { status: 500, cause: causeError })
|
|
256
|
+
|
|
257
|
+
const json = AppError.serialize(error, false)
|
|
258
|
+
expect(typeof json.cause).toBe('object')
|
|
259
|
+
expect((json.cause as Record<string, unknown>).message).toBe('cause')
|
|
260
|
+
expect((json.cause as Record<string, unknown>).status).toBe(409)
|
|
261
|
+
expect((json.cause as Record<string, unknown>).code).toBe('NOT_FOUND')
|
|
262
|
+
|
|
263
|
+
const recreated = AppError.from(json)
|
|
264
|
+
expect(recreated).toBeInstanceOf(AppError)
|
|
265
|
+
expect(recreated.cause).toBeInstanceOf(AppError)
|
|
266
|
+
expect((recreated.cause as InstanceType<typeof AppError>).status).toBe(409)
|
|
267
|
+
expect((recreated.cause as InstanceType<typeof AppError>).code).toBe('NOT_FOUND')
|
|
268
|
+
})
|
|
269
|
+
|
|
220
270
|
it('serialize can hide props for public output', () => {
|
|
221
271
|
const AppError = Error0.use(statusPlugin).use(codePlugin)
|
|
222
272
|
const error = new AppError('test', { status: 401, code: 'NOT_FOUND' })
|
|
@@ -270,6 +320,8 @@ describe('Error0', () => {
|
|
|
270
320
|
const error = new AppError('test')
|
|
271
321
|
expect(error.x).toBe(500)
|
|
272
322
|
expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
|
|
323
|
+
expectTypeOf(AppError.own(error, 'x')).toEqualTypeOf<number | undefined>()
|
|
324
|
+
expectTypeOf(AppError.flow(error, 'x')).toEqualTypeOf<Array<number | undefined>>()
|
|
273
325
|
|
|
274
326
|
Error0.prop('x', {
|
|
275
327
|
init: (input: number) => input,
|
|
@@ -280,6 +332,109 @@ describe('Error0', () => {
|
|
|
280
332
|
})
|
|
281
333
|
})
|
|
282
334
|
|
|
335
|
+
it('own/flow are typed by output type, not resolve type', () => {
|
|
336
|
+
const AppError = Error0.prop('code', {
|
|
337
|
+
init: (input: number | 'fallback') => input,
|
|
338
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
|
|
339
|
+
serialize: ({ value }) => value,
|
|
340
|
+
deserialize: ({ value }) => (typeof value === 'number' || value === 'fallback' ? value : undefined),
|
|
341
|
+
})
|
|
342
|
+
const error = new AppError('test')
|
|
343
|
+
|
|
344
|
+
expect(error.code).toBe(500)
|
|
345
|
+
expect(AppError.own(error, 'code')).toBe(undefined)
|
|
346
|
+
expect(AppError.own(error)).toEqual({ code: undefined })
|
|
347
|
+
expect(error.own()).toEqual({ code: undefined })
|
|
348
|
+
expectTypeOf<typeof error.code>().toEqualTypeOf<number>()
|
|
349
|
+
expectTypeOf(AppError.own(error, 'code')).toEqualTypeOf<number | 'fallback' | undefined>()
|
|
350
|
+
expectTypeOf(AppError.own(error)).toEqualTypeOf<{ code: number | 'fallback' | undefined }>()
|
|
351
|
+
expectTypeOf(AppError.flow(error, 'code')).toEqualTypeOf<Array<number | 'fallback' | undefined>>()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('own/flow runtime behavior across causes', () => {
|
|
355
|
+
type Code = 'A' | 'B'
|
|
356
|
+
const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
|
|
357
|
+
const AppError = Error0.prop('status', {
|
|
358
|
+
init: (input: number) => input,
|
|
359
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
360
|
+
serialize: ({ value }) => value,
|
|
361
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
362
|
+
}).prop('code', {
|
|
363
|
+
init: (input: Code) => input,
|
|
364
|
+
resolve: ({ flow }) => flow.find(isCode),
|
|
365
|
+
serialize: ({ value }) => value,
|
|
366
|
+
deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const root = new AppError('root', { status: 400, code: 'A' })
|
|
370
|
+
const mid = new AppError('mid', { cause: root })
|
|
371
|
+
const leaf = new AppError('leaf', { status: 500, cause: mid })
|
|
372
|
+
|
|
373
|
+
expect(leaf.own()).toEqual({ status: 500, code: undefined })
|
|
374
|
+
expect(AppError.own(leaf)).toEqual({ status: 500, code: undefined })
|
|
375
|
+
expect(leaf.flow('status')).toEqual([500, undefined, 400])
|
|
376
|
+
expect(AppError.flow(leaf, 'status')).toEqual([500, undefined, 400])
|
|
377
|
+
expect(leaf.flow('code')).toEqual([undefined, undefined, 'A'])
|
|
378
|
+
expect(AppError.flow(leaf, 'code')).toEqual([undefined, undefined, 'A'])
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('own/flow have strong types for static and instance methods', () => {
|
|
382
|
+
type Code = 'A' | 'B'
|
|
383
|
+
const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
|
|
384
|
+
const AppError = Error0.prop('status', {
|
|
385
|
+
init: (input: number) => input,
|
|
386
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
387
|
+
serialize: ({ value }) => value,
|
|
388
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
389
|
+
}).prop('code', {
|
|
390
|
+
init: (input: Code) => input,
|
|
391
|
+
resolve: ({ flow }) => flow.find(isCode),
|
|
392
|
+
serialize: ({ value }) => value,
|
|
393
|
+
deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
const error = new AppError('test', { status: 400, code: 'A' })
|
|
397
|
+
|
|
398
|
+
expectTypeOf(error.own('status')).toEqualTypeOf<number | undefined>()
|
|
399
|
+
expectTypeOf(error.own('code')).toEqualTypeOf<Code | undefined>()
|
|
400
|
+
|
|
401
|
+
expectTypeOf(AppError.own(error, 'status')).toEqualTypeOf<number | undefined>()
|
|
402
|
+
expectTypeOf(AppError.own(error, 'code')).toEqualTypeOf<Code | undefined>()
|
|
403
|
+
expectTypeOf(AppError.own(error)).toEqualTypeOf<{ status: number | undefined; code: Code | undefined }>()
|
|
404
|
+
expectTypeOf(AppError.flow(error, 'status')).toEqualTypeOf<Array<number | undefined>>()
|
|
405
|
+
expectTypeOf(AppError.flow(error, 'code')).toEqualTypeOf<Array<Code | undefined>>()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('resolve returns plain resolved props object without methods', () => {
|
|
409
|
+
type Code = 'A' | 'B'
|
|
410
|
+
const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
|
|
411
|
+
const AppError = Error0.prop('status', {
|
|
412
|
+
init: (input: number) => input,
|
|
413
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
|
|
414
|
+
serialize: ({ value }) => value,
|
|
415
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
416
|
+
})
|
|
417
|
+
.prop('code', {
|
|
418
|
+
init: (input: Code) => input,
|
|
419
|
+
resolve: ({ flow }) => flow.find(isCode),
|
|
420
|
+
serialize: ({ value }) => value,
|
|
421
|
+
deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
|
|
422
|
+
})
|
|
423
|
+
.method('isStatus', (error, status: number) => error.status === status)
|
|
424
|
+
|
|
425
|
+
const root = new AppError('root', { status: 400, code: 'A' })
|
|
426
|
+
const leaf = new AppError('leaf', { cause: root })
|
|
427
|
+
|
|
428
|
+
const resolvedStatic = AppError.resolve(leaf)
|
|
429
|
+
const resolvedInstance = leaf.resolve()
|
|
430
|
+
expect(resolvedStatic).toEqual({ status: 400, code: 'A' })
|
|
431
|
+
expect(resolvedInstance).toEqual({ status: 400, code: 'A' })
|
|
432
|
+
expect('isStatus' in resolvedStatic).toBe(false)
|
|
433
|
+
expect(Object.keys(resolvedInstance)).toEqual(['status', 'code'])
|
|
434
|
+
|
|
435
|
+
expectTypeOf(resolvedStatic).toEqualTypeOf<{ status: number; code: Code | undefined }>()
|
|
436
|
+
})
|
|
437
|
+
|
|
283
438
|
it('prop resolved type can be not undefined with init not provided', () => {
|
|
284
439
|
const AppError = Error0.prop('x', {
|
|
285
440
|
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
|
|
@@ -303,7 +458,7 @@ describe('Error0', () => {
|
|
|
303
458
|
it('serialize/deserialize can be set to false to disable them', () => {
|
|
304
459
|
const AppError = Error0.prop('status', {
|
|
305
460
|
init: (input: number) => input,
|
|
306
|
-
resolve: ({
|
|
461
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
307
462
|
serialize: false,
|
|
308
463
|
deserialize: false,
|
|
309
464
|
})
|
|
@@ -409,6 +564,86 @@ describe('Error0', () => {
|
|
|
409
564
|
expect(errorWithUnexpectedErrorAsCause.isExpected()).toBe(false)
|
|
410
565
|
})
|
|
411
566
|
|
|
567
|
+
it('messages can be combined on serialization', () => {
|
|
568
|
+
const AppError = Error0.use(statusPlugin)
|
|
569
|
+
.use(codePlugin)
|
|
570
|
+
.prop('message', {
|
|
571
|
+
resolve: ({ own }) => own as string,
|
|
572
|
+
serialize: ({ value, error }) => error.flow('message').join(': '),
|
|
573
|
+
deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
|
|
574
|
+
})
|
|
575
|
+
const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
|
|
576
|
+
const error2 = new AppError({ message: 'test2', status: 401, cause: error1 })
|
|
577
|
+
expect(error1.message).toEqual('test1')
|
|
578
|
+
expect(error2.message).toEqual('test2')
|
|
579
|
+
expect((error2.cause as any)?.message).toEqual('test1')
|
|
580
|
+
expect(error1.serialize().message).toEqual('test1')
|
|
581
|
+
expect(error2.serialize().message).toEqual('test2: test1')
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('stack plugin can merge stack across causes in one serialized value', () => {
|
|
585
|
+
const AppError = Error0.use(statusPlugin)
|
|
586
|
+
.use(codePlugin)
|
|
587
|
+
.stack(({ error }) =>
|
|
588
|
+
error
|
|
589
|
+
.causes()
|
|
590
|
+
.map((cause) => {
|
|
591
|
+
return cause instanceof Error ? cause.stack : undefined
|
|
592
|
+
})
|
|
593
|
+
.filter((value): value is string => typeof value === 'string')
|
|
594
|
+
.join('\n'),
|
|
595
|
+
)
|
|
596
|
+
const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
|
|
597
|
+
const error2 = new AppError('test2', { status: 401, cause: error1 })
|
|
598
|
+
const mergedStack1 = error1.serialize().stack as string
|
|
599
|
+
const mergedStack2 = error2.serialize().stack as string
|
|
600
|
+
expect(mergedStack1).toContain('Error0: test1')
|
|
601
|
+
expect(mergedStack2).toContain('Error0: test2')
|
|
602
|
+
expect(mergedStack2).toContain('Error0: test1')
|
|
603
|
+
expect(fixStack(mergedStack1)).toMatchInlineSnapshot(`
|
|
604
|
+
"Error0: test1
|
|
605
|
+
at <anonymous> (...)"
|
|
606
|
+
`)
|
|
607
|
+
expect(fixStack(mergedStack2)).toMatchInlineSnapshot(`
|
|
608
|
+
"Error0: test2
|
|
609
|
+
at <anonymous> (...)
|
|
610
|
+
Error0: test1
|
|
611
|
+
at <anonymous> (...)"
|
|
612
|
+
`)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('stack plugin can merge stack across causes in one serialized value by helper "merge"', () => {
|
|
616
|
+
const AppError = Error0.use(statusPlugin).use(codePlugin).stack('merge')
|
|
617
|
+
const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
|
|
618
|
+
const error2 = new AppError('test2', { status: 401, cause: error1 })
|
|
619
|
+
const mergedStack1 = error1.serialize().stack as string
|
|
620
|
+
const mergedStack2 = error2.serialize().stack as string
|
|
621
|
+
expect(mergedStack1).toContain('Error0: test1')
|
|
622
|
+
expect(mergedStack2).toContain('Error0: test2')
|
|
623
|
+
expect(mergedStack2).toContain('Error0: test1')
|
|
624
|
+
expect(fixStack(mergedStack1)).toMatchInlineSnapshot(`
|
|
625
|
+
"Error0: test1
|
|
626
|
+
at <anonymous> (...)"
|
|
627
|
+
`)
|
|
628
|
+
expect(fixStack(mergedStack2)).toMatchInlineSnapshot(`
|
|
629
|
+
"Error0: test2
|
|
630
|
+
at <anonymous> (...)
|
|
631
|
+
Error0: test1
|
|
632
|
+
at <anonymous> (...)"
|
|
633
|
+
`)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('Error0 assignable to LikeError0', () => {
|
|
637
|
+
type LikeError0 = {
|
|
638
|
+
from: (error: unknown) => Error
|
|
639
|
+
serialize: (error: Error) => Record<string, unknown>
|
|
640
|
+
}
|
|
641
|
+
expectTypeOf<typeof Error0>().toExtend<LikeError0>()
|
|
642
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
643
|
+
const AppError = Error0.use(statusPlugin)
|
|
644
|
+
expectTypeOf<typeof AppError>().toExtend<LikeError0>()
|
|
645
|
+
})
|
|
646
|
+
|
|
412
647
|
// we will have no variants
|
|
413
648
|
// becouse you can thorw any errorm and when you do AppError.from(yourError)
|
|
414
649
|
// can use adapt to assign desired props to error, it is enough for transport
|