@zerospin/error 2.0.1 → 2.1.0
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/.tsbuildinfo/build.tsbuildinfo +1 -1
- package/.turbo/turbo-build.log +13 -0
- package/.turbo/turbo-test.log +14 -0
- package/README.md +485 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +55 -2
- package/package.json +23 -19
- package/src/ZerospinError.ts +38 -33
- package/src/ZerospinErrorJsonSchema.ts +1 -1
- package/src/index.ts +0 -1
- package/tsconfig.json +1 -0
- package/tsup.config.ts +13 -0
- package/dist/ZerospinError.js +0 -44
- package/dist/ZerospinErrorJsonSchema.js +0 -7
- package/dist/types.js +0 -1
- package/src/types.ts +0 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/zerospinerror.ts","../src/zerospinerrorjsonschema.ts","../src/index.ts","../src/types.ts"],"version":"5.9.
|
|
1
|
+
{"root":["../src/zerospinerror.ts","../src/zerospinerrorjsonschema.ts","../src/index.ts","../src/types.ts"],"version":"5.9.3"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
> @zerospin/error@2.0.2 build /Users/morgs32/GitHub/zerospin/packages/error
|
|
3
|
+
> tsup
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: /Users/morgs32/GitHub/zerospin/packages/error/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: es2022
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m1.30 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 7ms
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
> @zerospin/error@2.0.2 test /Users/morgs32/GitHub/zerospin/packages/error
|
|
3
|
+
> vitest packages/profiler/src/toMatchProcedure
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[1m[46m RUN [49m[22m [36mv3.2.4 [39m[90m/Users/morgs32/GitHub/zerospin/packages/error[39m
|
|
7
|
+
|
|
8
|
+
[31mNo test files found, exiting with code 1
|
|
9
|
+
[39m
|
|
10
|
+
[2mfilter: [22m[33mpackages/profiler/src/toMatchProcedure[39m
|
|
11
|
+
[2minclude: [22m[33m**/*.{test,spec}.?(c|m)[jt]s?(x)[39m
|
|
12
|
+
[2mexclude: [22m[33m**/node_modules/**[2m, [22m**/dist/**[2m, [22m**/cypress/**[2m, [22m**/.{idea,git,cache,output,temp}/**[2m, [22m**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*[39m
|
|
13
|
+
|
|
14
|
+
ELIFECYCLE Test failed. See above for more details.
|
package/README.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# @zerospin/error
|
|
2
|
+
|
|
3
|
+
ZeroSpin Error provides a type-safe, structured error system for Effect-based applications. It extends Effect's `Data.TaggedError` to provide consistent error handling with serialization support, error codes, and optional metadata.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @zerospin/error effect
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
### Creating Errors
|
|
14
|
+
|
|
15
|
+
`ZerospinError` can be created in two ways:
|
|
16
|
+
|
|
17
|
+
**Simple form (code only):**
|
|
18
|
+
```typescript
|
|
19
|
+
import { ZerospinError } from '@zerospin/error'
|
|
20
|
+
|
|
21
|
+
const error = new ZerospinError('my-error-code')
|
|
22
|
+
// Creates an error with code: 'my-error-code', message: 'my-error-code'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Full form (with metadata):**
|
|
26
|
+
```typescript
|
|
27
|
+
const error = new ZerospinError({
|
|
28
|
+
code: 'failed-to-fetch',
|
|
29
|
+
message: 'Failed to fetch data from server',
|
|
30
|
+
cause: originalError,
|
|
31
|
+
extra: { url: 'https://api.example.com' },
|
|
32
|
+
status: 500
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Using ZerospinError in Effect Contexts
|
|
37
|
+
|
|
38
|
+
### 1. Throwing Errors in Effect.gen
|
|
39
|
+
|
|
40
|
+
In `Effect.gen` functions, use `yield*` to throw errors:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Effect } from 'effect'
|
|
44
|
+
import { ZerospinError } from '@zerospin/error'
|
|
45
|
+
|
|
46
|
+
const fetchData = Effect.gen(function* () {
|
|
47
|
+
const res = yield* Effect.promise(() => fetch('/api/data'))
|
|
48
|
+
|
|
49
|
+
if (res.status === 404) {
|
|
50
|
+
return yield* new ZerospinError({
|
|
51
|
+
code: 'resource-not-found',
|
|
52
|
+
message: 'Resource not found',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return yield* res.json()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Example from ZeroSpin:**
|
|
61
|
+
```typescript
|
|
62
|
+
// From packages/client/src/ZerospinClientAdapter.ts
|
|
63
|
+
export const ZerospinClientAdapter = Layer.effect(
|
|
64
|
+
ZerospinStorageAdapter,
|
|
65
|
+
Effect.fn('getClientAdapter')(function* () {
|
|
66
|
+
const isOPFSSupported = yield* OPFSAdapter.check().pipe(
|
|
67
|
+
Effect.either,
|
|
68
|
+
Effect.map((either) => Either.isRight(either))
|
|
69
|
+
)
|
|
70
|
+
if (isOPFSSupported) {
|
|
71
|
+
return OPFSAdapter
|
|
72
|
+
}
|
|
73
|
+
const isLocalStorageSupported = yield* LocalStorageAdapter.check().pipe(
|
|
74
|
+
Effect.either,
|
|
75
|
+
Effect.map((either) => Either.isRight(either))
|
|
76
|
+
)
|
|
77
|
+
if (isLocalStorageSupported) {
|
|
78
|
+
return LocalStorageAdapter
|
|
79
|
+
}
|
|
80
|
+
return yield* new ZerospinError({
|
|
81
|
+
code: 'no-adapter-available',
|
|
82
|
+
message: 'No adapter available',
|
|
83
|
+
})
|
|
84
|
+
})()
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Mapping Errors with Effect.mapError
|
|
89
|
+
|
|
90
|
+
Transform errors from other Effect operations:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { Effect } from 'effect'
|
|
94
|
+
import { ZerospinError } from '@zerospin/error'
|
|
95
|
+
|
|
96
|
+
const parseJson = Effect.promise(() => res.json()).pipe(
|
|
97
|
+
Effect.mapError((error) => {
|
|
98
|
+
return new ZerospinError({
|
|
99
|
+
code: 'failed-to-parse-json',
|
|
100
|
+
message: 'Failed to parse JSON',
|
|
101
|
+
cause: error,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Example from ZeroSpin:**
|
|
108
|
+
```typescript
|
|
109
|
+
// From packages/client/src/makeZerospinFetchFrontendApi.ts
|
|
110
|
+
const res = yield* Effect.promise(async () => {
|
|
111
|
+
return fetch(url, {
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
method: 'POST',
|
|
115
|
+
}).catch((error: unknown) => {
|
|
116
|
+
throw new ZerospinError({
|
|
117
|
+
code: 'failed-to-fetch',
|
|
118
|
+
message: 'Failed to fetch',
|
|
119
|
+
cause: error,
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}).pipe(
|
|
123
|
+
Effect.mapError((error) => {
|
|
124
|
+
return new ZerospinError({
|
|
125
|
+
code: 'failed-to-fetch',
|
|
126
|
+
message: 'Failed to fetch',
|
|
127
|
+
cause: error,
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3. Catching All Errors with Effect.catchAll
|
|
134
|
+
|
|
135
|
+
Handle any error and convert it to a ZerospinError:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { Effect } from 'effect'
|
|
139
|
+
import { ZerospinError } from '@zerospin/error'
|
|
140
|
+
|
|
141
|
+
const safeOperation = someEffect.pipe(
|
|
142
|
+
Effect.catchAll((error) => {
|
|
143
|
+
return new ZerospinError({
|
|
144
|
+
code: 'operation-failed',
|
|
145
|
+
message: error.message,
|
|
146
|
+
cause: error,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Example from ZeroSpin:**
|
|
153
|
+
```typescript
|
|
154
|
+
// From packages/zerospin/src/batch/makeSafe.ts
|
|
155
|
+
const safe = yield* validateUnknown({
|
|
156
|
+
onExcessProperty: 'preserve',
|
|
157
|
+
schema,
|
|
158
|
+
value: command,
|
|
159
|
+
}).pipe(
|
|
160
|
+
Effect.catchAll((error) => {
|
|
161
|
+
return new ZerospinError({
|
|
162
|
+
code: 'failed-to-validate-command',
|
|
163
|
+
message: error.message,
|
|
164
|
+
cause: error,
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 4. Handling Promise Errors
|
|
171
|
+
|
|
172
|
+
When working with promises, catch errors and throw ZerospinError:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Effect } from 'effect'
|
|
176
|
+
import { ZerospinError } from '@zerospin/error'
|
|
177
|
+
|
|
178
|
+
const fetchData = Effect.promise(async () => {
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch('/api/data')
|
|
181
|
+
return await response.json()
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new ZerospinError({
|
|
184
|
+
code: 'fetch-failed',
|
|
185
|
+
message: 'Failed to fetch data',
|
|
186
|
+
cause: error,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Example from ZeroSpin:**
|
|
193
|
+
```typescript
|
|
194
|
+
// From packages/client/src/makeZerospinFetchFrontendApi.ts
|
|
195
|
+
const res = yield* Effect.promise(async () => {
|
|
196
|
+
return fetch(url, {
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
method: 'POST',
|
|
200
|
+
}).catch((error: unknown) => {
|
|
201
|
+
throw new ZerospinError({
|
|
202
|
+
code: 'failed-to-fetch',
|
|
203
|
+
message: 'Failed to fetch',
|
|
204
|
+
cause: error,
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 5. Schema Validation Errors
|
|
211
|
+
|
|
212
|
+
Convert schema validation errors to ZerospinError:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { Effect, Schema } from 'effect'
|
|
216
|
+
import { ZerospinError } from '@zerospin/error'
|
|
217
|
+
|
|
218
|
+
const validateData = Schema.decode(Schema.String)(value).pipe(
|
|
219
|
+
Effect.mapError((error) => {
|
|
220
|
+
return new ZerospinError({
|
|
221
|
+
code: 'validation-failed',
|
|
222
|
+
message: error.message,
|
|
223
|
+
cause: error,
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Example from ZeroSpin:**
|
|
230
|
+
```typescript
|
|
231
|
+
// From packages/zerospin/src/utils/encodeUnknown.ts
|
|
232
|
+
export const encodeUnknown = Effect.fn('encodeUnknown')(<
|
|
233
|
+
DECODED,
|
|
234
|
+
ENCODED,
|
|
235
|
+
>(props: {
|
|
236
|
+
schema: Schema.Schema<DECODED, ENCODED>
|
|
237
|
+
value: unknown
|
|
238
|
+
errorMessage?: string
|
|
239
|
+
}): Effect.Effect<ENCODED, ZerospinError<'failed-to-encode-unknown'>> => {
|
|
240
|
+
const { errorMessage, schema, value } = props
|
|
241
|
+
|
|
242
|
+
return Schema.encodeUnknown(schema)(value).pipe(
|
|
243
|
+
Effect.mapError((error) => {
|
|
244
|
+
return new ZerospinError({
|
|
245
|
+
code: 'failed-to-encode-unknown',
|
|
246
|
+
message: errorMessage ?? error.message,
|
|
247
|
+
cause: error,
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Another example:**
|
|
255
|
+
```typescript
|
|
256
|
+
// From packages/zerospin/src/contract/makeContract.ts
|
|
257
|
+
return validateUnknown({
|
|
258
|
+
onExcessProperty: 'error',
|
|
259
|
+
schema: resultsSchema,
|
|
260
|
+
value: results,
|
|
261
|
+
}).pipe(
|
|
262
|
+
Effect.mapError((error) => {
|
|
263
|
+
return new ZerospinError({
|
|
264
|
+
code: 'failed-to-validate-results',
|
|
265
|
+
message: error.message,
|
|
266
|
+
cause: error,
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 6. Deserializing Errors from JSON
|
|
273
|
+
|
|
274
|
+
When receiving errors from APIs or serialized sources:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { ZerospinError } from '@zerospin/error'
|
|
278
|
+
|
|
279
|
+
// From packages/client/src/makeZerospinFetchFrontendApi.ts
|
|
280
|
+
const payload = yield* Effect.promise(() => {
|
|
281
|
+
return res.json() as Promise<IAnyErrorJson | IJson>
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
if (res.status === 200) {
|
|
285
|
+
return payload as IJson
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Deserialize error from JSON
|
|
289
|
+
return yield* new ZerospinError(payload as IAnyErrorJson)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 7. Error Handling in Route Handlers
|
|
293
|
+
|
|
294
|
+
Handle errors at the boundary and serialize for HTTP responses:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Effect, Exit, Cause } from 'effect'
|
|
298
|
+
import { ZerospinError } from '@zerospin/error'
|
|
299
|
+
import { NextResponse } from 'next/server'
|
|
300
|
+
|
|
301
|
+
const exit = await Effect.runPromiseExit(myEffect)
|
|
302
|
+
|
|
303
|
+
return Exit.match(exit, {
|
|
304
|
+
onFailure: (cause) => {
|
|
305
|
+
console.error(Cause.pretty(cause))
|
|
306
|
+
if (Cause.isFailType(cause)) {
|
|
307
|
+
const error = cause.error as ZerospinError
|
|
308
|
+
return NextResponse.json(error.serialize(), {
|
|
309
|
+
status: error.status ?? 400,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
return NextResponse.json(
|
|
313
|
+
new ZerospinError({
|
|
314
|
+
code: 'unexpected-error',
|
|
315
|
+
message: Cause.pretty(cause),
|
|
316
|
+
}).serialize(),
|
|
317
|
+
{ status: 500 }
|
|
318
|
+
)
|
|
319
|
+
},
|
|
320
|
+
onSuccess: (value) => {
|
|
321
|
+
return NextResponse.json(value)
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Example from ZeroSpin:**
|
|
327
|
+
```typescript
|
|
328
|
+
// From packages/zerospin/src/rpc/makeNextRoute.ts
|
|
329
|
+
return Exit.match(exit, {
|
|
330
|
+
onFailure: (cause) => {
|
|
331
|
+
console.error(Cause.pretty(cause))
|
|
332
|
+
if (Cause.isFailType(cause)) {
|
|
333
|
+
const error = cause.error as ZerospinError
|
|
334
|
+
return NextResponse.json(error.serialize(), {
|
|
335
|
+
status: error.status ?? 400,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
return NextResponse.json(
|
|
339
|
+
new ZerospinError({
|
|
340
|
+
code: 'unexpected-error',
|
|
341
|
+
message: Cause.pretty(cause),
|
|
342
|
+
}).serialize(),
|
|
343
|
+
{ status: 500 }
|
|
344
|
+
)
|
|
345
|
+
},
|
|
346
|
+
onSuccess: (value) => {
|
|
347
|
+
return NextResponse.json(value)
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Error Properties
|
|
353
|
+
|
|
354
|
+
### Error Structure
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
interface IError<T extends string = string, E = unknown> {
|
|
358
|
+
code: T // Error code identifier
|
|
359
|
+
status: null | number // HTTP status code (optional)
|
|
360
|
+
cause?: unknown // Original error that caused this
|
|
361
|
+
extra?: E // Additional typed metadata
|
|
362
|
+
message?: string // Human-readable message
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Type Safety with Error Codes
|
|
367
|
+
|
|
368
|
+
You can create type-safe error codes:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
type MyErrorCodes =
|
|
372
|
+
| 'failed-to-fetch'
|
|
373
|
+
| 'validation-failed'
|
|
374
|
+
| 'resource-not-found'
|
|
375
|
+
|
|
376
|
+
const error: ZerospinError<MyErrorCodes> = new ZerospinError({
|
|
377
|
+
code: 'failed-to-fetch', // TypeScript will validate this
|
|
378
|
+
message: 'Failed to fetch',
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Utility Methods
|
|
383
|
+
|
|
384
|
+
### Checking if Something is a ZerospinError
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { ZerospinError } from '@zerospin/error'
|
|
388
|
+
|
|
389
|
+
if (ZerospinError.isZerospinError(error)) {
|
|
390
|
+
console.log(error.code)
|
|
391
|
+
console.log(error.message)
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Example from ZeroSpin:**
|
|
396
|
+
```typescript
|
|
397
|
+
// From packages/zerospin/src/ZerospinErrorLayer.ts
|
|
398
|
+
Exit.mapError(exit, (error) => {
|
|
399
|
+
if (ZerospinError.isZerospinError(error)) {
|
|
400
|
+
if (error.cause) {
|
|
401
|
+
error.message += ` ${JSON.stringify(error.cause, null, 2)}`
|
|
402
|
+
}
|
|
403
|
+
if (error.extra) {
|
|
404
|
+
error.message += ` ${JSON.stringify(error.extra, null, 2)}`
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return error
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Serializing Errors
|
|
412
|
+
|
|
413
|
+
Serialize errors for transmission over networks or storage:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
const error = new ZerospinError({
|
|
417
|
+
code: 'my-error',
|
|
418
|
+
message: 'Something went wrong',
|
|
419
|
+
extra: { actorId: '123' },
|
|
420
|
+
status: 400,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Serialize full error
|
|
424
|
+
const json = error.serialize()
|
|
425
|
+
|
|
426
|
+
// Serialize without certain fields
|
|
427
|
+
const jsonWithoutExtra = error.serialize(['extra'])
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Best Practices
|
|
431
|
+
|
|
432
|
+
1. **Always provide meaningful error codes**: Use descriptive, kebab-case codes like `'failed-to-fetch'` instead of generic ones.
|
|
433
|
+
|
|
434
|
+
2. **Preserve original errors**: Use the `cause` property to maintain error chains:
|
|
435
|
+
```typescript
|
|
436
|
+
new ZerospinError({
|
|
437
|
+
code: 'operation-failed',
|
|
438
|
+
message: 'Operation failed',
|
|
439
|
+
cause: originalError, // Preserve the original error
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
3. **Use typed extra data**: Leverage TypeScript generics for type-safe metadata:
|
|
444
|
+
```typescript
|
|
445
|
+
new ZerospinError<ErrorCode, { actorId: string; action: string }>({
|
|
446
|
+
code: 'permission-denied',
|
|
447
|
+
extra: { actorId: '123', action: 'delete' },
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
4. **Handle at boundaries**: Convert to ZerospinError at Effect boundaries (promises, HTTP handlers, etc.) and let them propagate through your Effect pipeline.
|
|
452
|
+
|
|
453
|
+
5. **Serialize for APIs**: Use `serialize()` when sending errors over HTTP or storing them.
|
|
454
|
+
|
|
455
|
+
## API Reference
|
|
456
|
+
|
|
457
|
+
### ZerospinError Class
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
class ZerospinError<T extends string = never, E = unknown>
|
|
461
|
+
extends Data.TaggedError('ZerospinError')<IError<T, E>>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Constructor:**
|
|
465
|
+
- `new ZerospinError(code: string)` - Simple form
|
|
466
|
+
- `new ZerospinError(props: IProps<T, E>)` - Full form
|
|
467
|
+
|
|
468
|
+
**Static Methods:**
|
|
469
|
+
- `ZerospinError.isZerospinError(data: unknown): data is ZerospinError` - Type guard
|
|
470
|
+
- `ZerospinError.makeZerospinErrorJson(props): IAnyErrorJson` - Create JSON representation
|
|
471
|
+
|
|
472
|
+
**Instance Methods:**
|
|
473
|
+
- `serialize(omit?: string[]): IAnyErrorJson` - Serialize to JSON
|
|
474
|
+
|
|
475
|
+
### Types
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
type IAnyError = ZerospinError<string>
|
|
479
|
+
type IAnyErrorJson = Brand.Brand<'ZerospinErrorJson'> & {
|
|
480
|
+
code: string
|
|
481
|
+
extra: unknown
|
|
482
|
+
message: string
|
|
483
|
+
status: null | number
|
|
484
|
+
}
|
|
485
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as effect_Cause from 'effect/Cause';
|
|
2
|
+
import * as effect_Types from 'effect/Types';
|
|
3
|
+
import { RequiredKeysOf } from 'type-fest';
|
|
4
|
+
import { Brand, Schema } from 'effect';
|
|
5
|
+
|
|
6
|
+
type IAnyErrorJson<ERROR extends IAnyError = IAnyError> = Brand.Brand<'ZerospinErrorJson'> & {
|
|
7
|
+
code: ERROR['code'];
|
|
8
|
+
extra: ERROR['extra'];
|
|
9
|
+
message: string;
|
|
10
|
+
status: null | number;
|
|
11
|
+
};
|
|
12
|
+
type IAnyError = ZerospinError<string>;
|
|
13
|
+
interface IProps<T extends string = string, E = unknown> {
|
|
14
|
+
code: T;
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
extra?: E;
|
|
17
|
+
message?: string;
|
|
18
|
+
status?: null | number;
|
|
19
|
+
}
|
|
20
|
+
interface IError<T extends string = string, E = unknown> {
|
|
21
|
+
code: T;
|
|
22
|
+
status: null | number;
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
extra?: E;
|
|
25
|
+
message?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const ZerospinError_base: new <A extends Record<string, any> = {}>(args: effect_Types.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => effect_Cause.YieldableError & {
|
|
28
|
+
readonly _tag: "ZerospinError";
|
|
29
|
+
} & Readonly<A>;
|
|
30
|
+
declare class ZerospinError<T extends string = never, E = unknown> extends ZerospinError_base<IError<T, E>> {
|
|
31
|
+
message: string;
|
|
32
|
+
constructor(props: IProps<T, E> | T);
|
|
33
|
+
static isZerospinError(data: unknown): data is ZerospinError;
|
|
34
|
+
static makeZerospinErrorJson(props: {
|
|
35
|
+
cause: unknown;
|
|
36
|
+
code: string;
|
|
37
|
+
extra: unknown;
|
|
38
|
+
message: string;
|
|
39
|
+
status: null | number;
|
|
40
|
+
}): IAnyErrorJson;
|
|
41
|
+
serialize(omit?: Exclude<keyof IAnyErrorJson, RequiredKeysOf<IAnyErrorJson>>[]): IAnyErrorJson;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare const ZerospinErrorJsonSchema: Schema.Schema<IAnyErrorJson, any>;
|
|
45
|
+
|
|
46
|
+
export { type IAnyError, type IAnyErrorJson, type IError, ZerospinError, ZerospinErrorJsonSchema };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// src/ZerospinError.ts
|
|
2
|
+
import { Brand, Data } from "effect";
|
|
3
|
+
import { isObject } from "effect/Predicate";
|
|
4
|
+
var ZerospinError = class _ZerospinError extends Data.TaggedError("ZerospinError") {
|
|
5
|
+
message;
|
|
6
|
+
constructor(props) {
|
|
7
|
+
if (typeof props === "string") {
|
|
8
|
+
super({
|
|
9
|
+
code: props,
|
|
10
|
+
extra: null,
|
|
11
|
+
message: props,
|
|
12
|
+
status: null
|
|
13
|
+
});
|
|
14
|
+
this.message = props;
|
|
15
|
+
} else {
|
|
16
|
+
super({
|
|
17
|
+
status: null,
|
|
18
|
+
...props
|
|
19
|
+
});
|
|
20
|
+
this.message = props.message ?? props.code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
static isZerospinError(data) {
|
|
24
|
+
return isObject(data) && "_tag" in data && data._tag === "ZerospinError";
|
|
25
|
+
}
|
|
26
|
+
static makeZerospinErrorJson(props) {
|
|
27
|
+
return Brand.nominal()(props);
|
|
28
|
+
}
|
|
29
|
+
serialize(omit = []) {
|
|
30
|
+
const json = _ZerospinError.makeZerospinErrorJson({
|
|
31
|
+
cause: this.cause,
|
|
32
|
+
code: this.code,
|
|
33
|
+
extra: this.extra,
|
|
34
|
+
message: this.message,
|
|
35
|
+
status: this.status
|
|
36
|
+
});
|
|
37
|
+
for (const key of omit) {
|
|
38
|
+
delete json[key];
|
|
39
|
+
}
|
|
40
|
+
return json;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/ZerospinErrorJsonSchema.ts
|
|
45
|
+
import { Schema } from "effect";
|
|
46
|
+
var ZerospinErrorJsonSchema = Schema.Struct({
|
|
47
|
+
code: Schema.String,
|
|
48
|
+
extra: Schema.Unknown,
|
|
49
|
+
message: Schema.String,
|
|
50
|
+
status: Schema.Union(Schema.Null, Schema.Number)
|
|
51
|
+
});
|
|
52
|
+
export {
|
|
53
|
+
ZerospinError,
|
|
54
|
+
ZerospinErrorJsonSchema
|
|
55
|
+
};
|
package/package.json
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerospin/error",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
|
-
"types": "./
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
9
10
|
"import": "./dist/index.js",
|
|
10
11
|
"require": "./dist/index.js"
|
|
11
12
|
}
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"
|
|
15
|
+
"drizzle-orm": "^0.45.1",
|
|
16
|
+
"type-fest": "^5.4.1"
|
|
15
17
|
},
|
|
16
18
|
"devDependencies": {
|
|
17
|
-
"@effect/language-service": "^0.
|
|
18
|
-
"@effect/vitest": "^0.
|
|
19
|
-
"@
|
|
20
|
-
"@vitest/
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
19
|
+
"@effect/language-service": "^0.68.0",
|
|
20
|
+
"@effect/vitest": "^0.27.0",
|
|
21
|
+
"@types/node": "^22.13.2",
|
|
22
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
23
|
+
"@vitest/pretty-format": "^4.0.17",
|
|
24
|
+
"effect": "^3.19.14",
|
|
25
|
+
"eslint": "^9.39.2",
|
|
26
|
+
"glob": "^13.0.0",
|
|
27
|
+
"jsdom": "^27.4.0",
|
|
28
|
+
"knip": "^5.81.0",
|
|
29
|
+
"tsafe": "^1.8.12",
|
|
30
|
+
"tsup": "^8.5.1",
|
|
31
|
+
"tsx": "^4.21.0",
|
|
32
|
+
"vite-tsconfig-paths": "^6.0.4",
|
|
33
|
+
"vitest": "^4.0.17",
|
|
30
34
|
"@zerospin/utils": "2.0.1"
|
|
31
35
|
},
|
|
32
36
|
"peerDependencies": {
|
|
33
|
-
"effect": "^3.
|
|
37
|
+
"effect": "^3.17.11"
|
|
34
38
|
},
|
|
35
39
|
"scripts": {
|
|
36
|
-
"build": "
|
|
37
|
-
"watch": "
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"watch": "tsup --watch",
|
|
38
42
|
"typecheck": "tsc --noEmit",
|
|
39
43
|
"test": "vitest",
|
|
40
44
|
"lint": "eslint .",
|
package/src/ZerospinError.ts
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
|
-
import type { RequiredKeysOf } from 'type-fest'
|
|
1
|
+
import type { RequiredKeysOf } from 'type-fest';
|
|
2
2
|
|
|
3
|
-
import { Brand, Data } from 'effect'
|
|
4
|
-
import { isObject } from 'effect/Predicate'
|
|
3
|
+
import { Brand, Data } from 'effect';
|
|
4
|
+
import { isObject } from 'effect/Predicate';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
export type IAnyErrorJson<ERROR extends IAnyError = IAnyError> =
|
|
7
|
+
Brand.Brand<'ZerospinErrorJson'> & {
|
|
8
|
+
code: ERROR['code'];
|
|
9
|
+
extra: ERROR['extra'];
|
|
10
|
+
message: string;
|
|
11
|
+
status: null | number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type IAnyError = ZerospinError<string>;
|
|
7
15
|
|
|
8
16
|
interface IProps<T extends string = string, E = unknown> {
|
|
9
|
-
code: T
|
|
10
|
-
cause?: unknown
|
|
11
|
-
extra?: E
|
|
12
|
-
message?: string
|
|
13
|
-
status?: null | number
|
|
17
|
+
code: T;
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
extra?: E;
|
|
20
|
+
message?: string;
|
|
21
|
+
status?: null | number;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export interface IError<T extends string = string, E = unknown> {
|
|
17
|
-
code: T
|
|
18
|
-
status: null | number
|
|
19
|
-
cause?: unknown
|
|
20
|
-
extra?: E
|
|
21
|
-
message?: string
|
|
25
|
+
code: T;
|
|
26
|
+
status: null | number;
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
extra?: E;
|
|
29
|
+
message?: string;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export class ZerospinError<
|
|
25
33
|
T extends string = never,
|
|
26
34
|
E = unknown,
|
|
27
35
|
> extends Data.TaggedError('ZerospinError')<IError<T, E>> {
|
|
28
|
-
public override message!: string
|
|
36
|
+
public override message!: string;
|
|
29
37
|
|
|
30
38
|
constructor(props: IProps<T, E> | T) {
|
|
31
39
|
if (typeof props === 'string') {
|
|
@@ -34,37 +42,34 @@ export class ZerospinError<
|
|
|
34
42
|
extra: null as E,
|
|
35
43
|
message: props,
|
|
36
44
|
status: null,
|
|
37
|
-
})
|
|
38
|
-
this.message = props
|
|
45
|
+
});
|
|
46
|
+
this.message = props;
|
|
39
47
|
} else {
|
|
40
48
|
super({
|
|
41
49
|
status: null,
|
|
42
50
|
...props,
|
|
43
|
-
})
|
|
44
|
-
this.message = props.message ?? props.code
|
|
51
|
+
});
|
|
52
|
+
this.message = props.message ?? props.code;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
static isZerospinError(data: unknown): data is ZerospinError {
|
|
49
|
-
return isObject(data) && '_tag' in data && data._tag === 'ZerospinError'
|
|
57
|
+
return isObject(data) && '_tag' in data && data._tag === 'ZerospinError';
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
static makeZerospinErrorJson(props: {
|
|
53
|
-
cause: unknown
|
|
54
|
-
code: string
|
|
55
|
-
extra: unknown
|
|
56
|
-
message: string
|
|
57
|
-
status: null | number
|
|
61
|
+
cause: unknown;
|
|
62
|
+
code: string;
|
|
63
|
+
extra: unknown;
|
|
64
|
+
message: string;
|
|
65
|
+
status: null | number;
|
|
58
66
|
}): IAnyErrorJson {
|
|
59
|
-
return Brand.nominal<IAnyErrorJson>()(props)
|
|
67
|
+
return Brand.nominal<IAnyErrorJson>()(props);
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
serialize(
|
|
63
71
|
// You can omit anything BUT id, code
|
|
64
|
-
omit: Exclude<
|
|
65
|
-
keyof IAnyErrorJson,
|
|
66
|
-
RequiredKeysOf<IAnyErrorJson>
|
|
67
|
-
>[] = []
|
|
72
|
+
omit: Exclude<keyof IAnyErrorJson, RequiredKeysOf<IAnyErrorJson>>[] = []
|
|
68
73
|
): IAnyErrorJson {
|
|
69
74
|
const json = ZerospinError.makeZerospinErrorJson({
|
|
70
75
|
cause: this.cause,
|
|
@@ -72,12 +77,12 @@ export class ZerospinError<
|
|
|
72
77
|
extra: this.extra,
|
|
73
78
|
message: this.message,
|
|
74
79
|
status: this.status,
|
|
75
|
-
})
|
|
80
|
+
});
|
|
76
81
|
|
|
77
82
|
for (const key of omit) {
|
|
78
|
-
delete json[key]
|
|
83
|
+
delete json[key];
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
return json
|
|
86
|
+
return json;
|
|
82
87
|
}
|
|
83
88
|
}
|
package/src/index.ts
CHANGED
package/tsconfig.json
CHANGED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
clean: true,
|
|
5
|
+
dts: true, // Declarations via tsup; typecheck script still separate
|
|
6
|
+
entry: ['src/index.ts'],
|
|
7
|
+
format: ['esm'],
|
|
8
|
+
minify: process.env.NODE_ENV === 'production',
|
|
9
|
+
outDir: 'dist',
|
|
10
|
+
sourcemap: false,
|
|
11
|
+
splitting: false,
|
|
12
|
+
});
|
|
13
|
+
|
package/dist/ZerospinError.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Brand, Data } from 'effect';
|
|
2
|
-
import { isObject } from 'effect/Predicate';
|
|
3
|
-
export class ZerospinError extends Data.TaggedError('ZerospinError') {
|
|
4
|
-
message;
|
|
5
|
-
constructor(props) {
|
|
6
|
-
if (typeof props === 'string') {
|
|
7
|
-
super({
|
|
8
|
-
code: props,
|
|
9
|
-
extra: null,
|
|
10
|
-
message: props,
|
|
11
|
-
status: null,
|
|
12
|
-
});
|
|
13
|
-
this.message = props;
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
super({
|
|
17
|
-
status: null,
|
|
18
|
-
...props,
|
|
19
|
-
});
|
|
20
|
-
this.message = props.message ?? props.code;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
static isZerospinError(data) {
|
|
24
|
-
return isObject(data) && '_tag' in data && data._tag === 'ZerospinError';
|
|
25
|
-
}
|
|
26
|
-
static makeZerospinErrorJson(props) {
|
|
27
|
-
return Brand.nominal()(props);
|
|
28
|
-
}
|
|
29
|
-
serialize(
|
|
30
|
-
// You can omit anything BUT id, code
|
|
31
|
-
omit = []) {
|
|
32
|
-
const json = ZerospinError.makeZerospinErrorJson({
|
|
33
|
-
cause: this.cause,
|
|
34
|
-
code: this.code,
|
|
35
|
-
extra: this.extra,
|
|
36
|
-
message: this.message,
|
|
37
|
-
status: this.status,
|
|
38
|
-
});
|
|
39
|
-
for (const key of omit) {
|
|
40
|
-
delete json[key];
|
|
41
|
-
}
|
|
42
|
-
return json;
|
|
43
|
-
}
|
|
44
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/src/types.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Brand } from 'effect';
|
|
2
|
-
|
|
3
|
-
import type { ZerospinError } from './ZerospinError';
|
|
4
|
-
|
|
5
|
-
export type IAnyErrorJson<ERROR extends IAnyError = IAnyError> =
|
|
6
|
-
Brand.Brand<'ZerospinErrorJson'> & {
|
|
7
|
-
code: ERROR['code'];
|
|
8
|
-
extra: ERROR['extra'];
|
|
9
|
-
message: string;
|
|
10
|
-
status: null | number;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type IAnyError = ZerospinError<string>;
|