@devp0nt/error0 1.0.0-next.45 → 1.0.0-next.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cjs/index.cjs +80 -64
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +32 -22
  4. package/dist/cjs/plugins/cause-serialize.cjs +38 -0
  5. package/dist/cjs/plugins/cause-serialize.cjs.map +1 -0
  6. package/dist/cjs/plugins/cause-serialize.d.cts +5 -0
  7. package/dist/cjs/plugins/expected.cjs +49 -0
  8. package/dist/cjs/plugins/expected.cjs.map +1 -0
  9. package/dist/cjs/plugins/expected.d.cts +5 -0
  10. package/dist/cjs/plugins/message-merge.cjs +36 -0
  11. package/dist/cjs/plugins/message-merge.cjs.map +1 -0
  12. package/dist/cjs/plugins/message-merge.d.cts +5 -0
  13. package/dist/cjs/plugins/meta.cjs +73 -0
  14. package/dist/cjs/plugins/meta.cjs.map +1 -0
  15. package/dist/cjs/plugins/meta.d.cts +5 -0
  16. package/dist/cjs/plugins/stack-merge.cjs +39 -0
  17. package/dist/cjs/plugins/stack-merge.cjs.map +1 -0
  18. package/dist/cjs/plugins/stack-merge.d.cts +5 -0
  19. package/dist/cjs/plugins/tags.cjs +48 -0
  20. package/dist/cjs/plugins/tags.cjs.map +1 -0
  21. package/dist/cjs/plugins/tags.d.cts +5 -0
  22. package/dist/esm/index.d.ts +32 -22
  23. package/dist/esm/index.js +80 -64
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/plugins/cause-serialize.d.ts +5 -0
  26. package/dist/esm/plugins/cause-serialize.js +14 -0
  27. package/dist/esm/plugins/cause-serialize.js.map +1 -0
  28. package/dist/esm/plugins/expected.d.ts +5 -0
  29. package/dist/esm/plugins/expected.js +25 -0
  30. package/dist/esm/plugins/expected.js.map +1 -0
  31. package/dist/esm/plugins/message-merge.d.ts +5 -0
  32. package/dist/esm/plugins/message-merge.js +12 -0
  33. package/dist/esm/plugins/message-merge.js.map +1 -0
  34. package/dist/esm/plugins/meta.d.ts +5 -0
  35. package/dist/esm/plugins/meta.js +49 -0
  36. package/dist/esm/plugins/meta.js.map +1 -0
  37. package/dist/esm/plugins/stack-merge.d.ts +5 -0
  38. package/dist/esm/plugins/stack-merge.js +15 -0
  39. package/dist/esm/plugins/stack-merge.js.map +1 -0
  40. package/dist/esm/plugins/tags.d.ts +5 -0
  41. package/dist/esm/plugins/tags.js +24 -0
  42. package/dist/esm/plugins/tags.js.map +1 -0
  43. package/package.json +9 -1
  44. package/src/index.test.ts +77 -100
  45. package/src/index.ts +173 -120
  46. package/src/plugins/cause-serialize.test.ts +51 -0
  47. package/src/plugins/cause-serialize.ts +11 -0
  48. package/src/plugins/expected.test.ts +47 -0
  49. package/src/plugins/expected.ts +25 -0
  50. package/src/plugins/message-merge.test.ts +32 -0
  51. package/src/plugins/message-merge.ts +15 -0
  52. package/src/plugins/meta.test.ts +32 -0
  53. package/src/plugins/meta.ts +53 -0
  54. package/src/plugins/stack-merge.test.ts +64 -0
  55. package/src/plugins/stack-merge.ts +16 -0
  56. package/src/plugins/tags.test.ts +22 -0
  57. package/src/plugins/tags.ts +21 -0
