@devp0nt/error0 1.0.0-next.42 → 1.0.0-next.44
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 +204 -20
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +108 -37
- package/dist/esm/index.d.ts +108 -37
- package/dist/esm/index.js +204 -20
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.test.ts +293 -12
- package/src/index.ts +444 -81
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' })
|
|
@@ -243,10 +293,172 @@ describe('Error0', () => {
|
|
|
243
293
|
new AppError('test', { computed: 123 })
|
|
244
294
|
})
|
|
245
295
|
|
|
296
|
+
it('prop without init omits constructor input and infers resolve output', () => {
|
|
297
|
+
const AppError = Error0.prop('statusCode', {
|
|
298
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
299
|
+
serialize: ({ value }) => value,
|
|
300
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const error = new AppError('test')
|
|
304
|
+
expect(error.statusCode).toBe(undefined)
|
|
305
|
+
expectTypeOf<typeof error.statusCode>().toEqualTypeOf<number | undefined>()
|
|
306
|
+
|
|
307
|
+
// @ts-expect-error - statusCode input is disallowed when init is omitted
|
|
308
|
+
// eslint-disable-next-line no-new
|
|
309
|
+
new AppError('test', { statusCode: 123 })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('prop output type is inferred from resolve type', () => {
|
|
313
|
+
const AppError = Error0.prop('x', {
|
|
314
|
+
init: (input: number) => input,
|
|
315
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
|
|
316
|
+
serialize: ({ value }) => value,
|
|
317
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const error = new AppError('test')
|
|
321
|
+
expect(error.x).toBe(500)
|
|
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>>()
|
|
325
|
+
|
|
326
|
+
Error0.prop('x', {
|
|
327
|
+
init: (input: number) => input,
|
|
328
|
+
// @ts-expect-error - resolve type extends init type
|
|
329
|
+
resolve: ({ flow }) => 'string',
|
|
330
|
+
serialize: ({ value }) => value,
|
|
331
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
332
|
+
})
|
|
333
|
+
})
|
|
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
|
+
|
|
438
|
+
it('prop resolved type can be not undefined with init not provided', () => {
|
|
439
|
+
const AppError = Error0.prop('x', {
|
|
440
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
|
|
441
|
+
serialize: ({ value }) => value,
|
|
442
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
const error = new AppError('test')
|
|
446
|
+
expect(error.x).toBe(500)
|
|
447
|
+
expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
|
|
448
|
+
|
|
449
|
+
Error0.prop('x', {
|
|
450
|
+
init: (input: number) => input,
|
|
451
|
+
// @ts-expect-error - resolve type extends init type
|
|
452
|
+
resolve: ({ flow }) => 'string',
|
|
453
|
+
serialize: ({ value }) => value,
|
|
454
|
+
deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
246
458
|
it('serialize/deserialize can be set to false to disable them', () => {
|
|
247
459
|
const AppError = Error0.prop('status', {
|
|
248
460
|
init: (input: number) => input,
|
|
249
|
-
resolve: ({
|
|
461
|
+
resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
|
|
250
462
|
serialize: false,
|
|
251
463
|
deserialize: false,
|
|
252
464
|
})
|
|
@@ -352,6 +564,75 @@ describe('Error0', () => {
|
|
|
352
564
|
expect(errorWithUnexpectedErrorAsCause.isExpected()).toBe(false)
|
|
353
565
|
})
|
|
354
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
|
+
|
|
355
636
|
// we will have no variants
|
|
356
637
|
// becouse you can thorw any errorm and when you do AppError.from(yourError)
|
|
357
638
|
// can use adapt to assign desired props to error, it is enough for transport
|