@devp0nt/error0 1.0.0-next.45 → 1.0.0-next.47

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.
Files changed (57) hide show
  1. package/dist/cjs/index.cjs +80 -64
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +32 -22
  4. package/dist/cjs/plugins/cause-serialize.cjs +38 -0
  5. package/dist/cjs/plugins/cause-serialize.cjs.map +1 -0
  6. package/dist/cjs/plugins/cause-serialize.d.cts +5 -0
  7. package/dist/cjs/plugins/expected.cjs +49 -0
  8. package/dist/cjs/plugins/expected.cjs.map +1 -0
  9. package/dist/cjs/plugins/expected.d.cts +5 -0
  10. package/dist/cjs/plugins/message-merge.cjs +36 -0
  11. package/dist/cjs/plugins/message-merge.cjs.map +1 -0
  12. package/dist/cjs/plugins/message-merge.d.cts +5 -0
  13. package/dist/cjs/plugins/meta.cjs +73 -0
  14. package/dist/cjs/plugins/meta.cjs.map +1 -0
  15. package/dist/cjs/plugins/meta.d.cts +5 -0
  16. package/dist/cjs/plugins/stack-merge.cjs +39 -0
  17. package/dist/cjs/plugins/stack-merge.cjs.map +1 -0
  18. package/dist/cjs/plugins/stack-merge.d.cts +5 -0
  19. package/dist/cjs/plugins/tags.cjs +48 -0
  20. package/dist/cjs/plugins/tags.cjs.map +1 -0
  21. package/dist/cjs/plugins/tags.d.cts +5 -0
  22. package/dist/esm/index.d.ts +32 -22
  23. package/dist/esm/index.js +80 -64
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/plugins/cause-serialize.d.ts +5 -0
  26. package/dist/esm/plugins/cause-serialize.js +14 -0
  27. package/dist/esm/plugins/cause-serialize.js.map +1 -0
  28. package/dist/esm/plugins/expected.d.ts +5 -0
  29. package/dist/esm/plugins/expected.js +25 -0
  30. package/dist/esm/plugins/expected.js.map +1 -0
  31. package/dist/esm/plugins/message-merge.d.ts +5 -0
  32. package/dist/esm/plugins/message-merge.js +12 -0
  33. package/dist/esm/plugins/message-merge.js.map +1 -0
  34. package/dist/esm/plugins/meta.d.ts +5 -0
  35. package/dist/esm/plugins/meta.js +49 -0
  36. package/dist/esm/plugins/meta.js.map +1 -0
  37. package/dist/esm/plugins/stack-merge.d.ts +5 -0
  38. package/dist/esm/plugins/stack-merge.js +15 -0
  39. package/dist/esm/plugins/stack-merge.js.map +1 -0
  40. package/dist/esm/plugins/tags.d.ts +5 -0
  41. package/dist/esm/plugins/tags.js +24 -0
  42. package/dist/esm/plugins/tags.js.map +1 -0
  43. package/package.json +9 -1
  44. package/src/index.test.ts +77 -100
  45. package/src/index.ts +173 -120
  46. package/src/plugins/cause-serialize.test.ts +51 -0
  47. package/src/plugins/cause-serialize.ts +11 -0
  48. package/src/plugins/expected.test.ts +47 -0
  49. package/src/plugins/expected.ts +25 -0
  50. package/src/plugins/message-merge.test.ts +32 -0
  51. package/src/plugins/message-merge.ts +15 -0
  52. package/src/plugins/meta.test.ts +32 -0
  53. package/src/plugins/meta.ts +53 -0
  54. package/src/plugins/stack-merge.test.ts +64 -0
  55. package/src/plugins/stack-merge.ts +16 -0
  56. package/src/plugins/tags.test.ts +22 -0
  57. package/src/plugins/tags.ts +21 -0
package/src/index.test.ts CHANGED
@@ -74,7 +74,7 @@ describe('Error0', () => {
74
74
  })
75
75
 
