@devp0nt/error0 1.0.0-next.46 → 1.0.0-next.48

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 +87 -49
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +35 -12
  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 +35 -12
  23. package/dist/esm/index.js +87 -49
  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 +74 -82
  45. package/src/index.ts +130 -64
  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.ts CHANGED
@@ -59,15 +59,24 @@ export type ErrorPluginAdaptFn<
59
59
  TError extends Error0 = Error0,
60
60
  TOutputProps extends Record<string, unknown> = Record<never, never>,
61
61
  > = ((error: TError) => void) | ((error: TError) => ErrorPluginAdaptResult<TOutputProps>)
62
- export type ErrorPluginStackSerialize<TError extends Error0> =
63
- | ((options: { value: string | undefined; error: TError; isPublic: boolean }) => unknown)
64
- | boolean
65
- | 'merge'
66
- export type ErrorPluginStack<TError extends Error0 = Error0> = ErrorPluginStackSerialize<TError>
67
- export type ErrorPluginCauseSerialize<TError extends Error0> =
68
- | ((options: { value: unknown; error: TError; isPublic: boolean }) => unknown)
69
- | boolean
70
- export type ErrorPluginCause<TError extends Error0 = Error0> = ErrorPluginCauseSerialize<TError>
62
+ export type ErrorPluginStackSerialize<TError extends Error0> = (options: {
63
+ value: string | undefined
64
+ error: TError
65
+ isPublic: boolean
66
+ }) => unknown
67
+ export type ErrorPluginStack<TError extends Error0 = Error0> = { serialize: ErrorPluginStackSerialize<TError> }
68
+ export type ErrorPluginCauseSerialize<TError extends Error0> = (options: {
69
+ value: unknown
70
+ error: TError
71
+ isPublic: boolean
72
+ }) => unknown
73
+ export type ErrorPluginCause<TError extends Error0 = Error0> = { serialize: ErrorPluginCauseSerialize<TError> }
74
+ export type ErrorPluginMessageSerialize<TError extends Error0> = (options: {
75
+ value: string
76
+ error: TError
77
+ isPublic: boolean
78
+ }) => unknown
79
+ export type ErrorPluginMessage<TError extends Error0 = Error0> = { serialize: ErrorPluginMessageSerialize<TError> }
71
80
  type ErrorMethodRecord = {
72
81
  args: unknown[]
73
82
  output: unknown
@@ -85,6 +94,7 @@ export type ErrorPlugin<
85
94
  adapt?: Array<ErrorPluginAdaptFn<Error0, PluginOutputProps<TProps>>>
86
95
  stack?: ErrorPluginStack
87
96
  cause?: ErrorPluginCause
97
+ message?: ErrorPluginMessage
88
98
  }
89
99
  type AddPropToPluginProps<
90
100
  TProps extends ErrorPluginProps,
@@ -169,8 +179,10 @@ type ErrorPluginResolved = {
169
179
  adapt: Array<ErrorPluginAdaptFn<Error0, Record<string, unknown>>>
170
180
  stack?: ErrorPluginStack
171
181
  cause?: ErrorPluginCause
182
+ message?: ErrorPluginMessage
172
183
  }
173
184
  const RESERVED_STACK_PROP_ERROR = 'Error0: "stack" is a reserved prop key. Use .stack(...) plugin API instead'
185
+ const RESERVED_MESSAGE_PROP_ERROR = 'Error0: "message" is a reserved prop key. Use .message(...) plugin API instead'
174
186
 
