@devp0nt/error0 1.0.0-next.49 → 1.0.0-next.50

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 (55) hide show
  1. package/dist/cjs/index.cjs +5 -17
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +64 -21
  4. package/dist/cjs/plugins/cause-serialize.cjs +1 -1
  5. package/dist/cjs/plugins/cause-serialize.cjs.map +1 -1
  6. package/dist/cjs/plugins/code.cjs +38 -0
  7. package/dist/cjs/plugins/code.cjs.map +1 -0
  8. package/dist/cjs/plugins/code.d.cts +7 -0
  9. package/dist/cjs/plugins/expected.cjs +2 -2
  10. package/dist/cjs/plugins/expected.cjs.map +1 -1
  11. package/dist/cjs/plugins/expected.d.cts +31 -2
  12. package/dist/cjs/plugins/meta.cjs +1 -1
  13. package/dist/cjs/plugins/meta.cjs.map +1 -1
  14. package/dist/cjs/plugins/stack-merge.cjs +1 -1
  15. package/dist/cjs/plugins/stack-merge.cjs.map +1 -1
  16. package/dist/cjs/plugins/status.cjs +54 -0
  17. package/dist/cjs/plugins/status.cjs.map +1 -0
  18. package/dist/cjs/plugins/status.d.cts +8 -0
  19. package/dist/cjs/plugins/tags.cjs +41 -21
  20. package/dist/cjs/plugins/tags.cjs.map +1 -1
  21. package/dist/cjs/plugins/tags.d.cts +7 -2
  22. package/dist/esm/index.d.ts +64 -21
  23. package/dist/esm/index.js +5 -17
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/plugins/cause-serialize.js +1 -1
  26. package/dist/esm/plugins/cause-serialize.js.map +1 -1
  27. package/dist/esm/plugins/code.d.ts +7 -0
  28. package/dist/esm/plugins/code.js +14 -0
  29. package/dist/esm/plugins/code.js.map +1 -0
  30. package/dist/esm/plugins/expected.d.ts +31 -2
  31. package/dist/esm/plugins/expected.js +2 -2
  32. package/dist/esm/plugins/expected.js.map +1 -1
  33. package/dist/esm/plugins/meta.js +1 -1
  34. package/dist/esm/plugins/meta.js.map +1 -1
  35. package/dist/esm/plugins/stack-merge.js +1 -1
  36. package/dist/esm/plugins/stack-merge.js.map +1 -1
  37. package/dist/esm/plugins/status.d.ts +8 -0
  38. package/dist/esm/plugins/status.js +30 -0
  39. package/dist/esm/plugins/status.js.map +1 -0
  40. package/dist/esm/plugins/tags.d.ts +7 -2
  41. package/dist/esm/plugins/tags.js +41 -21
  42. package/dist/esm/plugins/tags.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/index.test.ts +53 -7
  45. package/src/index.ts +107 -67
  46. package/src/plugins/cause-serialize.ts +1 -1
  47. package/src/plugins/code.test.ts +27 -0
  48. package/src/plugins/code.ts +12 -0
  49. package/src/plugins/expected.ts +2 -2
  50. package/src/plugins/meta.ts +1 -1
  51. package/src/plugins/stack-merge.ts +1 -1
  52. package/src/plugins/status.test.ts +54 -0
  53. package/src/plugins/status.ts +29 -0
  54. package/src/plugins/tags.test.ts +54 -2
  55. package/src/plugins/tags.ts +48 -24
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/plugins/tags.ts"],"sourcesContent":["import { Error0 } from '../index.js'\n\nexport const tagsPlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boolean } = {}) =>\n Error0.plugin().use('prop', 'tags', {\n init: (input: string[]) => input,\n resolve: ({ flow }) => {\n const merged: string[] = []\n for (const value of flow) {\n if (Array.isArray(value)) {\n merged.push(...value.filter((item): item is string => typeof item === 'string'))\n }\n }\n return merged.length > 0 ? Array.from(new Set(merged)) : undefined\n },\n serialize: ({ resolved, isPublic }) => {\n if (hideWhenPublic && isPublic) {\n return undefined\n }\n return resolved\n },\n deserialize: ({ value }) => {\n if (!Array.isArray(value)) {\n return undefined\n }\n return value.filter((item): item is string => typeof item === 'string')\n },\n })\n"],"mappings":"AAAA,SAAS,cAAc;AAEhB,MAAM,aAAa,CAAC,EAAE,iBAAiB,KAAK,IAAkC,CAAC,MACpF,OAAO,OAAO,EAAE,IAAI,QAAQ,QAAQ;AAAA,EAClC,MAAM,CAAC,UAAoB;AAAA,EAC3B,SAAS,CAAC,EAAE,KAAK,MAAM;AACrB,UAAM,SAAmB,CAAC;AAC1B,eAAW,SAAS,MAAM;AACxB,UAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAO,KAAK,GAAG,MAAM,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ,CAAC;AAAA,MACjF;AAAA,IACF;AACA,WAAO,OAAO,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI;AAAA,EAC3D;AAAA,EACA,WAAW,CAAC,EAAE,UAAU,SAAS,MAAM;AACrC,QAAI,kBAAkB,UAAU;AAC9B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,EAAE,MAAM,MAAM;AAC1B,QAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,aAAO;AAAA,IACT;AACA,WAAO,MAAM,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ;AAAA,EACxE;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../src/plugins/tags.ts"],"sourcesContent":["import { Error0 } from '../index.js'\n\nexport const tagsPlugin = <TTag extends string>({\n hideWhenPublic = true,\n tags,\n strict = true,\n}: { hideWhenPublic?: boolean; tags?: TTag[] | readonly TTag[]; strict?: boolean } = {}) => {\n function hasTag(error: Error0, tag: TTag): boolean\n function hasTag(error: Error0, tag: TTag[], policy: 'every' | 'some'): boolean\n function hasTag(error: Error0, tag: TTag | TTag[], policy?: 'every' | 'some'): boolean {\n const tags = (error as any).tags as string[] | undefined\n if (!tags) {\n return false\n }\n if (Array.isArray(tag)) {\n if (policy === 'every') {\n return tag.every((item) => tags.includes(item))\n }\n return tag.some((item) => tags.includes(item))\n }\n return tags.includes(tag)\n }\n const isTag = (value: unknown): value is TTag =>\n typeof value === 'string' && (!tags || !strict || tags.includes(value as TTag))\n return Error0.plugin()\n .prop('tags', {\n init: (input: string[]) => input,\n resolve: ({ flow }) => {\n const merged: string[] = []\n for (const value of flow) {\n if (Array.isArray(value)) {\n merged.push(...value)\n }\n }\n return merged.length > 0 ? Array.from(new Set(merged)) : undefined\n },\n serialize: ({ resolved, isPublic }) => {\n if (hideWhenPublic && isPublic) {\n return undefined\n }\n return resolved\n },\n deserialize: ({ value }) => {\n if (!Array.isArray(value)) {\n return undefined\n }\n return value.filter((item) => isTag(item))\n },\n })\n .method('hasTag', hasTag)\n}\n"],"mappings":"AAAA,SAAS,cAAc;AAEhB,MAAM,aAAa,CAAsB;AAAA,EAC9C,iBAAiB;AAAA,EACjB;AAAA,EACA,SAAS;AACX,IAAqF,CAAC,MAAM;AAG1F,WAAS,OAAO,OAAe,KAAoB,QAAoC;AACrF,UAAMA,QAAQ,MAAc;AAC5B,QAAI,CAACA,OAAM;AACT,aAAO;AAAA,IACT;AACA,QAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,UAAI,WAAW,SAAS;AACtB,eAAO,IAAI,MAAM,CAAC,SAASA,MAAK,SAAS,IAAI,CAAC;AAAA,MAChD;AACA,aAAO,IAAI,KAAK,CAAC,SAASA,MAAK,SAAS,IAAI,CAAC;AAAA,IAC/C;AACA,WAAOA,MAAK,SAAS,GAAG;AAAA,EAC1B;AACA,QAAM,QAAQ,CAAC,UACb,OAAO,UAAU,aAAa,CAAC,QAAQ,CAAC,UAAU,KAAK,SAAS,KAAa;AAC/E,SAAO,OAAO,OAAO,EAClB,KAAK,QAAQ;AAAA,IACZ,MAAM,CAAC,UAAoB;AAAA,IAC3B,SAAS,CAAC,EAAE,KAAK,MAAM;AACrB,YAAM,SAAmB,CAAC;AAC1B,iBAAW,SAAS,MAAM;AACxB,YAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,iBAAO,KAAK,GAAG,KAAK;AAAA,QACtB;AAAA,MACF;AACA,aAAO,OAAO,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI;AAAA,IAC3D;AAAA,IACA,WAAW,CAAC,EAAE,UAAU,SAAS,MAAM;AACrC,UAAI,kBAAkB,UAAU;AAC9B,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IACA,aAAa,CAAC,EAAE,MAAM,MAAM;AAC1B,UAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,eAAO;AAAA,MACT;AACA,aAAO,MAAM,OAAO,CAAC,SAAS,MAAM,IAAI,CAAC;AAAA,IAC3C;AAAA,EACF,CAAC,EACA,OAAO,UAAU,MAAM;AAC5B;","names":["tags"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devp0nt/error0",
3
- "version": "1.0.0-next.49",
3
+ "version": "1.0.0-next.50",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Sergei Dmitriev",
package/src/index.test.ts CHANGED
@@ -683,15 +683,61 @@ describe('Error0', () => {
683
683
  expect(elapsedMs).toBeLessThan(budgetMs)
684
684
  })
