@devp0nt/error0 1.0.0-next.46 → 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 -49
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +32 -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 +32 -12
  23. package/dist/esm/index.js +80 -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 +56 -79
  45. package/src/index.ts +117 -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,10 +390,16 @@ 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,
@@ -426,12 +457,14 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
426
457
  ): ClassError0<TPluginsMap>
427
458
  (kind: 'stack', value: ErrorPluginStack<ErrorInstanceOfMap<TPluginsMap>>): ClassError0<TPluginsMap>
428
459
  (kind: 'cause', value: ErrorPluginCause<ErrorInstanceOfMap<TPluginsMap>>): ClassError0<TPluginsMap>
460
+ (kind: 'message', value: ErrorPluginMessage<ErrorInstanceOfMap<TPluginsMap>>): ClassError0<TPluginsMap>
429
461
  }
430
462
  plugin: () => PluginError0
431
463
  } & ErrorStaticMethods<TPluginsMap>
432
464
 
433
465
  export class Error0 extends Error {
434
466
  static readonly __pluginsMap?: EmptyPluginsMap
467
+ readonly __pluginsMap?: EmptyPluginsMap
435
468
  protected static _plugins: ErrorPlugin[] = []
436
469
 
437
470
  private static readonly _emptyPlugin: ErrorPluginResolved = {
@@ -440,6 +473,7 @@ export class Error0 extends Error {
440
473
  adapt: [],
441
474
  stack: undefined,
442
475
  cause: undefined,
476
+ message: undefined,
443
477
  }
444
478
 
445
479
  private static _getResolvedPlugin(this: typeof Error0): ErrorPluginResolved {
@@ -452,6 +486,9 @@ export class Error0 extends Error {
452
486
  if (plugin.props && 'stack' in plugin.props) {
453
487
  throw new Error(RESERVED_STACK_PROP_ERROR)
454
488
  }
489
+ if (plugin.props && 'message' in plugin.props) {
490
+ throw new Error(RESERVED_MESSAGE_PROP_ERROR)
491
+ }
455
492
  Object.assign(resolved.props, plugin.props ?? this._emptyPlugin.props)
456
493
  Object.assign(resolved.methods, plugin.methods ?? this._emptyPlugin.methods)
457
494
  resolved.adapt.push(...(plugin.adapt ?? this._emptyPlugin.adapt))
@@ -461,6 +498,9 @@ export class Error0 extends Error {
461
498
  if (typeof plugin.cause !== 'undefined') {
462
499
  resolved.cause = plugin.cause
463
500
  }
501
+ if (typeof plugin.message !== 'undefined') {
502
+ resolved.message = plugin.message
503
+ }
464
504
  }
465
505
  return resolved
466
506
  }
@@ -710,8 +750,7 @@ export class Error0 extends Error {
710
750
  }
711
751
  // we do not serialize causes
712
752
  // ;(recreated as unknown as { cause?: unknown }).cause = errorRecord.cause
713
- const stackPlugin = plugin.stack
714
- if (stackPlugin !== false && 'stack' in errorRecord) {
753
+ if ('stack' in errorRecord) {
715
754
  try {
716
755
  if (typeof errorRecord.stack === 'string') {
717
756
  recreated.stack = errorRecord.stack
@@ -721,8 +760,8 @@ export class Error0 extends Error {
721
760
  console.error('Error0: failed to deserialize stack', errorRecord)
722
761
  }
723
762
  }
724
- const causePlugin = plugin.cause ?? false
725
- if (causePlugin && 'cause' in errorRecord) {
763
+ const causePlugin = plugin.cause
764
+ if (causePlugin?.serialize && 'cause' in errorRecord) {
726
765
  try {
727
766
  if (this.isSerialized(errorRecord.cause)) {
728
767
  ;(recreated as { cause?: unknown }).cause = this._fromSerialized(errorRecord.cause)
@@ -793,6 +832,7 @@ export class Error0 extends Error {
793
832
  adapt: [...(pluginRecord._plugin.adapt ?? [])],
794
833
  stack: pluginRecord._plugin.stack,
795
834
  cause: pluginRecord._plugin.cause,
835
+ message: pluginRecord._plugin.message,
796
836
  }
797
837
  }
798
838
 
@@ -876,9 +916,14 @@ export class Error0 extends Error {
876
916
  kind: 'cause',
877
917
  value: ErrorPluginCause<ErrorInstanceOfMap<PluginsMapOf<TThis>>>,
878
918
  ): ClassError0<PluginsMapOf<TThis>>
919
+ static use<TThis extends typeof Error0>(
920
+ this: TThis,
921
+ kind: 'message',
922
+ value: ErrorPluginMessage<ErrorInstanceOfMap<PluginsMapOf<TThis>>>,
923
+ ): ClassError0<PluginsMapOf<TThis>>
879
924
  static use(
880
925
  this: typeof Error0,
881
- first: PluginError0 | 'prop' | 'method' | 'adapt' | 'stack' | 'cause',
926
+ first: PluginError0 | 'prop' | 'method' | 'adapt' | 'stack' | 'cause' | 'message',
882
927
  key?: unknown,
883
928
  value?: ErrorPluginPropOptions<unknown> | ErrorPluginMethodFn<unknown>,
884
929
  ): ClassError0 {
@@ -889,8 +934,8 @@ export class Error0 extends Error {
889
934
  if (typeof key === 'undefined') {
890
935
  throw new Error('Error0.use("stack", value) requires stack plugin value')
891
936
  }
892
- if (key !== 'merge' && typeof key !== 'boolean' && typeof key !== 'function') {
893
- throw new Error('Error0.use("stack", value) expects function | boolean | "merge"')
937
+ if (typeof key !== 'object' || key === null || typeof (key as { serialize?: unknown }).serialize !== 'function') {
938
+ throw new Error('Error0.use("stack", value) expects { serialize: function }')
894
939
  }
895
940
  return this._useWithPlugin({
896
941
  stack: key as ErrorPluginStack,
@@ -900,13 +945,24 @@ export class Error0 extends Error {
900
945
  if (typeof key === 'undefined') {
901
946
  throw new Error('Error0.use("cause", value) requires cause plugin value')
902
947
  }
903
- if (typeof key !== 'boolean' && typeof key !== 'function') {
904
- throw new Error('Error0.use("cause", value) expects function | boolean')
948
+ if (typeof key !== 'object' || key === null || typeof (key as { serialize?: unknown }).serialize !== 'function') {
949
+ throw new Error('Error0.use("cause", value) expects { serialize: function }')
905
950
  }
906
951
  return this._useWithPlugin({
907
952
  cause: key as ErrorPluginCause,
908
953
  })
909
954
  }
955
+ if (first === 'message') {
956
+ if (typeof key === 'undefined') {
957
+ throw new Error('Error0.use("message", value) requires message plugin value')
958
+ }
959
+ if (typeof key !== 'object' || key === null || typeof (key as { serialize?: unknown }).serialize !== 'function') {
960
+ throw new Error('Error0.use("message", value) expects { serialize: function }')
961
+ }
962
+ return this._useWithPlugin({
963
+ message: key as ErrorPluginMessage,
964
+ })
965
+ }
910
966
  if (first === 'adapt') {
911
967
  if (typeof key !== 'function') {
912
968
  throw new Error('Error0.use("adapt", value) requires adapt function')
@@ -923,6 +979,9 @@ export class Error0 extends Error {
923
979
  if (key === 'stack') {
924
980
  throw new Error(RESERVED_STACK_PROP_ERROR)
925
981
  }
982
+ if (key === 'message') {
983
+ throw new Error(RESERVED_MESSAGE_PROP_ERROR)
984
+ }
926
985
  return this._useWithPlugin({
927
986
  props: { [key]: value as ErrorPluginPropOptions<unknown> },
928
987
  })
@@ -940,14 +999,25 @@ export class Error0 extends Error {
940
999
  const error0 = this.from(error)
941
1000
  const resolvedProps = this.resolve(error0)
942
1001
  const resolvedRecord = resolvedProps as Record<string, unknown>
1002
+ const plugin = this._getResolvedPlugin()
1003
+ const messagePlugin = plugin.message
1004
+ let serializedMessage: unknown = error0.message
1005
+ try {
1006
+ if (messagePlugin) {
1007
+ serializedMessage = messagePlugin.serialize({ value: error0.message, error: error0, isPublic })
1008
+ }
1009
+ } catch {
1010
+ // eslint-disable-next-line no-console
1011
+ console.error('Error0: failed to serialize message', error0)
1012
+ serializedMessage = error0.message
1013
+ }
943
1014
  const json: Record<string, unknown> = {
944
1015
  name: error0.name,
945
- message: error0.message,
1016
+ message: serializedMessage,
946
1017
  // we do not serialize causes, it is enough that we have floated props and adapt helper
947
1018
  // cause: error0.cause,
948
1019
  }
949
1020
 
950
- const plugin = this._getResolvedPlugin()
951
1021
  const propsEntries = Object.entries(plugin.props)
952
1022
  for (const [key, prop] of propsEntries) {
953
1023
  if (prop.serialize === false) {
@@ -964,48 +1034,31 @@ export class Error0 extends Error {
964
1034
  console.error(`Error0: failed to serialize property ${key}`, resolvedRecord)
965
1035
  }
966
1036
  }
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)
1037
+ const stackPlugin = plugin.stack
1038
+ try {
1039
+ let serializedStack: unknown
1040
+ if (stackPlugin) {
1041
+ serializedStack = stackPlugin.serialize({ value: error0.stack, error: error0, isPublic })
1042
+ } else {
1043
+ serializedStack = error0.stack
990
1044
  }
1045
+ if (serializedStack !== undefined) {
1046
+ json.stack = serializedStack
1047
+ }
1048
+ } catch {
1049
+ // eslint-disable-next-line no-console
1050
+ console.error('Error0: failed to serialize stack', error0)
991
1051
  }
992
- const causeSerialize = plugin.cause ?? false
993
- if (causeSerialize) {
1052
+ const causePlugin = plugin.cause
1053
+ if (causePlugin?.serialize) {
994
1054
  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
- }
1055
+ const serializedCause = causePlugin.serialize({
1056
+ value: (error0 as { cause?: unknown }).cause,
1057
+ error: error0,
1058
+ isPublic,
1059
+ })
1060
+ if (serializedCause !== undefined) {
1061
+ json.cause = serializedCause
1009
1062
  }
1010
1063
  } catch {
1011
1064
  // eslint-disable-next-line no-console
@@ -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
+ })
@@ -0,0 +1,53 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ type Json = null | boolean | number | string | Json[] | { [key: string]: Json }
4
+
5
+ const toJsonSafe = (input: unknown): Json | undefined => {
6
+ if (input === null) {
7
+ return null
8
+ }
9
+ if (typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean') {
10
+ return input
11
+ }
12
+ if (Array.isArray(input)) {
13
+ return input.map((value) => toJsonSafe(value)) as Json[]
14
+ }
15
+ if (typeof input === 'object') {
16
+ const output: Record<string, Json> = {}
17
+ for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
18
+ const jsonValue = toJsonSafe(value)
19
+ if (jsonValue !== undefined) {
20
+ output[key] = jsonValue
21
+ }
22
+ }
23
+ return output
24
+ }
25
+ return undefined
26
+ }
27
+
28
+ const isMetaRecord = (value: unknown): value is Record<string, unknown> =>
29
+ typeof value === 'object' && value !== null && !Array.isArray(value)
30
+
31
+ export const metaPlugin = Error0.plugin().use('prop', 'meta', {
32
+ init: (input: Record<string, unknown>) => input,
33
+ resolve: ({ flow }) => {
34
+ const values = flow.filter(isMetaRecord)
35
+ if (values.length === 0) {
36
+ return undefined
37
+ }
38
+
39
+ // Merge cause meta into the current error; nearer errors win on conflicts.
40
+ const merged: Record<string, unknown> = {}
41
+ for (const value of [...values].reverse()) {
42
+ Object.assign(merged, value)
43
+ }
44
+ return merged
45
+ },
46
+ serialize: ({ value, isPublic }) => (isPublic ? undefined : toJsonSafe(value)),
47
+ deserialize: ({ value }) => {
48
+ if (!isMetaRecord(value)) {
49
+ return undefined
50
+ }
51
+ return value
52
+ },
53
+ })