@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.
- package/dist/cjs/index.cjs +80 -49
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +32 -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 +32 -12
- package/dist/esm/index.js +80 -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 +56 -79
- package/src/index.ts +117 -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,10 +390,16 @@ 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,
|
|
@@ -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
|
-
|
|
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
|
|
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 !== '
|
|
893
|
-
throw new Error('Error0.use("stack", value) expects
|
|
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 !== '
|
|
904
|
-
throw new Error('Error0.use("cause", value) expects function
|
|
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:
|
|
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
|
|
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)
|
|
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
|
|
993
|
-
if (
|
|
1052
|
+
const causePlugin = plugin.cause
|
|
1053
|
+
if (causePlugin?.serialize) {
|
|
994
1054
|
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
|
-
}
|
|
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
|
+
})
|