685
685
 
686
+ it('class can be created extended from Error0', () => {
687
+ class MyError extends Error0.use(statusPlugin).use(codePlugin) {
688
+ isStatusAndCode(status: number, code: string): boolean {
689
+ return this.status === status && this.code === code
690
+ }
691
+ }
692
+ const error = new MyError('test', { status: 400, code: 'NOT_FOUND' })
693
+ expect(error.isStatusAndCode(400, 'NOT_FOUND')).toBe(true)
694
+ expect(error.isStatusAndCode(400, 'BAD_REQUEST')).toBe(false)
695
+ expect(error.name).toBe('Error0')
696
+ expectTypeOf<typeof MyError>().toExtend<ClassError0>()
697
+ })
698
+
686
699
  it('Error0 assignable to LikeError0', () => {
687
- type LikeError0 = {
688
- from: (error: unknown) => Error
689
- serialize: (error: Error) => Record<string, unknown>
700
+ class MyError extends Error {
701
+ status?: number
702
+ code?: string
703
+ constructor(message: string, options: { cause?: unknown; status?: number; code?: string }) {
704
+ super(message, { cause: options.cause })
705
+ this.status = options.status
706
+ this.code = options.code
707
+ }
708
+
709
+ static from(error: unknown): MyError {
710
+ if (error instanceof MyError) {
711
+ return error
712
+ }
713
+ const object = typeof error === 'object' && error !== null ? (error as Record<string, unknown>) : {}
714
+ const message =
715
+ typeof object.message === 'string' ? object.message : typeof error === 'string' ? error : 'Unknown error'
716
+ const status = typeof object.status === 'number' ? object.status : undefined
717
+ const code = typeof object.code === 'string' ? object.code : undefined
718
+ return new MyError(message, {
719
+ cause: error,
720
+ status,
721
+ code,
722
+ })
723
+ }
724
+
725
+ static serialize(error: MyError): Record<string, unknown> {
726
+ return {
727
+ message: error.message,
728
+ status: error.status,
729
+ code: error.code,
730
+ }
731
+ }
690
732
  }
691
- expectTypeOf<typeof Error0>().toExtend<LikeError0>()
692
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
693
- const AppError = Error0.use(statusPlugin)
694
- expectTypeOf<typeof AppError>().toExtend<LikeError0>()
733
+
734
+ type LikeError0<TError> = {
735
+ new (message: string, options: { cause?: unknown; status?: number; code?: string }): TError
736
+ from: (error: unknown) => TError
737
+ serialize: (error: TError) => Record<string, unknown>
738
+ }
739
+ expectTypeOf<typeof MyError>().toExtend<LikeError0<MyError>>()
740
+ expectTypeOf<typeof Error0>().toExtend<typeof MyError>()
695
741
  })
696
742
 
697
743
  // we will have no variants
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export type ErrorPluginMethodFn<TOutputValue, TArgs extends unknown[] = unknown[
66
66
  error: TError,
67
67
  ...args: TArgs
68
68
  ) => TOutputValue
69
+ type ErrorPluginAnyMethodFn = (error: any, ...args: any[]) => any
69
70
  export type ErrorPluginAdaptResult<TOutputProps extends Record<string, unknown>> = Partial<TOutputProps> | undefined
70
71
  export type ErrorPluginAdaptFn<
71
72
  TError extends Error0 = Error0,
@@ -89,13 +90,10 @@ export type ErrorPluginMessageSerialize<TError extends Error0> = (options: {
89
90
  isPublic: boolean
90
91
  }) => unknown
91
92
  export type ErrorPluginMessage<TError extends Error0 = Error0> = { serialize: ErrorPluginMessageSerialize<TError> }
92
- type ErrorMethodRecord = {
93
- args: unknown[]
94
- output: unknown
95
- }
93
+ type ErrorMethodRecord = { fn: ErrorPluginAnyMethodFn }
96
94
 
97
95
  export type ErrorPluginProps = { [key: string]: ErrorPluginPropOptions<any, any> }
98
- export type ErrorPluginMethods = { [key: string]: ErrorPluginMethodFn<any, any[]> }
96
+ export type ErrorPluginMethods = { [key: string]: ErrorPluginAnyMethodFn }
99
97
 
100
98
  export type ErrorPlugin<
101
99
  TProps extends ErrorPluginProps = Record<never, never>,
@@ -118,9 +116,8 @@ type AddPropToPluginProps<
118
116
  type AddMethodToPluginMethods<
119
117
  TMethods extends ErrorPluginMethods,
120
118
  TKey extends string,
121
- TArgs extends unknown[],
122
- TOutputValue,
123
- > = TMethods & Record<TKey, ErrorPluginMethodFn<TOutputValue, TArgs>>
119
+ TMethod extends ErrorPluginAnyMethodFn,
120
+ > = TMethods & Record<TKey, TMethod>
124
121
  type PluginOutputProps<TProps extends ErrorPluginProps> = {
125
122
  [TKey in keyof TProps]: TProps[TKey] extends ErrorPluginPropOptions<any, any, any, infer TResolveValue>
126
123
  ? TResolveValue
@@ -160,24 +157,80 @@ type ErrorOwnMethods<TPluginsMap extends ErrorPluginsMap> = {
160
157
  type ErrorResolveMethods<TPluginsMap extends ErrorPluginsMap> = {
161
158
  resolve: () => ErrorResolvedProps<TPluginsMap>
162
159
  }
160
+ type BindInstanceMethod<TMethod> = TMethod extends {
161
+ (error: any, ...args: infer TArgs1): infer TOutput1
162
+ (error: any, ...args: infer TArgs2): infer TOutput2
163
+ (error: any, ...args: infer TArgs3): infer TOutput3
164
+ (error: any, ...args: infer TArgs4): infer TOutput4
165
+ }
166
+ ? {
167
+ (...args: TArgs1): TOutput1
168
+ (...args: TArgs2): TOutput2
169
+ (...args: TArgs3): TOutput3
170
+ (...args: TArgs4): TOutput4
171
+ }
172
+ : TMethod extends {
173
+ (error: any, ...args: infer TArgs1): infer TOutput1
174
+ (error: any, ...args: infer TArgs2): infer TOutput2
175
+ (error: any, ...args: infer TArgs3): infer TOutput3
176
+ }
177
+ ? {
178
+ (...args: TArgs1): TOutput1
179
+ (...args: TArgs2): TOutput2
180
+ (...args: TArgs3): TOutput3
181
+ }
182
+ : TMethod extends {
183
+ (error: any, ...args: infer TArgs1): infer TOutput1
184
+ (error: any, ...args: infer TArgs2): infer TOutput2
185
+ }
186
+ ? {
187
+ (...args: TArgs1): TOutput1
188
+ (...args: TArgs2): TOutput2
189
+ }
190
+ : TMethod extends (error: any, ...args: infer TArgs) => infer TOutput
191
+ ? (...args: TArgs) => TOutput
192
+ : never
193
+ type BindStaticMethod<TMethod> = TMethod extends {
194
+ (error: any, ...args: infer TArgs1): infer TOutput1
195
+ (error: any, ...args: infer TArgs2): infer TOutput2
196
+ (error: any, ...args: infer TArgs3): infer TOutput3
197
+ (error: any, ...args: infer TArgs4): infer TOutput4
198
+ }
199
+ ? {
200
+ (error: unknown, ...args: TArgs1): TOutput1
201
+ (error: unknown, ...args: TArgs2): TOutput2
202
+ (error: unknown, ...args: TArgs3): TOutput3
203
+ (error: unknown, ...args: TArgs4): TOutput4
204
+ }
205
+ : TMethod extends {
206
+ (error: any, ...args: infer TArgs1): infer TOutput1
207
+ (error: any, ...args: infer TArgs2): infer TOutput2
208
+ (error: any, ...args: infer TArgs3): infer TOutput3
209
+ }
210
+ ? {
211
+ (error: unknown, ...args: TArgs1): TOutput1
212
+ (error: unknown, ...args: TArgs2): TOutput2
213
+ (error: unknown, ...args: TArgs3): TOutput3
214
+ }
215
+ : TMethod extends {
216
+ (error: any, ...args: infer TArgs1): infer TOutput1
217
+ (error: any, ...args: infer TArgs2): infer TOutput2
218
+ }
219
+ ? {
220
+ (error: unknown, ...args: TArgs1): TOutput1
221
+ (error: unknown, ...args: TArgs2): TOutput2
222
+ }
223
+ : TMethod extends (error: any, ...args: infer TArgs) => infer TOutput
224
+ ? (error: unknown, ...args: TArgs) => TOutput
225
+ : never
163
226
  type ErrorMethods<TPluginsMap extends ErrorPluginsMap> = {
164
- [TKey in keyof TPluginsMap['methods']]: TPluginsMap['methods'][TKey] extends {
165
- args: infer TArgs extends unknown[]
166
- output: infer TOutput
167
- }
168
- ? (...args: TArgs) => TOutput
169
- : never
227
+ [TKey in keyof TPluginsMap['methods']]: BindInstanceMethod<TPluginsMap['methods'][TKey]['fn']>
170
228
  }
171
229
  export type ErrorResolved<TPluginsMap extends ErrorPluginsMap> = ErrorResolvedProps<TPluginsMap> &
172
230
  ErrorMethods<TPluginsMap>
173
231
 
174
232
  type ErrorStaticMethods<TPluginsMap extends ErrorPluginsMap> = {
175
- [TKey in keyof TPluginsMap['methods']]: TPluginsMap['methods'][TKey] extends {
176
- args: infer TArgs extends unknown[]
177
- output: infer TOutput
178
- }
179
- ? (error: unknown, ...args: TArgs) => TOutput
180
- : never
233
+ [TKey in keyof TPluginsMap['methods']]: BindStaticMethod<TPluginsMap['methods'][TKey]['fn']>
181
234
  }
182
235
 
183
236
  type EmptyPluginsMap = {
@@ -214,12 +267,11 @@ type PluginPropsMapOf<TPlugin extends ErrorPlugin> = {
214
267
  : never
215
268
  }
216
269
  type PluginMethodsMapOf<TPlugin extends ErrorPlugin> = {
217
- [TKey in keyof NonNullable<TPlugin['methods']>]: NonNullable<TPlugin['methods']>[TKey] extends (
218
- error: Error0,
219
- ...args: infer TArgs extends unknown[]
220
- ) => infer TOutput
221
- ? { args: TArgs; output: TOutput }
222
- : never
270
+ [TKey in keyof NonNullable<TPlugin['methods']>]: {
271
+ fn: NonNullable<TPlugin['methods']>[TKey] extends ErrorPluginAnyMethodFn
272
+ ? NonNullable<TPlugin['methods']>[TKey]
273
+ : never
274
+ }
223
275
  }
224
276
  type ErrorPluginsMapOfPlugin<TPlugin extends ErrorPlugin> = {
225
277
  props: PluginPropsMapOf<TPlugin>
@@ -242,12 +294,8 @@ type ExtendErrorPluginsMapWithProp<
242
294
  type ExtendErrorPluginsMapWithMethod<
243
295
  TMap extends ErrorPluginsMap,
244
296
  TKey extends string,
245
- TArgs extends unknown[],
246
- TOutputValue,
247
- > = ExtendErrorPluginsMap<
248
- TMap,
249
- ErrorPlugin<Record<never, never>, Record<TKey, ErrorPluginMethodFn<TOutputValue, TArgs>>>
250
- >
297
+ TMethod extends ErrorPluginAnyMethodFn,
298
+ > = ExtendErrorPluginsMap<TMap, ErrorPlugin<Record<never, never>, Record<TKey, TMethod>>>
251
299
 
252
300
  type PluginsMapOf<TClass> = TClass extends { __pluginsMap?: infer TPluginsMap }
253
301
  ? TPluginsMap extends ErrorPluginsMap
@@ -312,10 +360,10 @@ export class PluginError0<
312
360
  return this.use('prop', key, value)
313
361
  }
314
362
 
315
- method<TKey extends string, TArgs extends unknown[], TOutputValue>(
363
+ method<TKey extends string, TMethod extends (error: BuilderError0<TProps, TMethods>, ...args: any[]) => any>(
316
364
  key: TKey,
317
- value: ErrorPluginMethodFn<TOutputValue, TArgs, BuilderError0<TProps, TMethods>>,
318
- ): PluginError0<TProps, AddMethodToPluginMethods<TMethods, TKey, TArgs, TOutputValue>> {
365
+ value: TMethod,
366
+ ): PluginError0<TProps, AddMethodToPluginMethods<TMethods, TKey, TMethod>> {
319
367
  return this.use('method', key, value)
320
368
  }
321
369
 
@@ -347,11 +395,11 @@ export class PluginError0<
347
395
  key: TKey,
348
396
  value: ErrorPluginPropOptions<TInputValue, TOutputValue, BuilderError0<TProps, TMethods>, TResolveValue>,
349
397
  ): PluginError0<AddPropToPluginProps<TProps, TKey, TInputValue, TOutputValue, TResolveValue>, TMethods>
350
- use<TKey extends string, TArgs extends unknown[], TOutputValue>(
398
+ use<TKey extends string, TMethod extends (error: BuilderError0<TProps, TMethods>, ...args: any[]) => any>(
351
399
  kind: 'method',
352
400
  key: TKey,
353
- value: ErrorPluginMethodFn<TOutputValue, TArgs, BuilderError0<TProps, TMethods>>,
354
- ): PluginError0<TProps, AddMethodToPluginMethods<TMethods, TKey, TArgs, TOutputValue>>
401
+ value: TMethod,
402
+ ): PluginError0<TProps, AddMethodToPluginMethods<TMethods, TKey, TMethod>>
355
403
  use(
356
404
  kind: 'adapt',
357
405
  value: ErrorPluginAdaptFn<BuilderError0<TProps, TMethods>, PluginOutputProps<TProps>>,
@@ -460,10 +508,10 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
460
508
  // key: TKey,
461
509
  // value: ErrorPluginPropOptions<TInputValue, TOutputValue, ErrorInstanceOfMap<TPluginsMap>, TResolveValue>,
462
510
  // ) => ClassError0<ExtendErrorPluginsMapWithProp<TPluginsMap, TKey, TInputValue, TOutputValue, TResolveValue>>
463
- // method: <TKey extends string, TArgs extends unknown[], TOutputValue>(
511
+ // method: <TKey extends string, TMethod extends (error: ErrorInstanceOfMap<TPluginsMap>, ...args: any[]) => any>(
464
512
  // key: TKey,
465
- // value: ErrorPluginMethodFn<TOutputValue, TArgs, ErrorInstanceOfMap<TPluginsMap>>,
466
- // ) => ClassError0<ExtendErrorPluginsMapWithMethod<TPluginsMap, TKey, TArgs, TOutputValue>>
513
+ // value: TMethod,
514
+ // ) => ClassError0<ExtendErrorPluginsMapWithMethod<TPluginsMap, TKey, TMethod>>
467
515
  // adapt: (
468
516
  // value: ErrorPluginAdaptFn<ErrorInstanceOfMap<TPluginsMap>, ErrorResolvedProps<TPluginsMap>>,
469
517
  // ) => ClassError0<TPluginsMap>
@@ -483,11 +531,11 @@ export type ClassError0<TPluginsMap extends ErrorPluginsMap = EmptyPluginsMap> =
483
531
  key: TKey,
484
532
  value: ErrorPluginPropOptions<TInputValue, TOutputValue, ErrorInstanceOfMap<TPluginsMap>, TResolveValue>,
485
533
  ): ClassError0<ExtendErrorPluginsMapWithProp<TPluginsMap, TKey, TInputValue, TOutputValue, TResolveValue>>
486
- <TKey extends string, TArgs extends unknown[], TOutputValue>(
534
+ <TKey extends string, TMethod extends (error: ErrorInstanceOfMap<TPluginsMap>, ...args: any[]) => any>(
487
535
  kind: 'method',
488
536
  key: TKey,
489
- value: ErrorPluginMethodFn<TOutputValue, TArgs, ErrorInstanceOfMap<TPluginsMap>>,
490
- ): ClassError0<ExtendErrorPluginsMapWithMethod<TPluginsMap, TKey, TArgs, TOutputValue>>
537
+ value: TMethod,
538
+ ): ClassError0<ExtendErrorPluginsMapWithMethod<TPluginsMap, TKey, TMethod>>
491
539
  (
492
540
  kind: 'adapt',
493
541
  value: ErrorPluginAdaptFn<ErrorInstanceOfMap<TPluginsMap>, ErrorResolvedProps<TPluginsMap>>,
@@ -648,29 +696,17 @@ export class Error0 extends Error {
648
696
 
649
697
  private static readonly isOwnProperty = (object: object, key: string): boolean => {
650
698
  const ownStore = this._getOwnStore(object)
651
- if (ownStore && Object.prototype.hasOwnProperty.call(ownStore, key)) {
652
- return true
699
+ if (ownStore) {
700
+ return Object.prototype.hasOwnProperty.call(ownStore, key)
653
701
  }
654
- const d = Object.getOwnPropertyDescriptor(object, key)
655
- if (!d) return false
656
- if (typeof d.get === 'function' || typeof d.set === 'function') {
657
- if ('name' in object && object.name === 'Error0') {
658
- return false
659
- } else {
660
- return true
661
- }
662
- }
663
- return true
702
+ return !!Object.getOwnPropertyDescriptor(object, key)
664
703
  }
665
704
  private static _ownByKey(error: object, key: string): unknown {
666
705
  const ownStore = this._getOwnStore(error)
667
- if (ownStore && Object.prototype.hasOwnProperty.call(ownStore, key)) {
706
+ if (ownStore) {
668
707
  return ownStore[key]
669
708
  }
670
- if (this.isOwnProperty(error, key)) {
671
- return (error as Record<string, unknown>)[key]
672
- }
673
- return undefined
709
+ return (error as Record<string, unknown>)[key]
674
710
  }
675
711
  private static _flowByKey(error: object, key: string): unknown[] {
676
712
  const causes = this.causes(error, true)
@@ -963,11 +999,11 @@ export class Error0 extends Error {
963
999
  // return this.use('prop', key, value)
964
1000
  // }
965
1001
 
966
- // static method<TThis extends typeof Error0, TKey extends string, TArgs extends unknown[], TOutputValue>(
1002
+ // static method<TThis extends typeof Error0, TKey extends string, TMethod extends (error: ErrorInstanceOfMap<PluginsMapOf<TThis>>, ...args: any[]) => any>(
967
1003
  // this: TThis,
968
1004
  // key: TKey,
969
- // value: ErrorPluginMethodFn<TOutputValue, TArgs, ErrorInstanceOfMap<PluginsMapOf<TThis>>>,
970
- // ): ClassError0<ExtendErrorPluginsMapWithMethod<PluginsMapOf<TThis>, TKey, TArgs, TOutputValue>> {
1005
+ // value: TMethod,
1006
+ // ): ClassError0<ExtendErrorPluginsMapWithMethod<PluginsMapOf<TThis>, TKey, TMethod>> {
971
1007
  // return this.use('method', key, value)
972
1008
  // }
973
1009
 
@@ -1008,12 +1044,16 @@ export class Error0 extends Error {
1008
1044
  key: TKey,
1009
1045
  value: ErrorPluginPropOptions<TInputValue, TOutputValue, ErrorInstanceOfMap<PluginsMapOf<TThis>>, TResolveValue>,
1010
1046
  ): ClassError0<ExtendErrorPluginsMapWithProp<PluginsMapOf<TThis>, TKey, TInputValue, TOutputValue, TResolveValue>>
1011
- static use<TThis extends typeof Error0, TKey extends string, TArgs extends unknown[], TOutputValue>(
1047
+ static use<
1048
+ TThis extends typeof Error0,
1049
+ TKey extends string,
1050
+ TMethod extends (error: ErrorInstanceOfMap<PluginsMapOf<TThis>>, ...args: any[]) => any,
1051
+ >(
1012
1052
  this: TThis,
1013
1053
  kind: 'method',
1014
1054
  key: TKey,
1015
- value: ErrorPluginMethodFn<TOutputValue, TArgs, ErrorInstanceOfMap<PluginsMapOf<TThis>>>,
1016
- ): ClassError0<ExtendErrorPluginsMapWithMethod<PluginsMapOf<TThis>, TKey, TArgs, TOutputValue>>
1055
+ value: TMethod,
1056
+ ): ClassError0<ExtendErrorPluginsMapWithMethod<PluginsMapOf<TThis>, TKey, TMethod>>
1017
1057
  static use<TThis extends typeof Error0>(
1018
1058
  this: TThis,
1019
1059
  kind: 'adapt',
@@ -1,7 +1,7 @@
1
1
  import { Error0 } from '../index.js'
2
2
 
3
3
  export const causeSerializePlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boolean } = {}) =>
4
- Error0.plugin().use('cause', {
4
+ Error0.plugin().cause({
5
5
  serialize: ({ value, error, isPublic }) => {
6
6
  if (hideWhenPublic && isPublic) {
7
7
  return undefined
@@ -0,0 +1,27 @@
1
+ import { describe, expect, expectTypeOf, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { codePlugin } from './code.js'
4
+
5
+ describe('codePlugin', () => {
6
+ const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
7
+
8
+ it('serializes and deserializes allowed codes', () => {
9
+ const AppError = Error0.use(codePlugin({ codes: [...codes] }))
10
+ const error = new AppError('test', { code: 'NOT_FOUND' })
11
+
12
+ expect(error.code).toBe('NOT_FOUND')
13
+ expectTypeOf<typeof error.code>().toEqualTypeOf<'NOT_FOUND' | 'BAD_REQUEST' | 'UNAUTHORIZED' | undefined>()
14
+
15
+ const json = AppError.serialize(error, false)
16
+ expect(json.code).toBe('NOT_FOUND')
17
+
18
+ const recreated = AppError.from(json)
19
+ expect(recreated.code).toBe('NOT_FOUND')
20
+ })
21
+
22
+ it('ignores code values outside the allowed list', () => {
23
+ const AppError = Error0.use(codePlugin({ codes: [...codes] }))
24
+ const recreated = AppError.from({ message: 'test', code: 'SOMETHING_ELSE' })
25
+ expect(recreated.code).toBeUndefined()
26
+ })
27
+ })
@@ -0,0 +1,12 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ export const codePlugin = <TCode extends string>({ codes }: { codes?: TCode[] } = {}) => {
4
+ const isCode = (value: unknown): value is TCode =>
5
+ typeof value === 'string' && (!codes || codes.includes(value as TCode))
6
+ return Error0.plugin().prop('code', {
7
+ init: (code: TCode) => code,
8
+ resolve: ({ flow }) => flow.find(Boolean),
9
+ serialize: ({ resolved, isPublic }) => resolved,
10
+ deserialize: ({ value, record }) => (isCode(value) ? value : undefined),
11
+ })
12
+ }
@@ -15,7 +15,7 @@ const isExpected = (flow: unknown[]) => {
15
15
 
16
16
  export const expectedPlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boolean } = {}) =>
17
17
  Error0.plugin()
18
- .use('prop', 'expected', {
18
+ .prop('expected', {
19
19
  init: (input: boolean) => input,
20
20
  resolve: ({ flow }) => isExpected(flow),
21
21
  serialize: ({ resolved, isPublic }) => {
@@ -26,6 +26,6 @@ export const expectedPlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boo
26
26
  },
27
27
  deserialize: ({ value }) => (typeof value === 'boolean' ? value : undefined),
28
28
  })
29
- .use('method', 'isExpected', (error) => {
29
+ .method('isExpected', (error) => {
30
30
  return isExpected(error.flow('expected'))
31
31
  })
@@ -29,7 +29,7 @@ const isMetaRecord = (value: unknown): value is Record<string, unknown> =>
29
29
  typeof value === 'object' && value !== null && !Array.isArray(value)
30
30
 
31
31
  export const metaPlugin = ({ hideWhenPublic = true }: { hideWhenPublic?: boolean } = {}) =>
32
- Error0.plugin().use('prop', 'meta', {
32
+ Error0.plugin().prop('meta', {
33
33
  init: (input: Record<string, unknown>) => input,
34
34
  resolve: ({ flow }) => {
35
35
  const values = flow.filter(isMetaRecord)
@@ -4,7 +4,7 @@ export const stackMergePlugin = ({
4
4
  hideWhenPublic = true,
5
5
  delimiter = '\n',
6
6
  }: { hideWhenPublic?: boolean; delimiter?: string } = {}) =>
7
- Error0.plugin().use('stack', {
7
+ Error0.plugin().stack({
8
8
  serialize: ({ error, isPublic }) => {
9
9
  if (hideWhenPublic && isPublic) {
10
10
  return undefined
@@ -0,0 +1,54 @@
1
+ import { describe, expect, expectTypeOf, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { statusPlugin } from './status.js'
4
+
5
+ describe('statusPlugin', () => {
6
+ const statuses = {
7
+ BAD_REQUEST: 400,
8
+ UNAUTHORIZED: 401,
9
+ NOT_FOUND: 404,
10
+ } as const
11
+
12
+ it('maps status keys to numeric values', () => {
13
+ const AppError = Error0.use(statusPlugin({ statuses }))
14
+ const error = new AppError('test', { status: 'NOT_FOUND' })
15
+
16
+ expectTypeOf<typeof error.status>().toEqualTypeOf<number | undefined>()
17
+ expect(error.status).toBe(404)
18
+
19
+ const json = AppError.serialize(error, false)
20
+ expect(json.status).toBe(404)
21
+ })
22
+
23
+ it('accepts numeric status values when no statuses map is provided', () => {
24
+ const AppError = Error0.use(statusPlugin())
25
+ const error = new AppError('test', { status: 500 })
26
+ expect(error.status).toBe(500)
27
+ })
28
+
29
+ it('if status number not in list, it is not converts to undefined by default', () => {
30
+ const AppError = Error0.use(statusPlugin({ statuses }))
31
+ const error = new AppError('test', { status: 999 })
32
+ expect(error.status).toBe(999)
33
+ })
34
+
35
+ it('if status number not in list, it converts to undefined if strict is true', () => {
36
+ const AppError = Error0.use(statusPlugin({ statuses, strict: true }))
37
+ const error = new AppError('test', { status: 999 })
38
+ expect(error.status).toBeUndefined()
39
+ })
40
+
41
+ it('not allowed incorrect status name', () => {
42
+ const AppError = Error0.use(statusPlugin({ statuses }))
43
+ // @ts-expect-error - incorrect status name
44
+ const error = new AppError('test', { status: 'SOMETHING_ELSE' })
45
+ expect(error.status).toBeUndefined()
46
+ })
47
+
48
+ it('not allowed status name if statuses not provided', () => {
49
+ const AppError = Error0.use(statusPlugin())
50
+ // @ts-expect-error - incorrect status name
51
+ const error = new AppError('test', { status: 'SOMETHING_ELSE' })
52
+ expect(error.status).toBeUndefined()
53
+ })
54
+ })
@@ -0,0 +1,29 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ export const statusPlugin = <TStatuses extends Record<string, number> = Record<never, number>>({
4
+ statuses,
5
+ strict = false,
6
+ }: { statuses?: TStatuses; strict?: boolean } = {}) => {
7
+ const statusValues = statuses ? Object.values(statuses) : undefined
8
+ const isStatusValue = (value: unknown): value is number =>
9
+ typeof value === 'number' && (!statusValues || !strict || statusValues.includes(value))
10
+ const normalizeStatusValue = (value: unknown): number | undefined => {
11
+ return isStatusValue(value) ? value : undefined
12
+ }
13
+ const convertStatusValue = (value: number | string): number | undefined => {
14
+ if (typeof value === 'number') {
15
+ return normalizeStatusValue(value)
16
+ }
17
+ if (statuses && value in statuses) {
18
+ return statuses[value as keyof TStatuses]
19
+ }
20
+ return undefined
21
+ }
22
+
23
+ return Error0.plugin().prop('status', {
24
+ init: (status: number | Extract<keyof TStatuses, string>) => convertStatusValue(status),
25
+ resolve: ({ flow }) => flow.find(Boolean),
26
+ serialize: ({ resolved }) => resolved,
27
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
28
+ })
29
+ }
@@ -3,12 +3,14 @@ import { Error0 } from '../index.js'
3
3
  import { tagsPlugin } from './tags.js'
4
4
 
5
5
  describe('tagsPlugin', () => {
6
+ const tags = ['api', 'db', 'retry'] as const
7
+
6
8
  it('merges and deduplicates tags across causes', () => {
7
9
  const AppError = Error0.use(tagsPlugin())
8
10
  const root = new AppError('root', { tags: ['db', 'retry'] })
9
11
  const leaf = new AppError('leaf', { tags: ['api', 'retry'], cause: root })
10
- expect(leaf.resolve().tags).toEqual(['api', 'retry', 'db'])
11
- expectTypeOf(leaf.resolve().tags).toEqualTypeOf<string[] | undefined>()
12
+ expect(leaf.tags).toEqual(['api', 'retry', 'db'])
13
+ expectTypeOf(leaf.tags).toEqualTypeOf<string[] | undefined>()
12
14
  expectTypeOf(leaf.own('tags')).toEqualTypeOf<string[] | undefined>()
13
15
  expectTypeOf(leaf.flow('tags')).toEqualTypeOf<Array<string[] | undefined>>()
14
16
  })
@@ -19,4 +21,54 @@ describe('tagsPlugin', () => {
19
21
  expect(AppError.serialize(error, false).tags).toEqual(['internal'])
20
22
  expect('tags' in AppError.serialize(error, true)).toBe(false)
21
23
  })
24
+
25
+ it('supports hasTag for single, some, and every checks', () => {
26
+ const AppError = Error0.use(tagsPlugin())
27
+ const root = new AppError('root', { tags: ['db', 'retry'] })
28
+ const leaf = new AppError('leaf', { tags: ['api'], cause: root })
29
+
30
+ expect(AppError.hasTag(leaf, 'db')).toBe(true)
31
+ expect(leaf.hasTag('db')).toBe(true)
32
+ expect(leaf.hasTag('unknown')).toBe(false)
33
+ expect(leaf.hasTag(['api', 'db'], 'some')).toBe(true)
34
+ expect(leaf.hasTag(['api', 'db'], 'every')).toBe(true)
35
+ expect(leaf.hasTag(['api', 'unknown'], 'every')).toBe(false)
36
+ })
37
+
38
+ it('filters tags not in the allow-list when strict is true', () => {
39
+ const AppError = Error0.use(tagsPlugin({ tags: [...tags], strict: true }))
40
+ const recreated = AppError.from({
41
+ name: 'Error0',
42
+ message: 'test',
43
+ tags: ['db', 'invalid', 123],
44
+ })
45
+ expect(recreated.tags).toEqual(['db'])
46
+ })
47
+
48
+ it('keeps all string tags when strict is false', () => {
49
+ const AppError = Error0.use(tagsPlugin({ tags: [...tags], strict: false }))
50
+ const recreated = AppError.from({
51
+ name: 'Error0',
52
+ message: 'test',
53
+ tags: ['db', 'custom', 123],
54
+ })
55
+
56
+ expect(recreated.tags).toEqual(['db', 'custom'])
57
+ })
58
+
59
+ it('enforces typed hasTag inputs when allow-list is provided', () => {
60
+ const AppError = Error0.use(tagsPlugin({ tags }))
61
+ const error = new AppError('test', { tags: ['api', 'db'] })
62
+
63
+ expectTypeOf(error.hasTag('api')).toEqualTypeOf<boolean>()
64
+ expectTypeOf(error.hasTag(['api', 'db'], 'every')).toEqualTypeOf<boolean>()
65
+ expectTypeOf(error.hasTag(['api', 'retry'], 'some')).toEqualTypeOf<boolean>()
66
+
67
+ // @ts-expect-error - unknown tag is not part of allow-list
68
+ error.hasTag('custom')
69
+ // @ts-expect-error - array checks require policy argument
70
+ error.hasTag(['api', 'db'])
71
+ // @ts-expect-error - unsupported policy
72
+ error.hasTag(['api', 'db'], 'all')
73
+ })
22
74
  })