175
187
  type PluginPropsMapOf<TPlugin extends ErrorPlugin> = {
176
188
  [TKey in keyof NonNullable<TPlugin['props']>]: NonNullable<TPlugin['props']>[TKey] extends ErrorPluginPropOptions<
@@ -227,7 +239,7 @@ type PluginsMapOf<TClass> = TClass extends { __pluginsMap?: infer TPluginsMap }
227
239
  ? TPluginsMap
228
240
  : EmptyPluginsMap
229
241
  : EmptyPluginsMap
230
- type PluginsMapOfInstance<TInstance> = TInstance extends { constructor: { __pluginsMap?: infer TPluginsMap } }
242
+ type PluginsMapOfInstance<TInstance> = TInstance extends { __pluginsMap?: infer TPluginsMap }
231
243
  ? TPluginsMap extends ErrorPluginsMap
232
244
  ? TPluginsMap
233
245
  : EmptyPluginsMap
@@ -262,6 +274,7 @@ export class PluginError0<
262
274
  adapt: [...(plugin?.adapt ?? [])],
263
275
  stack: plugin?.stack,
264
276
  cause: plugin?.cause,
277
+ message: plugin?.message,
265
278
  }
266
279
  }
267
280
 
@@ -298,6 +311,10 @@ export class PluginError0<
298
311
  return this.use('cause', value)
299
312
  }
300
313
 
314
+ message(value: ErrorPluginMessage<BuilderError0<TProps, TMethods>>): PluginError0<TProps, TMethods> {
315
+ return this.use('message', value)
316
+ }
317
+
301
318
  use<
302
319
  TKey extends string,
303
320
  TInputValue = undefined,
@@ -319,8 +336,9 @@ export class PluginError0<
319
336
  ): PluginError0<TProps, TMethods>
320
337
  use(kind: 'stack', value: ErrorPluginStack<BuilderError0<TProps, TMethods>>): PluginError0<TProps, TMethods>
321
338
  use(kind: 'cause', value: ErrorPluginCause<BuilderError0<TProps, TMethods>>): PluginError0<TProps, TMethods>
339
+ use(kind: 'message', value: ErrorPluginMessage<BuilderError0<TProps, TMethods>>): PluginError0<TProps, TMethods>
322
340
  use(
323
- kind: 'prop' | 'method' | 'adapt' | 'stack' | 'cause',
341
+ kind: 'prop' | 'method' | 'adapt' | 'stack' | 'cause' | 'message',
324
342
  keyOrValue: unknown,
325
343
  value?: ErrorPluginPropOptions<unknown, unknown, any> | ErrorPluginMethodFn<unknown, unknown[], any>,
326
344
  ): PluginError0<any, any> {
@@ -329,11 +347,15 @@ export class PluginError0<
329
347
  const nextAdapt: Array<ErrorPluginAdaptFn<Error0, Record<string, unknown>>> = [...(this._plugin.adapt ?? [])]
330
348
  let nextStack: ErrorPluginStack | undefined = this._plugin.stack
331
349
  let nextCause: ErrorPluginCause | undefined = this._plugin.cause
350
+ let nextMessage: ErrorPluginMessage | undefined = this._plugin.message
332
351
  if (kind === 'prop') {
333
352
  const key = keyOrValue as string
334
353
  if (key === 'stack') {
335
354
  throw new Error(RESERVED_STACK_PROP_ERROR)
336
355
  }
356
+ if (key === 'message') {
357
+ throw new Error(RESERVED_MESSAGE_PROP_ERROR)
358
+ }
337
359
  if (value === undefined) {
338
360
  throw new Error('PluginError0.use("prop", key, value) requires value')
339
361
  }
@@ -348,8 +370,10 @@ export class PluginError0<
348
370
  nextAdapt.push(keyOrValue as ErrorPluginAdaptFn<Error0, Record<string, unknown>>)
349
371
  } else if (kind === 'stack') {
350
372
  nextStack = keyOrValue as ErrorPluginStack
351
- } else {
373
+ } else if (kind === 'cause') {
352
374
  nextCause = keyOrValue as ErrorPluginCause
375
+ } else {
376
+ nextMessage = keyOrValue as ErrorPluginMessage
353
377
  }
354
378
  return new PluginError0({
355
379
  props: nextProps,
@@ -357,6 +381,7 @@ export class PluginError0<
357
381
  adapt: nextAdapt,
358
382
  stack: nextStack,
359
383
  cause: nextCause,
384
+ message: nextMessage,
360
385
  })
361
386
  }
