@donkeylabs/server 0.1.0 → 0.1.2
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/cli/commands/init.ts +201 -12
- package/cli/donkeylabs +6 -0
- package/context.d.ts +17 -0
- package/docs/api-client.md +520 -0
- package/docs/cache.md +437 -0
- package/docs/cli.md +353 -0
- package/docs/core-services.md +338 -0
- package/docs/cron.md +465 -0
- package/docs/errors.md +303 -0
- package/docs/events.md +460 -0
- package/docs/handlers.md +549 -0
- package/docs/jobs.md +556 -0
- package/docs/logger.md +316 -0
- package/docs/middleware.md +682 -0
- package/docs/plugins.md +524 -0
- package/docs/project-structure.md +493 -0
- package/docs/rate-limiter.md +525 -0
- package/docs/router.md +566 -0
- package/docs/sse.md +542 -0
- package/docs/svelte-frontend.md +324 -0
- package/package.json +12 -9
- package/registry.d.ts +11 -0
- package/src/index.ts +1 -1
- package/src/server.ts +1 -0
package/docs/errors.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Error System
|
|
2
|
+
|
|
3
|
+
The error system provides throwable HTTP errors that are automatically caught by the server and converted to proper HTTP responses with status codes and JSON error bodies.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// In any route handler
|
|
9
|
+
const router = createRouter("users")
|
|
10
|
+
.route("get").typed({
|
|
11
|
+
input: z.object({ id: z.number() }),
|
|
12
|
+
output: z.object({ name: z.string() }),
|
|
13
|
+
handle: async (input, ctx) => {
|
|
14
|
+
const user = await ctx.db.selectFrom("users")
|
|
15
|
+
.where("id", "=", input.id)
|
|
16
|
+
.selectAll()
|
|
17
|
+
.executeTakeFirst();
|
|
18
|
+
|
|
19
|
+
if (!user) {
|
|
20
|
+
// Throws 404 with JSON body: { error: "NOT_FOUND", message: "User not found" }
|
|
21
|
+
throw ctx.errors.NotFound("User not found", { userId: input.id });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return user;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Available Errors
|
|
30
|
+
|
|
31
|
+
All standard HTTP errors are available via `ctx.errors`:
|
|
32
|
+
|
|
33
|
+
| Method | Status | Code | Description |
|
|
34
|
+
|--------|--------|------|-------------|
|
|
35
|
+
| `BadRequest()` | 400 | BAD_REQUEST | Invalid request data |
|
|
36
|
+
| `Unauthorized()` | 401 | UNAUTHORIZED | Authentication required |
|
|
37
|
+
| `Forbidden()` | 403 | FORBIDDEN | Not allowed to access |
|
|
38
|
+
| `NotFound()` | 404 | NOT_FOUND | Resource not found |
|
|
39
|
+
| `MethodNotAllowed()` | 405 | METHOD_NOT_ALLOWED | HTTP method not supported |
|
|
40
|
+
| `Conflict()` | 409 | CONFLICT | Resource conflict |
|
|
41
|
+
| `Gone()` | 410 | GONE | Resource no longer available |
|
|
42
|
+
| `UnprocessableEntity()` | 422 | UNPROCESSABLE_ENTITY | Validation failed |
|
|
43
|
+
| `TooManyRequests()` | 429 | TOO_MANY_REQUESTS | Rate limited |
|
|
44
|
+
| `InternalServer()` | 500 | INTERNAL_SERVER_ERROR | Server error |
|
|
45
|
+
| `NotImplemented()` | 501 | NOT_IMPLEMENTED | Feature not implemented |
|
|
46
|
+
| `BadGateway()` | 502 | BAD_GATEWAY | Upstream error |
|
|
47
|
+
| `ServiceUnavailable()` | 503 | SERVICE_UNAVAILABLE | Service down |
|
|
48
|
+
| `GatewayTimeout()` | 504 | GATEWAY_TIMEOUT | Upstream timeout |
|
|
49
|
+
|
|
50
|
+
## Error Response Format
|
|
51
|
+
|
|
52
|
+
All errors are returned as JSON with consistent structure:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"error": "NOT_FOUND",
|
|
57
|
+
"message": "User not found",
|
|
58
|
+
"details": { "userId": 123 }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `details` field is optional and only included if provided.
|
|
63
|
+
|
|
64
|
+
## Error Factory Signature
|
|
65
|
+
|
|
66
|
+
Each error factory has the same signature:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
ctx.errors.NotFound(
|
|
70
|
+
message?: string, // Custom message (uses default if omitted)
|
|
71
|
+
details?: Record<string, any>, // Additional context
|
|
72
|
+
cause?: Error // Original error that caused this
|
|
73
|
+
): HttpError
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Custom Errors
|
|
77
|
+
|
|
78
|
+
### Creating Custom Errors at Runtime
|
|
79
|
+
|
|
80
|
+
Use `ctx.errors.custom()` for one-off custom errors:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
throw ctx.errors.custom(418, "IM_A_TEAPOT", "I'm a teapot");
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Registering Custom Errors
|
|
87
|
+
|
|
88
|
+
Register reusable custom errors that appear on `ctx.errors`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// During server setup
|
|
92
|
+
const server = new AppServer({ db, port: 3000 });
|
|
93
|
+
|
|
94
|
+
// Access errors service
|
|
95
|
+
server.getCore().errors.register("PaymentRequired", {
|
|
96
|
+
status: 402,
|
|
97
|
+
code: "PAYMENT_REQUIRED",
|
|
98
|
+
defaultMessage: "Payment is required",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Now available everywhere
|
|
102
|
+
throw ctx.errors.PaymentRequired("Subscription expired");
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Plugin Custom Errors
|
|
106
|
+
|
|
107
|
+
Plugins can define custom errors that are automatically registered:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
export const paymentPlugin = createPlugin.define({
|
|
111
|
+
name: "payment",
|
|
112
|
+
customErrors: {
|
|
113
|
+
PaymentFailed: {
|
|
114
|
+
status: 402,
|
|
115
|
+
code: "PAYMENT_FAILED",
|
|
116
|
+
defaultMessage: "Payment processing failed",
|
|
117
|
+
},
|
|
118
|
+
InsufficientFunds: {
|
|
119
|
+
status: 402,
|
|
120
|
+
code: "INSUFFICIENT_FUNDS",
|
|
121
|
+
defaultMessage: "Insufficient funds",
|
|
122
|
+
},
|
|
123
|
+
CardDeclined: {
|
|
124
|
+
status: 402,
|
|
125
|
+
code: "CARD_DECLINED",
|
|
126
|
+
defaultMessage: "Card was declined",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
service: async (ctx) => ({
|
|
130
|
+
charge: async (amount: number) => {
|
|
131
|
+
if (amount > 10000) {
|
|
132
|
+
throw ctx.core.errors.InsufficientFunds("Maximum charge amount exceeded");
|
|
133
|
+
}
|
|
134
|
+
// ...
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
After plugin initialization, these errors are available on `ctx.errors`:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// In any route handler
|
|
144
|
+
throw ctx.errors.CardDeclined("Please try a different card");
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Type Augmentation for Custom Errors
|
|
148
|
+
|
|
149
|
+
For TypeScript autocomplete on custom errors, augment the `ErrorFactories` interface:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// In your plugin or types file
|
|
153
|
+
declare module "../core/errors" {
|
|
154
|
+
interface ErrorFactories {
|
|
155
|
+
PaymentFailed: ErrorFactory;
|
|
156
|
+
InsufficientFunds: ErrorFactory;
|
|
157
|
+
CardDeclined: ErrorFactory;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Validation Errors
|
|
163
|
+
|
|
164
|
+
For Zod validation failures, use `createValidationError`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { createValidationError } from "./core/errors";
|
|
168
|
+
|
|
169
|
+
// Convert Zod errors to HTTP error
|
|
170
|
+
const result = schema.safeParse(data);
|
|
171
|
+
if (!result.success) {
|
|
172
|
+
throw createValidationError(result.error.issues);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Response format:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"error": "BAD_REQUEST",
|
|
181
|
+
"message": "Validation Failed",
|
|
182
|
+
"details": {
|
|
183
|
+
"issues": [
|
|
184
|
+
{ "path": ["email"], "message": "Invalid email" },
|
|
185
|
+
{ "path": ["password"], "message": "Too short" }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Client-Side Error Handling
|
|
192
|
+
|
|
193
|
+
The API client provides typed error handling:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { createApiClient, ApiError, ValidationError, ErrorCodes } from "./client";
|
|
197
|
+
|
|
198
|
+
const api = createApiClient("http://localhost:3000");
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await api.users.get({ id: 999 });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof ValidationError) {
|
|
204
|
+
// Handle validation errors with field details
|
|
205
|
+
console.log(error.getFieldErrors("email"));
|
|
206
|
+
console.log(error.hasFieldError("password"));
|
|
207
|
+
} else if (error instanceof ApiError) {
|
|
208
|
+
// Handle other API errors
|
|
209
|
+
if (error.is(ErrorCodes.NOT_FOUND)) {
|
|
210
|
+
console.log("User not found");
|
|
211
|
+
} else if (error.is(ErrorCodes.UNAUTHORIZED)) {
|
|
212
|
+
// Redirect to login
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Access error properties
|
|
216
|
+
console.log(error.status); // 404
|
|
217
|
+
console.log(error.code); // "NOT_FOUND"
|
|
218
|
+
console.log(error.message); // "User not found"
|
|
219
|
+
console.log(error.details); // { userId: 999 }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Error Checking Utility
|
|
225
|
+
|
|
226
|
+
Check if any error is an HttpError:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
try {
|
|
230
|
+
await someOperation();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (ctx.errors.isHttpError(error)) {
|
|
233
|
+
// Safe to access error.status, error.code, etc.
|
|
234
|
+
console.log(error.status, error.code);
|
|
235
|
+
} else {
|
|
236
|
+
// Regular JavaScript error
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Best Practices
|
|
243
|
+
|
|
244
|
+
### Use Specific Error Types
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// Good - specific error type
|
|
248
|
+
throw ctx.errors.NotFound("User not found");
|
|
249
|
+
|
|
250
|
+
// Avoid - generic error
|
|
251
|
+
throw ctx.errors.BadRequest("User not found");
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Include Helpful Details
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// Good - includes context for debugging
|
|
258
|
+
throw ctx.errors.NotFound("User not found", {
|
|
259
|
+
userId: input.id,
|
|
260
|
+
searchedIn: "users",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Less helpful
|
|
264
|
+
throw ctx.errors.NotFound("Not found");
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Chain Errors for Debugging
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
try {
|
|
271
|
+
await externalService.call();
|
|
272
|
+
} catch (e) {
|
|
273
|
+
throw ctx.errors.BadGateway(
|
|
274
|
+
"External service failed",
|
|
275
|
+
{ service: "payment-gateway" },
|
|
276
|
+
e as Error // Preserve original error
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Handle in Middleware
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
export const errorLoggingMiddleware = createMiddleware({
|
|
285
|
+
name: "errorLogger",
|
|
286
|
+
execute: async (req, ctx, next) => {
|
|
287
|
+
try {
|
|
288
|
+
return await next();
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (ctx.core.errors.isHttpError(error)) {
|
|
291
|
+
ctx.core.logger.warn("HTTP error", {
|
|
292
|
+
status: error.status,
|
|
293
|
+
code: error.code,
|
|
294
|
+
message: error.message,
|
|
295
|
+
});
|
|
296
|
+
} else {
|
|
297
|
+
ctx.core.logger.error("Unhandled error", { error });
|
|
298
|
+
}
|
|
299
|
+
throw error; // Re-throw for server to handle
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
```
|