76
76
  it('class helpers prop/method/adapt mirror use API', () => {
77
- const AppError = Error0.prop('status', {
77
+ const AppError = Error0.use('prop', 'status', {
78
78
  init: (value: number) => value,
79
79
  resolve: ({ own, flow }) => {
80
80
  return typeof own === 'number' ? own : undefined
@@ -82,8 +82,8 @@ describe('Error0', () => {
82
82
  serialize: ({ value }) => value,
83
83
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
84
84
  })
85
- .method('isStatus', (error, expectedStatus: number) => error.status === expectedStatus)
86
- .adapt((error) => {
85
+ .use('method', 'isStatus', (error, expectedStatus: number) => error.status === expectedStatus)
86
+ .use('adapt', (error) => {
87
87
  if (error.cause instanceof Error && error.status === undefined) {
88
88
  return { status: 500 }
89
89
  }
@@ -165,7 +165,7 @@ describe('Error0', () => {
165
165
  })
166
166
 
167
167
  it('serialize uses identity by default and skips undefined plugin values', () => {
168
- const AppError = Error0.use(statusPlugin).prop('code', {
168
+ const AppError = Error0.use(statusPlugin).use('prop', 'code', {
169
169
  init: (input: string) => input,
170
170
  resolve: ({ flow }) => flow.find(Boolean),
171
171
  serialize: () => undefined,
@@ -185,30 +185,38 @@ describe('Error0', () => {
185
185
  })
186
186
 
187
187
  it('stack plugin can customize stack serialization without defining prop plugin', () => {
188
- const AppError = Error0.stack(({ value }) => (value ? `custom:${value}` : undefined))
188
+ const AppError = Error0.use('stack', { serialize: ({ value }) => (value ? `custom:${value}` : undefined) })
189
189
  const error = new AppError('test')
190
190
  const json = AppError.serialize(error)
191
191
  expect(typeof json.stack).toBe('string')
192
192
  expect((json.stack as string).startsWith('custom:')).toBe(true)
193
193
  })
194
194
 
195
- it('stack plugin serialize true keeps default stack', () => {
196
- const AppError = Error0.stack(true)
195
+ it('stack plugin can keep default stack via identity function', () => {
196
+ const AppError = Error0.use('stack', { serialize: ({ value }) => value })
197
197
  const error = new AppError('test')
198
198
  const json = AppError.serialize(error)
199
199
  expect(json.stack).toBe(error.stack)
200
200
  })
201
201
 
202
- it('stack plugin serialize false disables stack serialization', () => {
203
- const AppError = Error0.stack(false)
202
+ it('stack plugin can disable stack serialization via function', () => {
203
+ const AppError = Error0.use('stack', { serialize: () => undefined })
204
204
  const error = new AppError('test')
205
205
  const json = AppError.serialize(error)
206
206
  expect('stack' in json).toBe(false)
207
207
  })
208
208
 
209
+ it('stack plugin rejects boolean config', () => {
210
+ expect(() => Error0.use('stack', true as any)).toThrow('expects { serialize: function }')
211
+ })
212
+
213
+ it('message plugin rejects boolean config', () => {
214
+ expect(() => Error0.use('message', true as any)).toThrow('expects { serialize: function }')
215
+ })
216
+
209
217
  it('prop("stack") throws and suggests using stack plugin', () => {
210
218
  expect(() =>
211
- Error0.prop('stack', {
219
+ Error0.use('prop', 'stack', {
212
220
  init: (input: string) => input,
213
221
  resolve: ({ own }) => (typeof own === 'string' ? own : undefined),
214
222
  serialize: ({ value }) => value,
@@ -228,6 +236,26 @@ describe('Error0', () => {
228
236
  ).toThrow('reserved prop key')
229
237
  })
230
238
 
239
+ it('prop("message") throws and suggests using message plugin', () => {
240
+ expect(() =>
241
+ Error0.use('prop', 'message', {
242
+ resolve: ({ own }) => own as string,
243
+ serialize: ({ value }) => value,
244
+ deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
245
+ }),
246
+ ).toThrow('reserved prop key')
247
+ })
248
+
249
+ it('plugin builder also rejects prop("message") as reserved key', () => {
250
+ expect(() =>
251
+ Error0.plugin().prop('message', {
252
+ resolve: ({ own }) => own as string,
253
+ serialize: ({ value }) => value,
254
+ deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
255
+ }),
256
+ ).toThrow('reserved prop key')
257
+ })
258
+
231
259
  it('.serialize() -> .from() roundtrip keeps plugin values', () => {
232
260
  const AppError = Error0.use(statusPlugin).use(codePlugin)
233
261
  const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
@@ -249,22 +277,12 @@ describe('Error0', () => {
249
277
  expect('cause' in json).toBe(false)
250
278
  })
251
279
 
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
280
 
281
+ it('by default causes not serialized', () => {
282
+ const AppError = Error0.use(statusPlugin).use(codePlugin)
283
+ const error = new AppError('test', { status: 400, code: 'NOT_FOUND' })
257
284
  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')
285
+ expect('cause' in json).toBe(false)
268
286
  })
269
287
 
270
288
  it('serialize can hide props for public output', () => {
@@ -277,7 +295,7 @@ describe('Error0', () => {
277
295
  })
278
296
 
279
297
  it('prop init without input arg infers undefined-only constructor input', () => {
280
- const AppError = Error0.prop('computed', {
298
+ const AppError = Error0.use('prop', 'computed', {
281
299
  init: () => undefined as number | undefined,
282
300
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
283
301
  serialize: ({ value }) => value,
@@ -294,7 +312,7 @@ describe('Error0', () => {
294
312
  })
295
313
 
296
314
  it('prop without init omits constructor input and infers resolve output', () => {
297
- const AppError = Error0.prop('statusCode', {
315
+ const AppError = Error0.use('prop', 'statusCode', {
298
316
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
299
317
  serialize: ({ value }) => value,
300
318
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
@@ -310,7 +328,7 @@ describe('Error0', () => {
310
328
  })
311
329
 
312
330
  it('prop output type is inferred from resolve type', () => {
313
- const AppError = Error0.prop('x', {
331
+ const AppError = Error0.use('prop', 'x', {
314
332
  init: (input: number) => input,
315
333
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
316
334
  serialize: ({ value }) => value,
@@ -323,7 +341,7 @@ describe('Error0', () => {
323
341
  expectTypeOf(AppError.own(error, 'x')).toEqualTypeOf<number | undefined>()
324
342
  expectTypeOf(AppError.flow(error, 'x')).toEqualTypeOf<Array<number | undefined>>()
325
343
 
326
- Error0.prop('x', {
344
+ Error0.plugin().prop('x', {
327
345
  init: (input: number) => input,
328
346
  // @ts-expect-error - resolve type extends init type
329
347
  resolve: ({ flow }) => 'string',
@@ -333,7 +351,7 @@ describe('Error0', () => {
333
351
  })
334
352
 
335
353
  it('own/flow are typed by output type, not resolve type', () => {
336
- const AppError = Error0.prop('code', {
354
+ const AppError = Error0.use('prop', 'code', {
337
355
  init: (input: number | 'fallback') => input,
338
356
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
339
357
  serialize: ({ value }) => value,
@@ -354,12 +372,12 @@ describe('Error0', () => {
354
372
  it('own/flow runtime behavior across causes', () => {
355
373
  type Code = 'A' | 'B'
356
374
  const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
357
- const AppError = Error0.prop('status', {
375
+ const AppError = Error0.use('prop', 'status', {
358
376
  init: (input: number) => input,
359
377
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
360
378
  serialize: ({ value }) => value,
361
379
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
362
- }).prop('code', {
380
+ }).use('prop', 'code', {
363
381
  init: (input: Code) => input,
364
382
  resolve: ({ flow }) => flow.find(isCode),
365
383
  serialize: ({ value }) => value,
@@ -381,12 +399,12 @@ describe('Error0', () => {
381
399
  it('own/flow have strong types for static and instance methods', () => {
382
400
  type Code = 'A' | 'B'
383
401
  const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
384
- const AppError = Error0.prop('status', {
402
+ const AppError = Error0.use('prop', 'status', {
385
403
  init: (input: number) => input,
386
404
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
387
405
  serialize: ({ value }) => value,
388
406
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
389
- }).prop('code', {
407
+ }).use('prop', 'code', {
390
408
  init: (input: Code) => input,
391
409
  resolve: ({ flow }) => flow.find(isCode),
392
410
  serialize: ({ value }) => value,
@@ -408,19 +426,19 @@ describe('Error0', () => {
408
426
  it('resolve returns plain resolved props object without methods', () => {
409
427
  type Code = 'A' | 'B'
410
428
  const isCode = (item: unknown): item is Code => item === 'A' || item === 'B'
411
- const AppError = Error0.prop('status', {
429
+ const AppError = Error0.use('prop', 'status', {
412
430
  init: (input: number) => input,
413
431
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number') ?? 500,
414
432
  serialize: ({ value }) => value,
415
433
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
416
434
  })
417
- .prop('code', {
435
+ .use('prop', 'code', {
418
436
  init: (input: Code) => input,
419
437
  resolve: ({ flow }) => flow.find(isCode),
420
438
  serialize: ({ value }) => value,
421
439
  deserialize: ({ value }) => (value === 'A' || value === 'B' ? value : undefined),
422
440
  })
423
- .method('isStatus', (error, status: number) => error.status === status)
441
+ .use('method', 'isStatus', (error, status: number) => error.status === status)
424
442
 
425
443
  const root = new AppError('root', { status: 400, code: 'A' })
426
444
  const leaf = new AppError('leaf', { cause: root })
@@ -436,7 +454,7 @@ describe('Error0', () => {
436
454
  })
437
455
 
438
456
  it('prop resolved type can be not undefined with init not provided', () => {
439
- const AppError = Error0.prop('x', {
457
+ const AppError = Error0.use('prop', 'x', {
440
458
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number') || 500,
441
459
  serialize: ({ value }) => value,
442
460
  deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
@@ -446,7 +464,7 @@ describe('Error0', () => {
446
464
  expect(error.x).toBe(500)
447
465
  expectTypeOf<typeof error.x>().toEqualTypeOf<number>()
448
466
 
449
- Error0.prop('x', {
467
+ Error0.plugin().prop('x', {
450
468
  init: (input: number) => input,
451
469
  // @ts-expect-error - resolve type extends init type
452
470
  resolve: ({ flow }) => 'string',
@@ -456,7 +474,7 @@ describe('Error0', () => {
456
474
  })
457
475
 
458
476
  it('serialize/deserialize can be set to false to disable them', () => {
459
- const AppError = Error0.prop('status', {
477
+ const AppError = Error0.use('prop', 'status', {
460
478
  init: (input: number) => input,
461
479
  resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
462
480
  serialize: false,
@@ -516,7 +534,7 @@ describe('Error0', () => {
516
534
  assert.ok(parsedError)
517
535
  const AppError = Error0.use(statusPlugin)
518
536
  .use(codePlugin)
519
- .adapt((error) => {
537
+ .use('adapt', (error) => {
520
538
  if (error.cause instanceof ZodError) {
521
539
  error.message = `Validation Error: ${error.message}`
522
540
  return {
@@ -536,41 +554,19 @@ describe('Error0', () => {
536
554
  expect(error1.code).toBe(undefined)
537
555
  })
538
556
 
539
- it('expected prop can be realized to send or not to send error to your error tracker', () => {
540
- const AppError = Error0.use(statusPlugin)
541
- .prop('expected', {
542
- init: (input: boolean) => input,
543
- resolve: ({ flow }) => flow.find((value) => typeof value === 'boolean'),
544
- serialize: ({ value }) => value,
545
- deserialize: ({ value }) => (typeof value === 'boolean' ? value : undefined),
546
- })
547
- .method('isExpected', (error) => {
548
- return error.expected ?? false
549
- })
550
- const errorExpected = new AppError('test', { status: 400, expected: true })
551
- const errorUnexpected = new AppError('test', { status: 400, expected: false })
552
- const usualError = new Error('test')
553
- const errorFromUsualError = AppError.from(usualError)
554
- const errorWithExpectedErrorAsCause = new AppError('test', { status: 400, cause: errorExpected })
555
- const errorWithUnexpectedErrorAsCause = new AppError('test', { status: 400, cause: errorUnexpected })
556
- expect(errorExpected.expected).toBe(true)
557
- expect(errorUnexpected.expected).toBe(false)
558
- expect(AppError.isExpected(usualError)).toBe(false)
559
- expect(errorFromUsualError.expected).toBe(undefined)
560
- expect(errorFromUsualError.isExpected()).toBe(false)
561
- expect(errorWithExpectedErrorAsCause.expected).toBe(true)
562
- expect(errorWithExpectedErrorAsCause.isExpected()).toBe(true)
563
- expect(errorWithUnexpectedErrorAsCause.expected).toBe(false)
564
- expect(errorWithUnexpectedErrorAsCause.isExpected()).toBe(false)
565
- })
566
557
 
567
558
  it('messages can be combined on serialization', () => {
568
559
  const AppError = Error0.use(statusPlugin)
569
560
  .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),
561
+ .use('message', {
562
+ serialize: ({ error }) =>
563
+ error
564
+ .causes()
565
+ .map((cause) => {
566
+ return cause instanceof Error ? cause.message : undefined
567
+ })
568
+ .filter((value): value is string => typeof value === 'string')
569
+ .join(': '),
574
570
  })
575
571
  const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
576
572
  const error2 = new AppError({ message: 'test2', status: 401, cause: error1 })
@@ -584,15 +580,16 @@ describe('Error0', () => {
584
580
  it('stack plugin can merge stack across causes in one serialized value', () => {
585
581
  const AppError = Error0.use(statusPlugin)
586
582
  .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
- )
583
+ .use('stack', {
584
+ serialize: ({ error }) =>
585
+ error
586
+ .causes()
587
+ .map((cause) => {
588
+ return cause instanceof Error ? cause.stack : undefined
589
+ })
590
+ .filter((value): value is string => typeof value === 'string')
591
+ .join('\n'),
592
+ })
596
593
  const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
597
594
  const error2 = new AppError('test2', { status: 401, cause: error1 })
598
595
  const mergedStack1 = error1.serialize().stack as string
@@ -612,26 +609,6 @@ describe('Error0', () => {
612
609
  `)
613
610
  })
614
611
 
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
612
 
636
613
  it('Error0 assignable to LikeError0', () => {
637
614
  type LikeError0 = {