@@ -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
+ })
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { stackMergePlugin } from './stack-merge.js'
4
+
5
+ const fixStack = (stack: string | undefined) => {
6
+ if (!stack) {
7
+ return stack
8
+ }
9
+ const lines = stack.split('\n')
10
+ const fixedLines = lines.map((line) => {
11
+ const withoutPath = line.replace(/\(.*\)$/, '(...)')
12
+ return withoutPath
13
+ })
14
+ return fixedLines.join('\n')
15
+ }
16
+
17
+ describe('stackMergePlugin', () => {
18
+ const statusPlugin = Error0.plugin().use('prop', 'status', {
19
+ init: (input: number) => input,
20
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'number'),
21
+ serialize: ({ value }) => value,
22
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
23
+ })
24
+
25
+ const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
26
+ type Code = (typeof codes)[number]
27
+ const codePlugin = Error0.plugin().use('prop', 'code', {
28
+ init: (input: Code) => input,
29
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'string' && codes.includes(value)),
30
+ serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
31
+ deserialize: ({ value }) =>
32
+ typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
33
+ })
34
+
35
+ it('can merge stack across causes in one serialized value', () => {
36
+ const AppError = Error0.use(statusPlugin).use(codePlugin).use(stackMergePlugin)
37
+ const error1 = new AppError('test1', { status: 400, code: 'NOT_FOUND' })
38
+ const error2 = new AppError('test2', { status: 401, cause: error1 })
39
+ const mergedStack1 = error1.serialize(false).stack as string
40
+ const mergedStack2 = error2.serialize(false).stack as string
41
+ const mergedStack2Public = error2.serialize(true).stack as string | undefined
42
+ expect(mergedStack1).toContain('Error0: test1')
43
+ expect(mergedStack2).toContain('Error0: test2')
44
+ expect(mergedStack2).toContain('Error0: test1')
45
+ expect(fixStack(mergedStack1)).toMatchInlineSnapshot(`
46
+ "Error0: test1
47
+ at <anonymous> (...)"
48
+ `)
49
+ expect(fixStack(mergedStack2)).toMatchInlineSnapshot(`
50
+ "Error0: test2
51
+ at <anonymous> (...)
52
+ Error0: test1
53
+ at <anonymous> (...)"
54
+ `)
55
+ expect(mergedStack2Public).toBeUndefined()
56
+ })
57
+
58
+ it('by default serializes stack of this error only', () => {
59
+ const AppError = Error0.use(statusPlugin).use(codePlugin).use(stackMergePlugin)
60
+ const error = new AppError('test', { status: 400, code: 'NOT_FOUND' })
61
+ const json = AppError.serialize(error, false)
62
+ expect(json.stack).toBe(error.stack)
63
+ })
64
+ })
@@ -0,0 +1,16 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ export const stackMergePlugin = Error0.plugin().use('stack', {
4
+ serialize: ({ error, isPublic }) => {
5
+ if (isPublic) {
6
+ return undefined
7
+ }
8
+ return error
9
+ .causes()
10
+ .map((cause) => {
11
+ return cause instanceof Error ? cause.stack : undefined
12
+ })
13
+ .filter((value): value is string => typeof value === 'string')
14
+ .join('\n')
15
+ },
16
+ })
@@ -0,0 +1,22 @@
1
+ import { describe, expect, expectTypeOf, it } from 'bun:test'
2
+ import { Error0 } from '../index.js'
3
+ import { tagsPlugin } from './tags.js'
4
+
5
+ describe('tagsPlugin', () => {
6
+ it('merges and deduplicates tags across causes', () => {
7
+ const AppError = Error0.use(tagsPlugin)
8
+ const root = new AppError('root', { tags: ['db', 'retry'] })
9
+ 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
+ expectTypeOf(leaf.own('tags')).toEqualTypeOf<string[] | undefined>()
13
+ expectTypeOf(leaf.flow('tags')).toEqualTypeOf<Array<string[] | undefined>>()
14
+ })
15
+
16
+ it('serializes tags only for private output', () => {
17
+ const AppError = Error0.use(tagsPlugin)
18
+ const error = new AppError('test', { tags: ['internal'] })
19
+ expect(AppError.serialize(error, false).tags).toEqual(['internal'])
20
+ expect('tags' in AppError.serialize(error, true)).toBe(false)
21
+ })
22
+ })
@@ -0,0 +1,21 @@
1
+ import { Error0 } from '../index.js'
2
+
3
+ export const tagsPlugin = Error0.plugin().use('prop', 'tags', {
4
+ init: (input: string[]) => input,
5
+ resolve: ({ flow }) => {
6
+ const merged: string[] = []
7
+ for (const value of flow) {
8
+ if (Array.isArray(value)) {
9
+ merged.push(...value.filter((item): item is string => typeof item === 'string'))
10
+ }
11
+ }
12
+ return merged.length > 0 ? Array.from(new Set(merged)) : undefined
13
+ },
14
+ serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
15
+ deserialize: ({ value }) => {
16
+ if (!Array.isArray(value)) {
17
+ return undefined
18
+ }
19
+ return value.filter((item): item is string => typeof item === 'string')
20
+ },
21
+ })