362
387
  }
@@ -365,14 +390,24 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
365
390
  new (
366
391
  message: string,
367
392
  input?: ErrorInput<TPluginsMap>,
368
- ): Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>
393
+ ): Error0 &
394
+ ErrorResolved<TPluginsMap> &
395
+ ErrorOwnMethods<TPluginsMap> &
396
+ ErrorResolveMethods<TPluginsMap> & { readonly __pluginsMap?: TPluginsMap }
369
397
  new (
370
398
  input: { message: string } & ErrorInput<TPluginsMap>,
371
- ): Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>
399
+ ): Error0 &
400
+ ErrorResolved<TPluginsMap> &
401
+ ErrorOwnMethods<TPluginsMap> &
402
+ ErrorResolveMethods<TPluginsMap> & { readonly __pluginsMap?: TPluginsMap }
372
403
  readonly __pluginsMap?: TPluginsMap
373
404
  from: (
374
405
  error: unknown,
375
406
  ) => Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>
407
+ round: (
408
+ error: unknown,
409
+ isPublic?: boolean,
410
+ ) => Error0 & ErrorResolved<TPluginsMap> & ErrorOwnMethods<TPluginsMap> & ErrorResolveMethods<TPluginsMap>
376
411
  resolve: (error: unknown) => ErrorResolvedProps<TPluginsMap>
377
412
  serialize: (error: unknown, isPublic?: boolean) => Record<string, unknown>
378
413
  own: {
@@ -426,12 +461,14 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
426
461
  ): ClassError0<TPluginsMap>
427
462
  (kind: 'stack', value: ErrorPluginStack<ErrorInstanceOfMap<TPluginsMap>>): ClassError0<TPluginsMap>
428
463
  (kind: 'cause', value: ErrorPluginCause<ErrorInstanceOfMap<TPluginsMap>>): ClassError0<TPluginsMap>
464
+ (kind: 'message', value: ErrorPluginMessage<ErrorInstanceOfMap<TPluginsMap>>): ClassError0<TPluginsMap>
429
465
  }
430
466
  plugin: () => PluginError0
431
467
  } & ErrorStaticMethods<TPluginsMap>
432
468
 
