@devp0nt/error0 1.0.0-next.43 → 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/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: ({ value, flow }) => {
80
- return typeof value === 'number' ? value : undefined
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 of stack prop', () => {
188
- const AppError = Error0.prop('stack', {
189
- init: (input: string) => input,
190
- resolve: ({ value }) => (typeof value === 'string' ? value : undefined),
191
- serialize: ({ value }) => undefined,
192
- deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
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: ({ value, flow }) => value ?? flow.find((item) => typeof item === 'number'),
461
+ resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
307
462
  serialize: false,
308
463
  deserialize: false,
309
464
  })
@@ -409,6 +564,75 @@ 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
+
412
636
  // we will have no variants
413
637
  // becouse you can thorw any errorm and when you do AppError.from(yourError)
414
638
  // can use adapt to assign desired props to error, it is enough for transport