@devp0nt/error0 1.0.0-next.40 → 1.0.0-next.42

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/src/index.test.ts CHANGED
@@ -20,37 +20,23 @@ const fixStack = (stack: string | undefined) => {
20
20
  }
21
21
 
22
22
  describe('Error0', () => {
23
- const statusExtension = Error0.extension()
23
+ const statusPlugin = Error0.plugin()
24
24
  .prop('status', {
25
- input: (value: number) => value,
26
- output: (error) => {
27
- for (const value of error.flow('status')) {
28
- const status = Number(value)
29
- if (!Number.isNaN(status)) {
30
- return status
31
- }
32
- }
33
- return undefined
34
- },
35
- serialize: (value) => value,
25
+ init: (input: number) => input,
26
+ resolve: ({ flow }) => flow.find(Boolean),
27
+ serialize: ({ value }) => value,
28
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
36
29
  })
37
30
  .method('isStatus', (error, status: number) => error.status === status)
38
31
 
39
32
  const codes = ['NOT_FOUND', 'BAD_REQUEST', 'UNAUTHORIZED'] as const
40
33
  type Code = (typeof codes)[number]
41
- const codeExtension = Error0.extension().extend('prop', 'code', {
42
- input: (value: Code) => value,
43
- output: (error) => {
44
- for (const value of error.flow('code')) {
45
- if (typeof value === 'string') {
46
- if (codes.includes(value as Code)) {
47
- return value as Code
48
- }
49
- }
50
- }
51
- return undefined
52
- },
53
- serialize: (value, error, isPublic) => (isPublic ? undefined : value),
34
+ const codePlugin = Error0.plugin().use('prop', 'code', {
35
+ init: (input: Code) => input,
36
+ resolve: ({ flow }) => flow.find(Boolean),
37
+ serialize: ({ value, isPublic }) => (isPublic ? undefined : value),
38
+ deserialize: ({ value }) =>
39
+ typeof value === 'string' && codes.includes(value as Code) ? (value as Code) : undefined,
54
40
  })
55
41
 
56
42
  it('simple', () => {
@@ -66,19 +52,12 @@ describe('Error0', () => {
66
52
  `)
67
53
  })
68
54
 
69
- it('with direct prop extension', () => {
70
- const AppError = Error0.extend('prop', 'status', {
71
- input: (value: number) => value,
72
- output: (error: Error0) => {
73
- for (const value of error.flow('status')) {
74
- const status = Number(value)
75
- if (!Number.isNaN(status)) {
76
- return status
77
- }
78
- }
79
- return undefined
80
- },
81
- serialize: (value: number | undefined) => value,
55
+ it('with direct prop plugin', () => {
56
+ const AppError = Error0.use('prop', 'status', {
57
+ init: (input: number) => input,
58
+ resolve: ({ flow }) => flow.find(Boolean),
59
+ serialize: ({ value }) => value,
60
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
82
61
  })
83
62
  const error = new AppError('test', { status: 400 })
84
63
  expect(error).toBeInstanceOf(AppError)
@@ -94,8 +73,32 @@ describe('Error0', () => {
94
73
  expectTypeOf<typeof AppError>().toExtend<ClassError0>()
95
74
  })
96
75
 
97
- it('with defined extension', () => {
98
- const AppError = Error0.extend(statusExtension)
76
+ it('class helpers prop/method/adapt mirror use API', () => {
77
+ const AppError = Error0.prop('status', {
78
+ init: (value: number) => value,
79
+ resolve: ({ value, flow }) => {
80
+ return typeof value === 'number' ? value : undefined
81
+ },
82
+ serialize: ({ value }) => value,
83
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
84
+ })
85
+ .method('isStatus', (error, expectedStatus: number) => error.status === expectedStatus)
86
+ .adapt((error) => {
87
+ if (error.cause instanceof Error && error.status === undefined) {
88
+ return { status: 500 }
89
+ }
90
+ return undefined
91
+ })
92
+
93
+ const error = AppError.from(new Error('inner'))
94
+ expect(error.status).toBe(500)
95
+ expect(error.isStatus(500)).toBe(true)
96
+ expect(AppError.isStatus(error, 500)).toBe(true)
97
+ expectTypeOf<typeof AppError>().toExtend<ClassError0>()
98
+ })
99
+
100
+ it('with defined plugin', () => {
101
+ const AppError = Error0.use(statusPlugin)
99
102
  const error = new AppError('test', { status: 400 })
100
103
  expect(error).toBeInstanceOf(AppError)
101
104
  expect(error).toBeInstanceOf(Error0)
@@ -109,9 +112,9 @@ describe('Error0', () => {
109
112
  `)
110
113
  })
111
114
 
112
- it('twice extended Error0 extends previous by types', () => {
113
- const AppError1 = Error0.extend(statusExtension)
114
- const AppError2 = AppError1.extend(codeExtension)
115
+ it('twice used Error0 extends previous by types', () => {
116
+ const AppError1 = Error0.use(statusPlugin)
117
+ const AppError2 = AppError1.use(codePlugin)
115
118
  const error1 = new AppError1('test', { status: 400 })
116
119
  const error2 = new AppError2('test', { status: 400, code: 'NOT_FOUND' })
117
120
  expect(error1.status).toBe(400)
@@ -126,7 +129,7 @@ describe('Error0', () => {
126
129
  })
127
130
 
128
131
  it('can have cause', () => {
129
- const AppError = Error0.extend(statusExtension)
132
+ const AppError = Error0.use(statusPlugin)
130
133
  const anotherError = new Error('another error')
131
134
  const error = new AppError('test', { status: 400, cause: anotherError })
132
135
  expect(error.status).toBe(400)
@@ -140,7 +143,7 @@ describe('Error0', () => {
140
143
  })
141
144
 
142
145
  it('can have many causes', () => {
143
- const AppError = Error0.extend(statusExtension)
146
+ const AppError = Error0.use(statusPlugin)
144
147
  const anotherError = new Error('another error')
145
148
  const error1 = new AppError('test1', { status: 400, cause: anotherError })
146
149
  const error2 = new AppError('test2', { status: 400, cause: error1 })
@@ -150,7 +153,7 @@ describe('Error0', () => {
150
153
  })
151
154
 
152
155
  it('properties floating', () => {
153
- const AppError = Error0.extend(statusExtension).extend(codeExtension)
156
+ const AppError = Error0.use(statusPlugin).use(codePlugin)
154
157
  const anotherError = new Error('another error')
155
158
  const error1 = new AppError('test1', { status: 400, cause: anotherError })
156
159
  const error2 = new AppError('test2', { code: 'NOT_FOUND', cause: error1 })
@@ -161,51 +164,40 @@ describe('Error0', () => {
161
164
  expect(Error0.causes(error2)).toEqual([error2, error1, anotherError])
162
165
  })
163
166
 
164
- it('serialize uses identity by default and skips undefined extension values', () => {
165
- const AppError = Error0.extend(statusExtension).extend('prop', 'code', {
166
- input: (value: string) => value,
167
- output: (error) => {
168
- for (const value of error.flow('code')) {
169
- if (typeof value === 'string') {
170
- return value
171
- }
172
- }
173
- return undefined
174
- },
167
+ it('serialize uses identity by default and skips undefined plugin values', () => {
168
+ const AppError = Error0.use(statusPlugin).prop('code', {
169
+ init: (input: string) => input,
170
+ resolve: ({ flow }) => flow.find(Boolean),
175
171
  serialize: () => undefined,
172
+ deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
176
173
  })
177
174
  const error = new AppError('test', { status: 401, code: 'secret' })
178
- const json = AppError.serialize(error) as Record<string, unknown>
175
+ const json = AppError.serialize(error)
179
176
  expect(json.status).toBe(401)
180
177
  expect('code' in json).toBe(false)
181
178
  })
182
179
 
183
- it('serialize keeps stack by default without stack extension', () => {
184
- const AppError = Error0.extend(statusExtension)
180
+ it('serialize keeps stack by default without stack plugin', () => {
181
+ const AppError = Error0.use(statusPlugin)
185
182
  const error = new AppError('test', { status: 500 })
186
- const json = AppError.serialize(error) as Record<string, unknown>
183
+ const json = AppError.serialize(error)
187
184
  expect(json.stack).toBe(error.stack)
188
185
  })
189
186
 
190
- it('stack extension can customize serialization of stack prop', () => {
191
- const AppError = Error0.extend('prop', 'stack', {
192
- input: (value: string) => value,
193
- output: (error: Error0) => {
194
- const stack = error.own('stack')
195
- if (typeof stack === 'string') {
196
- return stack
197
- }
198
- return undefined
199
- },
200
- serialize: () => undefined,
187
+ it('stack plugin can customize serialization of stack prop', () => {
188
+ const AppError = Error0.prop('stack', {
189
+ init: (input: string) => input,
190
+ resolve: ({ value }) => (typeof value === 'string' ? value : undefined),
191
+ serialize: ({ value }) => undefined,
192
+ deserialize: ({ value }) => (typeof value === 'string' ? value : undefined),
201
193
  })
202
194
  const error = new AppError('test')
203
- const json = AppError.serialize(error) as Record<string, unknown>
195
+ const json = AppError.serialize(error)
204
196
  expect('stack' in json).toBe(false)
205
197
  })
206
198
 
207
- it('.serialize() -> .from() roundtrip keeps extension values', () => {
208
- const AppError = Error0.extend(statusExtension).extend(codeExtension)
199
+ it('.serialize() -> .from() roundtrip keeps plugin values', () => {
200
+ const AppError = Error0.use(statusPlugin).use(codePlugin)
209
201
  const error = new AppError('test', { status: 409, code: 'NOT_FOUND' })
210
202
  const json = AppError.serialize(error, false)
211
203
  const recreated = AppError.from(json)
@@ -215,31 +207,57 @@ describe('Error0', () => {
215
207
  expect(AppError.serialize(recreated, false)).toEqual(json)
216
208
  })
217
209
 
218
- it('computed values and rich methods work in static/instance modes', () => {
219
- const AppError = Error0.extend(statusExtension)
220
- .extend(codeExtension)
221
- .extend('computed', 'summary', (error) => {
222
- return `${error.message}:${error.code ?? 'none'}`
223
- })
224
- .extend('method', 'hasCode', (error, expectedCode: string) => error.code === expectedCode)
225
-
226
- const error = new AppError('test', { status: 400, code: 'NOT_FOUND' })
227
- expect(error.summary).toBe('test:NOT_FOUND')
228
- expect(error.hasCode('NOT_FOUND')).toBe(true)
229
- expect(AppError.hasCode(error, 'NOT_FOUND')).toBe(true)
230
- expect(AppError.hasCode('just string', 'NOT_FOUND')).toBe(false)
231
- expect('summary' in AppError.serialize(error)).toBe(false)
210
+ it('.serialize() floated props and not serialize causes', () => {
211
+ const AppError = Error0.use(statusPlugin).use(codePlugin)
212
+ const error1 = new AppError('test', { status: 409 })
213
+ const error2 = new AppError('test', { code: 'NOT_FOUND', cause: error1 })
214
+ const json = AppError.serialize(error2, false)
215
+ expect(json.status).toBe(409)
216
+ expect(json.code).toBe('NOT_FOUND')
217
+ expect('cause' in json).toBe(false)
232
218
  })
233
219
 
234
220
  it('serialize can hide props for public output', () => {
235
- const AppError = Error0.extend(statusExtension).extend(codeExtension)
221
+ const AppError = Error0.use(statusPlugin).use(codePlugin)
236
222
  const error = new AppError('test', { status: 401, code: 'NOT_FOUND' })
237
- const privateJson = AppError.serialize(error, false) as Record<string, unknown>
238
- const publicJson = AppError.serialize(error, true) as Record<string, unknown>
223
+ const privateJson = AppError.serialize(error, false)
224
+ const publicJson = AppError.serialize(error, true)
239
225
  expect(privateJson.code).toBe('NOT_FOUND')
240
226
  expect('code' in publicJson).toBe(false)
241
227
  })
242
228
 
229
+ it('prop init without input arg infers undefined-only constructor input', () => {
230
+ const AppError = Error0.prop('computed', {
231
+ init: () => undefined as number | undefined,
232
+ resolve: ({ flow }) => flow.find((item) => typeof item === 'number'),
233
+ serialize: ({ value }) => value,
234
+ deserialize: ({ value }) => (typeof value === 'number' ? value : undefined),
235
+ })
236
+
237
+ const error = new AppError('test')
238
+ expect(error.computed).toBe(undefined)
239
+ expectTypeOf<typeof error.computed>().toEqualTypeOf<number | undefined>()
240
+
241
+ // @ts-expect-error - computed input is disallowed when init has no input arg
242
+ // eslint-disable-next-line no-new
243
+ new AppError('test', { computed: 123 })
244
+ })
245
+
246
+ it('serialize/deserialize can be set to false to disable them', () => {
247
+ const AppError = Error0.prop('status', {
248
+ init: (input: number) => input,
249
+ resolve: ({ value, flow }) => value ?? flow.find((item) => typeof item === 'number'),
250
+ serialize: false,
251
+ deserialize: false,
252
+ })
253
+ const error = new AppError('test', { status: 401 })
254
+ const json = AppError.serialize(error)
255
+ expect('status' in json).toBe(false)
256
+
257
+ const recreated = AppError.from({ ...json, status: 999 })
258
+ expect(recreated.status).toBe(undefined)
259
+ })
260
+
243
261
  it('by default error0 created from another error has same message', () => {
244
262
  const schema = z.object({
245
263
  x: z.string(),
@@ -251,16 +269,16 @@ describe('Error0', () => {
251
269
  expect(error.message).toBe(parsedError.message)
252
270
  })
253
271
 
254
- it('refine message and other props via direct transformations', () => {
272
+ it('adapt message and other props via direct transformations', () => {
255
273
  const schema = z.object({
256
274
  x: z.string(),
257
275
  })
258
276
  const parseResult = schema.safeParse({ x: 123 })
259
277
  const parsedError = parseResult.error
260
278
  assert.ok(parsedError)
261
- const AppError = Error0.extend(statusExtension)
262
- .extend(codeExtension)
263
- .extend('refine', (error) => {
279
+ const AppError = Error0.use(statusPlugin)
280
+ .use(codePlugin)
281
+ .use('adapt', (error) => {
264
282
  if (error.cause instanceof ZodError) {
265
283
  error.status = 422
266
284
  error.code = 'NOT_FOUND'
@@ -277,16 +295,16 @@ describe('Error0', () => {
277
295
  expect(error1.code).toBe(undefined)
278
296
  })
279
297
 
280
- it('refine message and other props via return output values from extension', () => {
298
+ it('adapt message and other props via return output values from plugin', () => {
281
299
  const schema = z.object({
282
300
  x: z.string(),
283
301
  })
284
302
  const parseResult = schema.safeParse({ x: 123 })
285
303
  const parsedError = parseResult.error
286
304
  assert.ok(parsedError)
287
- const AppError = Error0.extend(statusExtension)
288
- .extend(codeExtension)
289
- .extend('refine', (error) => {
305
+ const AppError = Error0.use(statusPlugin)
306
+ .use(codePlugin)
307
+ .adapt((error) => {
290
308
  if (error.cause instanceof ZodError) {
291
309
  error.message = `Validation Error: ${error.message}`
292
310
  return {
@@ -307,21 +325,15 @@ describe('Error0', () => {
307
325
  })
308
326
 
309
327
  it('expected prop can be realized to send or not to send error to your error tracker', () => {
310
- const AppError = Error0.extend(statusExtension)
311
- .extend('prop', 'expected', {
312
- input: (value: boolean) => value,
313
- output: (error) => {
314
- for (const value of error.flow('expected')) {
315
- if (typeof value === 'boolean') {
316
- return value
317
- }
318
- }
319
- return undefined
320
- },
321
- serialize: (value) => value,
328
+ const AppError = Error0.use(statusPlugin)
329
+ .prop('expected', {
330
+ init: (input: boolean) => input,
331
+ resolve: ({ flow }) => flow.find((value) => typeof value === 'boolean'),
332
+ serialize: ({ value }) => value,
333
+ deserialize: ({ value }) => (typeof value === 'boolean' ? value : undefined),
322
334
  })
323
- .extend('method', 'isExpected', (error) => {
324
- return error.expected || false
335
+ .method('isExpected', (error) => {
336
+ return error.expected ?? false
325
337
  })
326
338
  const errorExpected = new AppError('test', { status: 400, expected: true })
327
339
  const errorUnexpected = new AppError('test', { status: 400, expected: false })
@@ -342,13 +354,13 @@ describe('Error0', () => {
342
354
 
343
355
  // we will have no variants
344
356
  // becouse you can thorw any errorm and when you do AppError.from(yourError)
345
- // can use refine to assign desired props to error, it is enough for transport
357
+ // can use adapt to assign desired props to error, it is enough for transport
346
358
  // you even can create computed or method to retrieve your error, so no problems with variants
347
359
 
348
360
  // it('can create and recongnize variant', () => {
349
- // const AppError = Error0.extend(statusExtension)
350
- // .extend(codeExtension)
351
- // .extend('prop', 'userId', {
361
+ // const AppError = Error0.use(statusPlugin)
362
+ // .use(codePlugin)
363
+ // .use('prop', 'userId', {
352
364
  // input: (value: string) => value,
353
365
  // output: (error) => {
354
366
  // for (const value of error.flow('userId')) {