433
469
  export class Error0 extends Error {
434
470
  static readonly __pluginsMap?: EmptyPluginsMap
471
+ readonly __pluginsMap?: EmptyPluginsMap
435
472
  protected static _plugins: ErrorPlugin[] = []
436
473
 
437
474
  private static readonly _emptyPlugin: ErrorPluginResolved = {
@@ -440,6 +477,7 @@ export class Error0 extends Error {
440
477
  adapt: [],
441
478
  stack: undefined,
442
479
  cause: undefined,
480
+ message: undefined,
443
481
  }
444
482
 
445
483
  private static _getResolvedPlugin(this: typeof Error0): ErrorPluginResolved {
@@ -452,6 +490,9 @@ export class Error0 extends Error {
452
490
  if (plugin.props && 'stack' in plugin.props) {
453
491
  throw new Error(RESERVED_STACK_PROP_ERROR)
454
492
  }
493
+ if (plugin.props && 'message' in plugin.props) {
494
+ throw new Error(RESERVED_MESSAGE_PROP_ERROR)
495
+ }
455
496
  Object.assign(resolved.props, plugin.props ?? this._emptyPlugin.props)
456
497
  Object.assign(resolved.methods, plugin.methods ?? this._emptyPlugin.methods)
457
498
  resolved.adapt.push(...(plugin.adapt ?? this._emptyPlugin.adapt))
@@ -461,6 +502,9 @@ export class Error0 extends Error {
461
502
  if (typeof plugin.cause !== 'undefined') {
462
503
  resolved.cause = plugin.cause
463
504
  }
505
+ if (typeof plugin.message !== 'undefined') {
506
+ resolved.message = plugin.message
507
+ }
464
508
  }
465
509
  return resolved
466
510
  }
@@ -673,6 +717,10 @@ export class Error0 extends Error {
673
717
  return this._fromNonError0(error)
674
718
  }
675
719
 
720
+ static round(error: unknown, isPublic = false): Error0 {
721
+ return this.from(this.serialize(error, isPublic))
722
+ }
723
+
676
724
  private static _applyAdapt(error: Error0): Error0 {
677
725
  const plugin = this._getResolvedPlugin()
678
726
  for (const adapt of plugin.adapt) {
@@ -710,8 +758,7 @@ export class Error0 extends Error {
710
758
  }
711
759
  // we do not serialize causes
712
760
  // ;(recreated as unknown as { cause?: unknown }).cause = errorRecord.cause
713
- const stackPlugin = plugin.stack
714
- if (stackPlugin !== false && 'stack' in errorRecord) {
761
+ if ('stack' in errorRecord) {
715
762
  try {
716
763
  if (typeof errorRecord.stack === 'string') {
717
764
  recreated.stack = errorRecord.stack
@@ -721,8 +768,8 @@ export class Error0 extends Error {
721
768
  console.error('Error0: failed to deserialize stack', errorRecord)
722
769
  }
723
770
  }
724
- const causePlugin = plugin.cause ?? false
725
- if (causePlugin && 'cause' in errorRecord) {
771
+ const causePlugin = plugin.cause
772
+ if (causePlugin?.serialize && 'cause' in errorRecord) {
726
773
  try {
727
774
  if (this.isSerialized(errorRecord.cause)) {
728
775
  ;(recreated as { cause?: unknown }).cause = this._fromSerialized(errorRecord.cause)
@@ -793,6 +840,7 @@ export class Error0 extends Error {
793
840
  adapt: [...(pluginRecord._plugin.adapt ?? [])],
794
841
  stack: pluginRecord._plugin.stack,
795
842
  cause: pluginRecord._plugin.cause,
843
+ message: pluginRecord._plugin.message,
796
844
  }
797
845
  }
798
846
 
@@ -876,9 +924,14 @@ export class Error0 extends Error {
876
924
  kind: 'cause',
877
925
  value: ErrorPluginCause<ErrorInstanceOfMap<PluginsMapOf<TThis>>>,
878
926
  ): ClassError0<PluginsMapOf<TThis>>
927
+ static use<TThis extends typeof Error0>(
928
+ this: TThis,
929
+ kind: 'message',
930
+ value: ErrorPluginMessage<ErrorInstanceOfMap<PluginsMapOf<TThis>>>,
931
+ ): ClassError0<PluginsMapOf<TThis>>
879
932
  static use(
880
933
  this: typeof Error0,
881
- first: PluginError0 | 'prop' | 'method' | 'adapt' | 'stack' | 'cause',
934
+ first: PluginError0 | 'prop' | 'method' | 'adapt' | 'stack' | 'cause' | 'message',
882
935
  key?: unknown,
883
936
  value?: ErrorPluginPropOptions<unknown> | ErrorPluginMethodFn<unknown>,
884
937
  ): ClassError0 {
@@ -889,8 +942,8 @@ export class Error0 extends Error {
889
942
  if (typeof key === 'undefined') {
890
943
  throw new Error('Error0.use("stack", value) requires stack plugin value')
891
944
  }
892
- if (key !== 'merge' && typeof key !== 'boolean' && typeof key !== 'function') {
893
- throw new Error('Error0.use("stack", value) expects function | boolean | "merge"')
945
+ if (typeof key !== 'object' || key === null || typeof (key as { serialize?: unknown }).serialize !== 'function') {
946
+ throw new Error('Error0.use("stack", value) expects { serialize: function }')
894
947
  }
895
948
  return this._useWithPlugin({
896
949
  stack: key as ErrorPluginStack,
@@ -900,13 +953,24 @@ export class Error0 extends Error {
900
953
  if (typeof key === 'undefined') {
901
954
  throw new Error('Error0.use("cause", value) requires cause plugin value')
902
955
  }
903
- if (typeof key !== 'boolean' && typeof key !== 'function') {
904
- throw new Error('Error0.use("cause", value) expects function | boolean')
956
+ if (typeof key !== 'object' || key === null || typeof (key as { serialize?: unknown }).serialize !== 'function') {
957
+ throw new Error('Error0.use("cause", value) expects { serialize: function }')
905
958
  }
906
959
  return this._useWithPlugin({
907
960
  cause: key as ErrorPluginCause,
908
961
  })
909
962
  }
963
+ if (first === 'message') {
964
+ if (typeof key === 'undefined') {
965
+ throw new Error('Error0.use("message", value) requires message plugin value')
966
+ }
967
+ if (typeof key !== 'object' || key === null || typeof (key as { serialize?: unknown }).serialize !== 'function') {
968
+ throw new Error('Error0.use("message", value) expects { serialize: function }')
969
+ }
970
+ return this._useWithPlugin({
971
+ message: key as ErrorPluginMessage,
972
+ })
973
+ }
910
974
  if (first === 'adapt') {
911
975
  if (typeof key !== 'function') {
912
976
  throw new Error('Error0.use("adapt", value) requires adapt function')
@@ -923,6 +987,9 @@ export class Error0 extends Error {
923
987
  if (key === 'stack') {
924
988
  throw new Error(RESERVED_STACK_PROP_ERROR)
925
989
  }
990
+ if (key === 'message') {
991
+ throw new Error(RESERVED_MESSAGE_PROP_ERROR)
992
+ }
926
993
  return this._useWithPlugin({
927
994
  props: { [key]: value as ErrorPluginPropOptions<unknown> },
928
995
  })
@@ -940,14 +1007,25 @@ export class Error0 extends Error {
940
1007
  const error0 = this.from(error)
941
1008
  const resolvedProps = this.resolve(error0)
942
1009
  const resolvedRecord = resolvedProps as Record<string, unknown>
1010
+ const plugin = this._getResolvedPlugin()
1011
+ const messagePlugin = plugin.message
1012
+ let serializedMessage: unknown = error0.message
1013
+ try {
1014
+ if (messagePlugin) {
1015
+ serializedMessage = messagePlugin.serialize({ value: error0.message, error: error0, isPublic })
1016
+ }
1017
+ } catch {
1018
+ // eslint-disable-next-line no-console
1019
+ console.error('Error0: failed to serialize message', error0)
1020
+ serializedMessage = error0.message
1021
+ }
943
1022
  const json: Record<string, unknown> = {
944
1023
  name: error0.name,
945
- message: error0.message,
1024
+ message: serializedMessage,
946
1025
  // we do not serialize causes, it is enough that we have floated props and adapt helper
947
1026
  // cause: error0.cause,
948
1027
  }
949
1028
 
950
- const plugin = this._getResolvedPlugin()
951
1029
  const propsEntries = Object.entries(plugin.props)
952
1030
  for (const [key, prop] of propsEntries) {
953
1031
  if (prop.serialize === false) {
@@ -964,48 +1042,31 @@ export class Error0 extends Error {
964
1042
  console.error(`Error0: failed to serialize property ${key}`, resolvedRecord)
965
1043
  }
966
1044
  }
967
- const stackSerialize = plugin.stack
968
- if (stackSerialize !== false) {
969
- try {
970
- let serializedStack: unknown
971
- if (stackSerialize === 'merge') {
972
- serializedStack = error0
973
- .causes()
974
- .map((cause) => {
975
- return cause instanceof Error ? cause.stack : undefined
976
- })
977
- .filter((value): value is string => typeof value === 'string')
978
- .join('\n')
979
- } else if (typeof stackSerialize === 'function') {
980
- serializedStack = stackSerialize({ value: error0.stack, error: error0, isPublic })
981
- } else {
982
- serializedStack = error0.stack
983
- }
984
- if (serializedStack !== undefined) {
985
- json.stack = serializedStack
986
- }
987
- } catch {
988
- // eslint-disable-next-line no-console
989
- console.error('Error0: failed to serialize stack', error0)
1045
+ const stackPlugin = plugin.stack
1046
+ try {
1047
+ let serializedStack: unknown
1048
+ if (stackPlugin) {
1049
+ serializedStack = stackPlugin.serialize({ value: error0.stack, error: error0, isPublic })
1050
+ } else {
1051
+ serializedStack = error0.stack
990
1052
  }
1053
+ if (serializedStack !== undefined) {
1054
+ json.stack = serializedStack
1055
+ }
1056
+ } catch {
1057
+ // eslint-disable-next-line no-console
1058
+ console.error('Error0: failed to serialize stack', error0)
991
1059
  }
992
- const causeSerialize = plugin.cause ?? false
993
- if (causeSerialize) {
1060
+ const causePlugin = plugin.cause
1061
+ if (causePlugin?.serialize) {
994
1062
  try {
995
- if (causeSerialize === true) {
996
- const causeValue = (error0 as { cause?: unknown }).cause
997
- if (this.is(causeValue)) {
998
- json.cause = this.serialize(causeValue, isPublic)
999
- }
1000
- } else {
1001
- const serializedCause = causeSerialize({
1002
- value: (error0 as { cause?: unknown }).cause,
1003
- error: error0,
1004
- isPublic,
1005
- })
1006
- if (serializedCause !== undefined) {
1007
- json.cause = serializedCause
1008
- }
1063
+ const serializedCause = causePlugin.serialize({
1064
+ value: (error0 as { cause?: unknown }).cause,
1065
+ error: error0,
1066
+ isPublic,
1067
+ })
1068
+ if (serializedCause !== undefined) {
1069
+ json.cause = serializedCause
1009
1070
  }
1010
1071
  } catch {
1011
1072
  // eslint-disable-next-line no-console
@@ -1022,4 +1083,9 @@ export class Error0 extends Error {
1022
1083
  const ctor = this.constructor as typeof Error0
1023
1084
  return ctor.serialize(this, isPublic)
1024
1085
  }
1086
+
1087
+ round<TThis extends Error0>(this: TThis, isPublic = true): TThis {
1088
+ const ctor = this.constructor as typeof Error0
1089
+ return ctor.round(this, isPublic) as TThis
1090
+ }
1025
1091
  }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { causeSerializePlugin } from './cause-serialize.js'
4
+
5
+ describe('causeSerializePlugin', () => {
6
+ const statusPlugin = Error0.plugin().use('prop', 'status', {
7
+ init: (input: number) => input,
8
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
9
+ serialize: ({ value }) => value,
10
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
11
+ })
12
+
13
+ const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
14
+ type Code = (typeof codes)[number]
15
+ const codePlugin = Error0.plugin().use('prop', 'code', {
16
+ init: (input: Code) => input,
17
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'string' && codes.includes(value as Code)),
18
+ serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
19
+ deserialize: ({ value }) =>
20
+ typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
21
+ })
22
+
23
+ it('serializes and deserializes nested Error0 causes', () => {
24
+ const AppError = Error0.use(statusPlugin).use(codePlugin).use(causeSerializePlugin)
25
+ const deepCauseError = new AppError('deep cause')
26
+ const causeError = new AppError('cause', { status: 409, code: 'NOT_FOUND', cause: deepCauseError })
27
+ const error = new AppError('root', { status: 500, cause: causeError })
28
+
29
+ const json = AppError.serialize(error, false)
30
+ expect(typeof json.cause).toBe('object')
31
+ expect((json.cause as any).message).toBe('cause')
32
+ expect((json.cause as any).status).toBe(409)
33
+ expect((json.cause as any).code).toBe('NOT_FOUND')
34
+ expect((json.cause as any).cause).toBeDefined()
35
+ expect((json.cause as any).cause.message).toBe('deep cause')
36
+ expect((json.cause as any).cause.status).toBe(undefined)
37
+ expect((json.cause as any).cause.code).toBe(undefined)
38
+ expect((json.cause as any).cause.cause).toBeUndefined()
39
+
40
+ const recreated = AppError.from(json)
41
+ expect(recreated).toBeInstanceOf(AppError)
42
+ expect(recreated.cause).toBeInstanceOf(AppError)
43
+ expect((recreated.cause as any).status).toBe(409)
44
+ expect((recreated.cause as any).code).toBe('NOT_FOUND')
45
+ expect((recreated.cause as any).cause).toBeInstanceOf(AppError)
46
+ expect((recreated.cause as any).cause.message).toBe('deep cause')
47
+ expect((recreated.cause as any).cause.status).toBe(undefined)
48
+ expect((recreated.cause as any).cause.code).toBe(undefined)
49
+ expect((recreated.cause as any).cause.cause).toBeUndefined()
50
+ })
51
+ })
@@ -0,0 +1,11 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ export const causeSerializePlugin = Error0.plugin().use('cause', {
4
+ serialize: ({ value, error, isPublic }) => {
5
+ const ctor = error.constructor as typeof Error0
6
+ if (ctor.is(value)) {
7
+ return ctor.serialize(value, isPublic)
8
+ }
9
+ return undefined
10
+ },
11
+ })
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { expectedPlugin } from './expected.js'
4
+
5
+ describe('expectedPlugin', () => {
6
+ const statusPlugin = Error0.plugin().use('prop', 'status', {
7
+ init: (input: number) => input,
8
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
9
+ serialize: ({ value }) => value,
10
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
11
+ })
12
+
13
+ it('can be used to control error tracker behavior', () => {
14
+ const AppError = Error0.use(statusPlugin).use(expectedPlugin)
15
+ const errorExpected = new AppError('test', { status: 400, expected: true })
16
+ const errorUnexpected = new AppError('test', { status: 400, expected: false })
17
+ const usualError = new Error('test')
18
+ const errorFromUsualError = AppError.from(usualError)
19
+ const errorWithExpectedErrorAsCause = new AppError('test', { status: 400, cause: errorExpected })
20
+ const errorWithUnexpectedErrorAsCause = new AppError('test', { status: 400, cause: errorUnexpected })
21
+ expect(errorExpected.expected).toBe(true)
22
+ expect(errorUnexpected.expected).toBe(false)
23
+ expect(AppError.isExpected(usualError)).toBe(false)
24
+ expect(errorFromUsualError.expected).toBe(false)
25
+ expect(errorFromUsualError.isExpected()).toBe(false)
26
+ expect(errorWithExpectedErrorAsCause.expected).toBe(true)
27
+ expect(errorWithExpectedErrorAsCause.isExpected()).toBe(true)
28
+ expect(errorWithUnexpectedErrorAsCause.expected).toBe(false)
29
+ expect(errorWithUnexpectedErrorAsCause.isExpected()).toBe(false)
30
+ })
31
+
32
+ it('resolves to false when any cause has false', () => {
33
+ const AppError = Error0.use(expectedPlugin)
34
+ const root = new AppError('root', { expected: true })
35
+ const middle = new AppError('middle', { expected: false, cause: root })
36
+ const leaf = new AppError('leaf', { expected: false, cause: middle })
37
+ expect(leaf.expected).toBe(false)
38
+ expect(leaf.isExpected()).toBe(false)
39
+ })
40
+
41
+ it('treats undefined expected as unexpected', () => {
42
+ const AppError = Error0.use(expectedPlugin)
43
+ const error = new AppError('without expected')
44
+ expect(error.expected).toBe(false)
45
+ expect(error.isExpected()).toBe(false)
46
+ })
47
+ })
@@ -0,0 +1,25 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ const isExpected = (flow: unknown[]) => {
4
+ let expected = false
5
+ for (const value of flow) {
6
+ if (value === false) {
7
+ return false
8
+ }
9
+ if (value === true) {
10
+ expected = true
11
+ }
12
+ }
13
+ return expected
14
+ }
15
+
16
+ export const expectedPlugin = Error0.plugin()
17
+ .use('prop', 'expected', {
18
+ init: (input: boolean) => input,
19
+ resolve: ({ flow }) => isExpected(flow),
20
+ serialize: ({ value }) => value,
21
+ deserialize: ({ value }) => (typeof value === 'boolean' ? value : undefined),
22
+ })
23
+ .use('method', 'isExpected', (error) => {
24
+ return isExpected(error.flow('expected'))
25
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { messageMergePlugin } from './message-merge.js'
4
+
5
+ describe('messageMergePlugin', () => {
6
+ const statusPlugin = Error0.plugin().use('prop', 'status', {
7
+ init: (input: number) => input,
8
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
9
+ serialize: ({ value }) => value,
10
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
11
+ })
12
+
13
+ const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
14
+ type Code = (typeof codes)[number]
15
+ const codePlugin = Error0.plugin().use('prop', 'code', {
16
+ init: (input: Code) => input,
17
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'string' && codes.includes(value)),
18
+ serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
19
+ deserialize: ({ value }) =>
20
+ typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
21
+ })
22
+
23
+ it('can merge message across causes in one serialized value', () => {
24
+ const AppError = Error0.use(statusPlugin).use(codePlugin).use(messageMergePlugin)
25
+ const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
26
+ const error2 = new AppError('test2', { status: 401, cause: error1 })
27
+ expect(error1.message).toBe('test1')
28
+ expect(error2.message).toBe('test2')
29
+ expect(error1.serialize().message).toBe('test1')
30
+ expect(error2.serialize().message).toBe('test2: test1')
31
+ })
32
+ })
@@ -0,0 +1,15 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ export const messageMergePlugin = Error0.plugin().use('message', {
4
+ serialize: ({ error }) => {
5
+ return (
6
+ error
7
+ .causes()
8
+ .map((cause) => {
9
+ return cause instanceof Error ? cause.message : undefined
10
+ })
11
+ .filter((value): value is string => typeof value === 'string')
12
+ .join(': ') || 'Unknown error'
13
+ )
14
+ },
15
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { metaPlugin } from './meta.js'
4
+
5
+ describe('metaPlugin', () => {
6
+ it('merges meta across causes into current error', () => {
7
+ const AppError = Error0.use(metaPlugin)
8
+ const root = new AppError('root', { meta: { requestId: 'r1', source: 'db' } })
9
+ const leaf = new AppError('leaf', { meta: { route: '/ideas', source: 'api' }, cause: root })
10
+ expect(leaf.resolve().meta).toEqual({
11
+ requestId: 'r1',
12
+ source: 'api',
13
+ route: '/ideas',
14
+ })
15
+ })
16
+
17
+ it('serializes meta only for private output and keeps json-safe values', () => {
18
+ const AppError = Error0.use(metaPlugin)
19
+ const error = new AppError('test', {
20
+ meta: {
21
+ ok: true,
22
+ nested: { id: 1 },
23
+ skip: () => 'x',
24
+ },
25
+ })
26
+ expect(AppError.serialize(error, false).meta).toEqual({
27
+ ok: true,
28
+ nested: { id: 1 },
29
+ })
30
+ expect('meta' in AppError.serialize(error, true)).toBe(false)
31
+ })
32
+ })