@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.
- package/dist/cjs/index.cjs +87 -49
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +35 -12
- package/dist/cjs/plugins/cause-serialize.cjs +38 -0
- package/dist/cjs/plugins/cause-serialize.cjs.map +1 -0
- package/dist/cjs/plugins/cause-serialize.d.cts +5 -0
- package/dist/cjs/plugins/expected.cjs +49 -0
- package/dist/cjs/plugins/expected.cjs.map +1 -0
- package/dist/cjs/plugins/expected.d.cts +5 -0
- package/dist/cjs/plugins/message-merge.cjs +36 -0
- package/dist/cjs/plugins/message-merge.cjs.map +1 -0
- package/dist/cjs/plugins/message-merge.d.cts +5 -0
- package/dist/cjs/plugins/meta.cjs +73 -0
- package/dist/cjs/plugins/meta.cjs.map +1 -0
- package/dist/cjs/plugins/meta.d.cts +5 -0
- package/dist/cjs/plugins/stack-merge.cjs +39 -0
- package/dist/cjs/plugins/stack-merge.cjs.map +1 -0
- package/dist/cjs/plugins/stack-merge.d.cts +5 -0
- package/dist/cjs/plugins/tags.cjs +48 -0
- package/dist/cjs/plugins/tags.cjs.map +1 -0
- package/dist/cjs/plugins/tags.d.cts +5 -0
- package/dist/esm/index.d.ts +35 -12
- package/dist/esm/index.js +87 -49
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/plugins/cause-serialize.d.ts +5 -0
- package/dist/esm/plugins/cause-serialize.js +14 -0
- package/dist/esm/plugins/cause-serialize.js.map +1 -0
- package/dist/esm/plugins/expected.d.ts +5 -0
- package/dist/esm/plugins/expected.js +25 -0
- package/dist/esm/plugins/expected.js.map +1 -0
- package/dist/esm/plugins/message-merge.d.ts +5 -0
- package/dist/esm/plugins/message-merge.js +12 -0
- package/dist/esm/plugins/message-merge.js.map +1 -0
- package/dist/esm/plugins/meta.d.ts +5 -0
- package/dist/esm/plugins/meta.js +49 -0
- package/dist/esm/plugins/meta.js.map +1 -0
- package/dist/esm/plugins/stack-merge.d.ts +5 -0
- package/dist/esm/plugins/stack-merge.js +15 -0
- package/dist/esm/plugins/stack-merge.js.map +1 -0
- package/dist/esm/plugins/tags.d.ts +5 -0
- package/dist/esm/plugins/tags.js +24 -0
- package/dist/esm/plugins/tags.js.map +1 -0
- package/package.json +9 -1
- package/src/index.test.ts +74 -82
- package/src/index.ts +130 -64
- package/src/plugins/cause-serialize.test.ts +51 -0
- package/src/plugins/cause-serialize.ts +11 -0
- package/src/plugins/expected.test.ts +47 -0
- package/src/plugins/expected.ts +25 -0
- package/src/plugins/message-merge.test.ts +32 -0
- package/src/plugins/message-merge.ts +15 -0
- package/src/plugins/meta.test.ts +32 -0
- package/src/plugins/meta.ts +53 -0
- package/src/plugins/stack-merge.test.ts +64 -0
- package/src/plugins/stack-merge.ts +16 -0
- package/src/plugins/tags.test.ts +22 -0
- 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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export type
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 {
|
|
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 &
|
|
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 &
|
|
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
|
-
|
|
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
|
|
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 !== '
|
|
893
|
-
throw new Error('Error0.use("stack", value) expects
|
|
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 !== '
|
|
904
|
-
throw new Error('Error0.use("cause", value) expects function
|
|
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:
|
|
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
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
|
993
|
-
if (
|
|
1060
|
+
const causePlugin = plugin.cause
|
|
1061
|
+
if (causePlugin?.serialize) {
|
|
994
1062
|
try {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
+
})
|