@donkeylabs/server 0.1.1 → 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/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/handlers.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# Handlers
|
|
2
|
+
|
|
3
|
+
Request handlers define how routes process HTTP requests. Built-in handlers cover common cases, and you can create custom handlers for specialized needs.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createHandler } from "./handlers";
|
|
9
|
+
import type { ServerContext } from "./router";
|
|
10
|
+
|
|
11
|
+
// Define handler function signature
|
|
12
|
+
type MyFn = (data: MyInput, ctx: ServerContext) => Promise<MyOutput>;
|
|
13
|
+
|
|
14
|
+
// Create custom handler
|
|
15
|
+
export const MyHandler = createHandler<MyFn>(async (req, def, handle, ctx) => {
|
|
16
|
+
const data = await req.json();
|
|
17
|
+
const result = await handle(data, ctx);
|
|
18
|
+
return Response.json(result);
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Built-in Handlers
|
|
25
|
+
|
|
26
|
+
### TypedHandler (Default)
|
|
27
|
+
|
|
28
|
+
JSON-RPC style handler with automatic validation:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
router.route("greet").typed({
|
|
32
|
+
input: z.object({ name: z.string() }),
|
|
33
|
+
output: z.object({ message: z.string() }),
|
|
34
|
+
handle: async (input, ctx) => {
|
|
35
|
+
return { message: `Hello, ${input.name}!` };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Behavior:**
|
|
41
|
+
- Accepts POST requests only (405 for others)
|
|
42
|
+
- Parses JSON body
|
|
43
|
+
- Validates input with Zod schema (if provided)
|
|
44
|
+
- Calls your handler with parsed input
|
|
45
|
+
- Validates output with Zod schema (if provided)
|
|
46
|
+
- Returns JSON response
|
|
47
|
+
|
|
48
|
+
**Error Responses:**
|
|
49
|
+
|
|
50
|
+
| Status | Condition |
|
|
51
|
+
|--------|-----------|
|
|
52
|
+
| 405 | Non-POST request |
|
|
53
|
+
| 400 | Invalid JSON body |
|
|
54
|
+
| 400 | Input validation failed |
|
|
55
|
+
| 500 | Handler threw error |
|
|
56
|
+
|
|
57
|
+
### RawHandler
|
|
58
|
+
|
|
59
|
+
Full control over Request and Response:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
router.route("upload").raw({
|
|
63
|
+
handle: async (req, ctx) => {
|
|
64
|
+
if (req.method !== "POST") {
|
|
65
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const formData = await req.formData();
|
|
69
|
+
const file = formData.get("file") as File;
|
|
70
|
+
|
|
71
|
+
// Process file...
|
|
72
|
+
|
|
73
|
+
return Response.json({ success: true });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Use cases:**
|
|
79
|
+
- File uploads/downloads
|
|
80
|
+
- Streaming responses
|
|
81
|
+
- Server-Sent Events
|
|
82
|
+
- Custom content types
|
|
83
|
+
- WebSocket upgrades
|
|
84
|
+
- Non-JSON APIs
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## API Reference
|
|
89
|
+
|
|
90
|
+
### HandlerRuntime Interface
|
|
91
|
+
|
|
92
|
+
All handlers implement this interface:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
interface HandlerRuntime<Fn extends Function = Function> {
|
|
96
|
+
execute(
|
|
97
|
+
req: Request,
|
|
98
|
+
def: RouteDefinition,
|
|
99
|
+
userHandle: Fn,
|
|
100
|
+
ctx: ServerContext
|
|
101
|
+
): Promise<Response>;
|
|
102
|
+
|
|
103
|
+
readonly __signature: Fn; // Required for type inference
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### createHandler Factory
|
|
108
|
+
|
|
109
|
+
Create custom handlers without manual phantom types:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
function createHandler<Fn extends Function>(
|
|
113
|
+
execute: (
|
|
114
|
+
req: Request,
|
|
115
|
+
def: RouteDefinition,
|
|
116
|
+
handle: Fn,
|
|
117
|
+
ctx: ServerContext
|
|
118
|
+
) => Promise<Response>
|
|
119
|
+
): HandlerRuntime<Fn>;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Creating Custom Handlers
|
|
125
|
+
|
|
126
|
+
### Step 1: Define Function Signature
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// The signature your handler users will implement
|
|
130
|
+
type EchoFn = (body: any, ctx: ServerContext) => Promise<{ echo: any }>;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Step 2: Create Handler
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { createHandler } from "./handlers";
|
|
137
|
+
|
|
138
|
+
export const EchoHandler = createHandler<EchoFn>(async (req, def, handle, ctx) => {
|
|
139
|
+
// 1. Process the request
|
|
140
|
+
const body = await req.json();
|
|
141
|
+
|
|
142
|
+
// 2. Call the user's handler
|
|
143
|
+
const result = await handle(body, ctx);
|
|
144
|
+
|
|
145
|
+
// 3. Return the response
|
|
146
|
+
return Response.json(result);
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Step 3: Register in Plugin
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createPlugin } from "./core";
|
|
154
|
+
import { EchoHandler } from "./handlers/echo";
|
|
155
|
+
|
|
156
|
+
export const echoPlugin = createPlugin.define({
|
|
157
|
+
name: "echo",
|
|
158
|
+
handlers: {
|
|
159
|
+
echo: EchoHandler, // Key becomes method name
|
|
160
|
+
},
|
|
161
|
+
service: async () => ({}),
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Step 4: Regenerate Registry
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
bun run gen:registry
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Step 5: Use in Routes
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// Now available as .echo() method
|
|
175
|
+
router.route("test").echo({
|
|
176
|
+
handle: async (body, ctx) => {
|
|
177
|
+
return { echo: body };
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Custom Handler Examples
|
|
185
|
+
|
|
186
|
+
### XML Handler
|
|
187
|
+
|
|
188
|
+
Accept and return XML:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { createHandler } from "./handlers";
|
|
192
|
+
import { parseXML, buildXML } from "./utils/xml";
|
|
193
|
+
|
|
194
|
+
type XMLFn = (data: object, ctx: ServerContext) => Promise<object>;
|
|
195
|
+
|
|
196
|
+
export const XMLHandler = createHandler<XMLFn>(async (req, def, handle, ctx) => {
|
|
197
|
+
// Parse XML body
|
|
198
|
+
const xmlText = await req.text();
|
|
199
|
+
const data = parseXML(xmlText);
|
|
200
|
+
|
|
201
|
+
// Call user handler
|
|
202
|
+
const result = await handle(data, ctx);
|
|
203
|
+
|
|
204
|
+
// Return XML response
|
|
205
|
+
return new Response(buildXML(result), {
|
|
206
|
+
headers: { "Content-Type": "application/xml" },
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Form Handler
|
|
212
|
+
|
|
213
|
+
Process form submissions:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
type FormFn = (fields: Record<string, string>, ctx: ServerContext) => Promise<any>;
|
|
217
|
+
|
|
218
|
+
export const FormHandler = createHandler<FormFn>(async (req, def, handle, ctx) => {
|
|
219
|
+
const formData = await req.formData();
|
|
220
|
+
const fields: Record<string, string> = {};
|
|
221
|
+
|
|
222
|
+
for (const [key, value] of formData.entries()) {
|
|
223
|
+
if (typeof value === "string") {
|
|
224
|
+
fields[key] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = await handle(fields, ctx);
|
|
229
|
+
return Response.json(result);
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### GraphQL-Style Handler
|
|
234
|
+
|
|
235
|
+
Single endpoint with operation selection:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
type GraphQLFn = (
|
|
239
|
+
query: string,
|
|
240
|
+
variables: Record<string, any>,
|
|
241
|
+
ctx: ServerContext
|
|
242
|
+
) => Promise<{ data?: any; errors?: any[] }>;
|
|
243
|
+
|
|
244
|
+
export const GraphQLHandler = createHandler<GraphQLFn>(async (req, def, handle, ctx) => {
|
|
245
|
+
const body = await req.json();
|
|
246
|
+
const { query, variables = {} } = body;
|
|
247
|
+
|
|
248
|
+
if (!query) {
|
|
249
|
+
return Response.json({ errors: [{ message: "Query required" }] }, { status: 400 });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await handle(query, variables, ctx);
|
|
253
|
+
return Response.json(result);
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Streaming Handler
|
|
258
|
+
|
|
259
|
+
Support streaming responses:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
type StreamFn = (
|
|
263
|
+
input: any,
|
|
264
|
+
ctx: ServerContext
|
|
265
|
+
) => AsyncGenerator<string, void, unknown>;
|
|
266
|
+
|
|
267
|
+
export const StreamHandler = createHandler<StreamFn>(async (req, def, handle, ctx) => {
|
|
268
|
+
const input = await req.json();
|
|
269
|
+
|
|
270
|
+
const stream = new ReadableStream({
|
|
271
|
+
async start(controller) {
|
|
272
|
+
const encoder = new TextEncoder();
|
|
273
|
+
|
|
274
|
+
for await (const chunk of handle(input, ctx)) {
|
|
275
|
+
controller.enqueue(encoder.encode(chunk));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
controller.close();
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return new Response(stream, {
|
|
283
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Usage:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
router.route("stream").stream({
|
|
292
|
+
handle: async function* (input, ctx) {
|
|
293
|
+
for (let i = 0; i < 10; i++) {
|
|
294
|
+
yield `Chunk ${i}\n`;
|
|
295
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Batch Handler
|
|
302
|
+
|
|
303
|
+
Process multiple operations in one request:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
type BatchFn = (
|
|
307
|
+
operations: { id: string; method: string; params: any }[],
|
|
308
|
+
ctx: ServerContext
|
|
309
|
+
) => Promise<{ id: string; result?: any; error?: string }[]>;
|
|
310
|
+
|
|
311
|
+
export const BatchHandler = createHandler<BatchFn>(async (req, def, handle, ctx) => {
|
|
312
|
+
const body = await req.json();
|
|
313
|
+
|
|
314
|
+
if (!Array.isArray(body)) {
|
|
315
|
+
return Response.json({ error: "Expected array" }, { status: 400 });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const results = await handle(body, ctx);
|
|
319
|
+
return Response.json(results);
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Handler Configuration
|
|
326
|
+
|
|
327
|
+
Custom handlers can access route configuration:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// Route definition includes custom config
|
|
331
|
+
router.route("cached").typed({
|
|
332
|
+
input: z.object({ id: z.string() }),
|
|
333
|
+
cache: { ttl: 60000 }, // Custom config
|
|
334
|
+
handle: async (input, ctx) => { ... },
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Access in handler:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
export const CachedHandler = createHandler<CachedFn>(async (req, def, handle, ctx) => {
|
|
342
|
+
const cacheConfig = (def as any).cache;
|
|
343
|
+
|
|
344
|
+
if (cacheConfig) {
|
|
345
|
+
const cached = await ctx.core.cache.get(cacheKey);
|
|
346
|
+
if (cached) return Response.json(cached);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = await handle(input, ctx);
|
|
350
|
+
|
|
351
|
+
if (cacheConfig) {
|
|
352
|
+
await ctx.core.cache.set(cacheKey, result, cacheConfig.ttl);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return Response.json(result);
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Error Handling
|
|
362
|
+
|
|
363
|
+
### In Custom Handlers
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
export const SafeHandler = createHandler<SafeFn>(async (req, def, handle, ctx) => {
|
|
367
|
+
try {
|
|
368
|
+
const body = await req.json();
|
|
369
|
+
const result = await handle(body, ctx);
|
|
370
|
+
return Response.json(result);
|
|
371
|
+
} catch (error: any) {
|
|
372
|
+
ctx.core.logger.error("Handler error", { error: error.message });
|
|
373
|
+
|
|
374
|
+
if (error.name === "ValidationError") {
|
|
375
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return Response.json({ error: "Internal server error" }, { status: 500 });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Zod Validation Errors
|
|
384
|
+
|
|
385
|
+
TypedHandler returns structured validation errors:
|
|
386
|
+
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"error": "Validation Failed",
|
|
390
|
+
"details": [
|
|
391
|
+
{
|
|
392
|
+
"path": ["email"],
|
|
393
|
+
"message": "Invalid email",
|
|
394
|
+
"code": "invalid_string"
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"path": ["age"],
|
|
398
|
+
"message": "Expected number, received string",
|
|
399
|
+
"code": "invalid_type"
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Handler Resolution
|
|
408
|
+
|
|
409
|
+
The server resolves handlers at runtime:
|
|
410
|
+
|
|
411
|
+
1. Route specifies handler name (e.g., `"typed"`, `"raw"`, `"echo"`)
|
|
412
|
+
2. Server looks up handler in merged registry (built-in + plugin handlers)
|
|
413
|
+
3. Handler's `execute()` method is called with request, definition, user handle, and context
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
// In server.ts (simplified)
|
|
417
|
+
const handler = handlers[route.handler];
|
|
418
|
+
const response = await handler.execute(req, route, route.handle, ctx);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## TypeScript Integration
|
|
424
|
+
|
|
425
|
+
### Handler Type Inference
|
|
426
|
+
|
|
427
|
+
The `__signature` phantom type enables autocomplete:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
// When you type: router.route("test").echo({
|
|
431
|
+
// handle: ... <-- TypeScript knows this should be EchoFn
|
|
432
|
+
// });
|
|
433
|
+
|
|
434
|
+
interface HandlerRuntime<Fn extends Function> {
|
|
435
|
+
readonly __signature: Fn; // This enables inference
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Generating Registry
|
|
440
|
+
|
|
441
|
+
After adding handlers to a plugin, regenerate types:
|
|
442
|
+
|
|
443
|
+
```sh
|
|
444
|
+
bun run gen:registry
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
This generates `registry.d.ts` which augments `IRouteBuilder`:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
declare module "./router" {
|
|
451
|
+
interface IRouteBuilder<TRouter> {
|
|
452
|
+
echo(config: { handle: EchoFn }): TRouter;
|
|
453
|
+
// ... other handlers
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Best Practices
|
|
461
|
+
|
|
462
|
+
### 1. Keep Handlers Focused
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
// Good - single responsibility
|
|
466
|
+
export const JSONHandler = createHandler<JSONFn>(async (req, def, handle, ctx) => {
|
|
467
|
+
const body = await req.json();
|
|
468
|
+
const result = await handle(body, ctx);
|
|
469
|
+
return Response.json(result);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Bad - too many concerns
|
|
473
|
+
export const EverythingHandler = createHandler<Fn>(async (req, def, handle, ctx) => {
|
|
474
|
+
// Auth check
|
|
475
|
+
// Rate limiting
|
|
476
|
+
// Caching
|
|
477
|
+
// Logging
|
|
478
|
+
// Validation
|
|
479
|
+
// Error handling
|
|
480
|
+
// ...
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Use middleware for cross-cutting concerns.
|
|
485
|
+
|
|
486
|
+
### 2. Validate Input Early
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
export const SafeHandler = createHandler<SafeFn>(async (req, def, handle, ctx) => {
|
|
490
|
+
// Validate before calling user handler
|
|
491
|
+
let body;
|
|
492
|
+
try {
|
|
493
|
+
body = await req.json();
|
|
494
|
+
} catch {
|
|
495
|
+
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const result = await handle(body, ctx);
|
|
499
|
+
return Response.json(result);
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### 3. Use Proper Error Responses
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
// Good - proper HTTP status codes
|
|
507
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
508
|
+
return Response.json({ error: "Validation failed" }, { status: 400 });
|
|
509
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
510
|
+
|
|
511
|
+
// Bad - always 200 with error in body
|
|
512
|
+
return Response.json({ success: false, error: "Not found" });
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### 4. Document Handler Contracts
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
/**
|
|
519
|
+
* FormHandler - Processes multipart form submissions
|
|
520
|
+
*
|
|
521
|
+
* Request: multipart/form-data with fields
|
|
522
|
+
* Response: JSON
|
|
523
|
+
*
|
|
524
|
+
* Handler signature:
|
|
525
|
+
* (fields: Record<string, string>, files: File[], ctx) => Promise<any>
|
|
526
|
+
*/
|
|
527
|
+
export const FormHandler = createHandler<FormFn>(...);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### 5. Test Handlers Independently
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import { EchoHandler } from "./handlers/echo";
|
|
534
|
+
|
|
535
|
+
test("EchoHandler echoes body", async () => {
|
|
536
|
+
const req = new Request("http://test", {
|
|
537
|
+
method: "POST",
|
|
538
|
+
body: JSON.stringify({ hello: "world" }),
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const mockHandle = async (body: any) => ({ echo: body });
|
|
542
|
+
const ctx = createMockContext();
|
|
543
|
+
|
|
544
|
+
const response = await EchoHandler.execute(req, {}, mockHandle, ctx);
|
|
545
|
+
const json = await response.json();
|
|
546
|
+
|
|
547
|
+
expect(json).toEqual({ echo: { hello: "world" } });
|
|
548
|
+
});
|
|
549
|
+
```
|