@donkeylabs/server 0.1.3 → 0.1.4

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 (55) hide show
  1. package/examples/starter/node_modules/@donkeylabs/server/README.md +15 -0
  2. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/generate.ts +461 -0
  3. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/init.ts +476 -0
  4. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/interactive.ts +223 -0
  5. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/plugin.ts +192 -0
  6. package/examples/starter/node_modules/@donkeylabs/server/cli/donkeylabs +106 -0
  7. package/examples/starter/node_modules/@donkeylabs/server/cli/index.ts +100 -0
  8. package/examples/starter/node_modules/@donkeylabs/server/context.d.ts +17 -0
  9. package/examples/starter/node_modules/@donkeylabs/server/docs/api-client.md +520 -0
  10. package/examples/starter/node_modules/@donkeylabs/server/docs/cache.md +437 -0
  11. package/examples/starter/node_modules/@donkeylabs/server/docs/cli.md +353 -0
  12. package/examples/starter/node_modules/@donkeylabs/server/docs/core-services.md +338 -0
  13. package/examples/starter/node_modules/@donkeylabs/server/docs/cron.md +465 -0
  14. package/examples/starter/node_modules/@donkeylabs/server/docs/errors.md +303 -0
  15. package/examples/starter/node_modules/@donkeylabs/server/docs/events.md +460 -0
  16. package/examples/starter/node_modules/@donkeylabs/server/docs/handlers.md +549 -0
  17. package/examples/starter/node_modules/@donkeylabs/server/docs/jobs.md +556 -0
  18. package/examples/starter/node_modules/@donkeylabs/server/docs/logger.md +316 -0
  19. package/examples/starter/node_modules/@donkeylabs/server/docs/middleware.md +682 -0
  20. package/examples/starter/node_modules/@donkeylabs/server/docs/plugins.md +524 -0
  21. package/examples/starter/node_modules/@donkeylabs/server/docs/project-structure.md +493 -0
  22. package/examples/starter/node_modules/@donkeylabs/server/docs/rate-limiter.md +525 -0
  23. package/examples/starter/node_modules/@donkeylabs/server/docs/router.md +566 -0
  24. package/examples/starter/node_modules/@donkeylabs/server/docs/sse.md +542 -0
  25. package/examples/starter/node_modules/@donkeylabs/server/docs/svelte-frontend.md +324 -0
  26. package/examples/starter/node_modules/@donkeylabs/server/mcp/donkeylabs-mcp +3238 -0
  27. package/examples/starter/node_modules/@donkeylabs/server/mcp/server.ts +3238 -0
  28. package/examples/starter/node_modules/@donkeylabs/server/package.json +77 -0
  29. package/examples/starter/node_modules/@donkeylabs/server/registry.d.ts +11 -0
  30. package/examples/starter/node_modules/@donkeylabs/server/src/client/base.ts +481 -0
  31. package/examples/starter/node_modules/@donkeylabs/server/src/client/index.ts +150 -0
  32. package/examples/starter/node_modules/@donkeylabs/server/src/core/cache.ts +183 -0
  33. package/examples/starter/node_modules/@donkeylabs/server/src/core/cron.ts +255 -0
  34. package/examples/starter/node_modules/@donkeylabs/server/src/core/errors.ts +320 -0
  35. package/examples/starter/node_modules/@donkeylabs/server/src/core/events.ts +163 -0
  36. package/examples/starter/node_modules/@donkeylabs/server/src/core/index.ts +94 -0
  37. package/examples/starter/node_modules/@donkeylabs/server/src/core/jobs.ts +334 -0
  38. package/examples/starter/node_modules/@donkeylabs/server/src/core/logger.ts +131 -0
  39. package/examples/starter/node_modules/@donkeylabs/server/src/core/rate-limiter.ts +193 -0
  40. package/examples/starter/node_modules/@donkeylabs/server/src/core/sse.ts +210 -0
  41. package/examples/starter/node_modules/@donkeylabs/server/src/core.ts +428 -0
  42. package/examples/starter/node_modules/@donkeylabs/server/src/handlers.ts +87 -0
  43. package/examples/starter/node_modules/@donkeylabs/server/src/harness.ts +70 -0
  44. package/examples/starter/node_modules/@donkeylabs/server/src/index.ts +38 -0
  45. package/examples/starter/node_modules/@donkeylabs/server/src/middleware.ts +34 -0
  46. package/examples/starter/node_modules/@donkeylabs/server/src/registry.ts +13 -0
  47. package/examples/starter/node_modules/@donkeylabs/server/src/router.ts +155 -0
  48. package/examples/starter/node_modules/@donkeylabs/server/src/server.ts +234 -0
  49. package/examples/starter/node_modules/@donkeylabs/server/templates/init/donkeylabs.config.ts.template +14 -0
  50. package/examples/starter/node_modules/@donkeylabs/server/templates/init/index.ts.template +41 -0
  51. package/examples/starter/node_modules/@donkeylabs/server/templates/plugin/index.ts.template +25 -0
  52. package/examples/starter/src/routes/health/ping/models/model.ts +11 -7
  53. package/package.json +3 -3
  54. package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-native-jsx.d.ts +0 -32
  55. package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-shims-v4.d.ts +0 -290
@@ -0,0 +1,566 @@
1
+ # Router & Routes
2
+
3
+ Fluent API for defining type-safe routes with handler selection and middleware chaining.
4
+
5
+ ## Quick Start
6
+
7
+ ```ts
8
+ import { createRouter } from "./router";
9
+ import { z } from "zod";
10
+
11
+ const router = createRouter("api")
12
+ .route("hello").typed({
13
+ input: z.object({ name: z.string() }),
14
+ handle: async (input, ctx) => {
15
+ return { message: `Hello, ${input.name}!` };
16
+ }
17
+ });
18
+ ```
19
+
20
+ ---
21
+
22
+ ## API Reference
23
+
24
+ ### createRouter
25
+
26
+ Create a new router with optional prefix:
27
+
28
+ ```ts
29
+ const router = createRouter("api"); // Routes prefixed with "api."
30
+ const router = createRouter(); // No prefix
31
+ ```
32
+
33
+ ### Router Methods
34
+
35
+ | Method | Description |
36
+ |--------|-------------|
37
+ | `route(name)` | Start defining a route, returns RouteBuilder |
38
+ | `middleware` | Start middleware chain, returns MiddlewareBuilder |
39
+ | `getRoutes()` | Get all registered route definitions |
40
+
41
+ ### RouteBuilder Methods
42
+
43
+ After calling `router.route("name")`, you get a RouteBuilder with handler methods:
44
+
45
+ | Method | Description |
46
+ |--------|-------------|
47
+ | `.typed(config)` | JSON-RPC style handler (default) |
48
+ | `.raw(config)` | Full Request/Response control |
49
+ | `.<custom>(config)` | Custom handlers from plugins |
50
+
51
+ ### MiddlewareBuilder Methods
52
+
53
+ After calling `router.middleware`, chain middleware then define routes:
54
+
55
+ ```ts
56
+ router.middleware
57
+ .auth({ required: true })
58
+ .rateLimit({ limit: 100, window: "1m" })
59
+ .route("protected").typed({ ... });
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Route Naming
65
+
66
+ Routes are named as `prefix.name`:
67
+
68
+ ```ts
69
+ const router = createRouter("users");
70
+
71
+ router.route("list"); // Route name: "users.list"
72
+ router.route("get"); // Route name: "users.get"
73
+ router.route("create"); // Route name: "users.create"
74
+ ```
75
+
76
+ **HTTP Requests:**
77
+ ```sh
78
+ POST /users.list # Calls users.list handler
79
+ POST /users.get # Calls users.get handler
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Handler Types
85
+
86
+ ### Typed Handler (Default)
87
+
88
+ JSON-RPC style with automatic validation:
89
+
90
+ ```ts
91
+ router.route("greet").typed({
92
+ // Optional: Zod schema for input validation
93
+ input: z.object({
94
+ name: z.string(),
95
+ age: z.number().optional(),
96
+ }),
97
+
98
+ // Optional: Zod schema for output validation
99
+ output: z.object({
100
+ message: z.string(),
101
+ }),
102
+
103
+ // Required: Handler function
104
+ handle: async (input, ctx) => {
105
+ return { message: `Hello, ${input.name}!` };
106
+ },
107
+ });
108
+ ```
109
+
110
+ **Behavior:**
111
+ - POST only (returns 405 for other methods)
112
+ - Parses JSON body automatically
113
+ - Validates input against schema (returns 400 on failure)
114
+ - Validates output against schema
115
+ - Returns JSON response
116
+
117
+ ### Raw Handler
118
+
119
+ Full control over Request/Response:
120
+
121
+ ```ts
122
+ router.route("download").raw({
123
+ handle: async (req, ctx) => {
124
+ const file = await Bun.file("data.csv").text();
125
+ return new Response(file, {
126
+ headers: { "Content-Type": "text/csv" },
127
+ });
128
+ },
129
+ });
130
+ ```
131
+
132
+ **Use cases:**
133
+ - File uploads/downloads
134
+ - Streaming responses
135
+ - SSE endpoints
136
+ - Custom content types
137
+ - WebSocket upgrades
138
+
139
+ ### Custom Handlers
140
+
141
+ Plugins can register custom handlers:
142
+
143
+ ```ts
144
+ // Plugin registers "echo" handler
145
+ router.route("test").echo({
146
+ handle: async (body, ctx) => {
147
+ return { echo: body };
148
+ },
149
+ });
150
+ ```
151
+
152
+ See [Handlers Documentation](handlers.md) for creating custom handlers.
153
+
154
+ ---
155
+
156
+ ## Server Context
157
+
158
+ Every handler receives `ServerContext`:
159
+
160
+ ```ts
161
+ router.route("example").typed({
162
+ handle: async (input, ctx) => {
163
+ // Database (Kysely)
164
+ const users = await ctx.db.selectFrom("users").selectAll().execute();
165
+
166
+ // Plugin services
167
+ const data = await ctx.plugins.myPlugin.getData();
168
+
169
+ // Core services
170
+ ctx.core.logger.info("Processing request", { input });
171
+ const cached = await ctx.core.cache.get("key");
172
+
173
+ // Request info
174
+ console.log(ctx.ip); // Client IP
175
+ console.log(ctx.requestId); // Unique request ID
176
+ console.log(ctx.user); // Set by auth middleware
177
+
178
+ return { users };
179
+ },
180
+ });
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Middleware
186
+
187
+ Apply middleware before routes:
188
+
189
+ ### Single Middleware
190
+
191
+ ```ts
192
+ router.middleware
193
+ .auth({ required: true })
194
+ .route("protected").typed({
195
+ handle: async (input, ctx) => {
196
+ // ctx.user is guaranteed by auth middleware
197
+ return { userId: ctx.user.id };
198
+ },
199
+ });
200
+ ```
201
+
202
+ ### Chained Middleware
203
+
204
+ ```ts
205
+ router.middleware
206
+ .cors({ origin: "*" })
207
+ .auth({ required: true })
208
+ .rateLimit({ limit: 100, window: "1m" })
209
+ .route("api").typed({
210
+ handle: async (input, ctx) => {
211
+ // All middleware applied
212
+ },
213
+ });
214
+ ```
215
+
216
+ ### Middleware for Multiple Routes
217
+
218
+ ```ts
219
+ const protectedRoutes = router.middleware
220
+ .auth({ required: true })
221
+ .rateLimit({ limit: 1000, window: "1h" });
222
+
223
+ protectedRoutes.route("profile").typed({ ... });
224
+ protectedRoutes.route("settings").typed({ ... });
225
+ protectedRoutes.route("orders").typed({ ... });
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Input Validation
231
+
232
+ Use Zod schemas for automatic validation:
233
+
234
+ ```ts
235
+ import { z } from "zod";
236
+
237
+ const CreateUserInput = z.object({
238
+ email: z.string().email(),
239
+ name: z.string().min(1).max(100),
240
+ age: z.number().int().positive().optional(),
241
+ role: z.enum(["user", "admin"]).default("user"),
242
+ });
243
+
244
+ router.route("createUser").typed({
245
+ input: CreateUserInput,
246
+ handle: async (input, ctx) => {
247
+ // input is typed and validated
248
+ // input.email: string
249
+ // input.name: string
250
+ // input.age: number | undefined
251
+ // input.role: "user" | "admin"
252
+
253
+ const user = await ctx.db.insertInto("users")
254
+ .values(input)
255
+ .returningAll()
256
+ .executeTakeFirstOrThrow();
257
+
258
+ return user;
259
+ },
260
+ });
261
+ ```
262
+
263
+ **Validation Errors:**
264
+
265
+ ```json
266
+ {
267
+ "error": "Validation Failed",
268
+ "details": [
269
+ {
270
+ "path": ["email"],
271
+ "message": "Invalid email"
272
+ }
273
+ ]
274
+ }
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Output Validation
280
+
281
+ Validate and type your responses:
282
+
283
+ ```ts
284
+ const UserResponse = z.object({
285
+ id: z.number(),
286
+ email: z.string(),
287
+ createdAt: z.string(),
288
+ });
289
+
290
+ router.route("getUser").typed({
291
+ input: z.object({ id: z.number() }),
292
+ output: UserResponse,
293
+ handle: async (input, ctx) => {
294
+ const user = await ctx.db.selectFrom("users")
295
+ .selectAll()
296
+ .where("id", "=", input.id)
297
+ .executeTakeFirstOrThrow();
298
+
299
+ // Return type is validated against UserResponse
300
+ return user;
301
+ },
302
+ });
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Real-World Examples
308
+
309
+ ### CRUD Operations
310
+
311
+ ```ts
312
+ const router = createRouter("users");
313
+
314
+ // List users
315
+ router.route("list").typed({
316
+ input: z.object({
317
+ page: z.number().default(1),
318
+ limit: z.number().default(20),
319
+ }),
320
+ handle: async (input, ctx) => {
321
+ const offset = (input.page - 1) * input.limit;
322
+
323
+ const users = await ctx.db.selectFrom("users")
324
+ .selectAll()
325
+ .limit(input.limit)
326
+ .offset(offset)
327
+ .execute();
328
+
329
+ return { users, page: input.page };
330
+ },
331
+ });
332
+
333
+ // Get single user
334
+ router.route("get").typed({
335
+ input: z.object({ id: z.number() }),
336
+ handle: async (input, ctx) => {
337
+ const user = await ctx.db.selectFrom("users")
338
+ .selectAll()
339
+ .where("id", "=", input.id)
340
+ .executeTakeFirstOrThrow();
341
+
342
+ return user;
343
+ },
344
+ });
345
+
346
+ // Create user
347
+ router.middleware
348
+ .auth({ required: true, role: "admin" })
349
+ .route("create").typed({
350
+ input: z.object({
351
+ email: z.string().email(),
352
+ name: z.string(),
353
+ }),
354
+ handle: async (input, ctx) => {
355
+ const user = await ctx.db.insertInto("users")
356
+ .values(input)
357
+ .returningAll()
358
+ .executeTakeFirstOrThrow();
359
+
360
+ await ctx.core.events.emit("user.created", user);
361
+
362
+ return user;
363
+ },
364
+ });
365
+
366
+ // Update user
367
+ router.middleware
368
+ .auth({ required: true })
369
+ .route("update").typed({
370
+ input: z.object({
371
+ id: z.number(),
372
+ name: z.string().optional(),
373
+ email: z.string().email().optional(),
374
+ }),
375
+ handle: async (input, ctx) => {
376
+ const { id, ...updates } = input;
377
+
378
+ const user = await ctx.db.updateTable("users")
379
+ .set(updates)
380
+ .where("id", "=", id)
381
+ .returningAll()
382
+ .executeTakeFirstOrThrow();
383
+
384
+ return user;
385
+ },
386
+ });
387
+
388
+ // Delete user
389
+ router.middleware
390
+ .auth({ required: true, role: "admin" })
391
+ .route("delete").typed({
392
+ input: z.object({ id: z.number() }),
393
+ handle: async (input, ctx) => {
394
+ await ctx.db.deleteFrom("users")
395
+ .where("id", "=", input.id)
396
+ .execute();
397
+
398
+ return { success: true };
399
+ },
400
+ });
401
+ ```
402
+
403
+ ### File Upload (Raw Handler)
404
+
405
+ ```ts
406
+ router.route("upload").raw({
407
+ handle: async (req, ctx) => {
408
+ const formData = await req.formData();
409
+ const file = formData.get("file") as File;
410
+
411
+ if (!file) {
412
+ return Response.json({ error: "No file provided" }, { status: 400 });
413
+ }
414
+
415
+ const buffer = await file.arrayBuffer();
416
+ const path = `uploads/${Date.now()}-${file.name}`;
417
+
418
+ await Bun.write(path, buffer);
419
+
420
+ return Response.json({
421
+ path,
422
+ size: file.size,
423
+ type: file.type,
424
+ });
425
+ },
426
+ });
427
+ ```
428
+
429
+ ### SSE Endpoint (Raw Handler)
430
+
431
+ ```ts
432
+ router.route("events").raw({
433
+ handle: async (req, ctx) => {
434
+ const { client, response } = ctx.core.sse.addClient();
435
+ ctx.core.sse.subscribe(client.id, `user:${ctx.user.id}`);
436
+ return response;
437
+ },
438
+ });
439
+ ```
440
+
441
+ ### Streaming Response
442
+
443
+ ```ts
444
+ router.route("stream").raw({
445
+ handle: async (req, ctx) => {
446
+ const stream = new ReadableStream({
447
+ async start(controller) {
448
+ for (let i = 0; i < 10; i++) {
449
+ controller.enqueue(`data: ${i}\n\n`);
450
+ await new Promise((r) => setTimeout(r, 100));
451
+ }
452
+ controller.close();
453
+ },
454
+ });
455
+
456
+ return new Response(stream, {
457
+ headers: { "Content-Type": "text/event-stream" },
458
+ });
459
+ },
460
+ });
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Route Registration
466
+
467
+ Register routes with the server:
468
+
469
+ ```ts
470
+ import { AppServer } from "./server";
471
+ import { userRouter } from "./routes/users";
472
+ import { orderRouter } from "./routes/orders";
473
+
474
+ const server = new AppServer({ db, port: 3000 });
475
+
476
+ // Register single router
477
+ server.use(userRouter);
478
+
479
+ // Register multiple routers
480
+ server.use(userRouter);
481
+ server.use(orderRouter);
482
+
483
+ await server.start();
484
+ ```
485
+
486
+ ---
487
+
488
+ ## Best Practices
489
+
490
+ ### 1. Organize by Domain
491
+
492
+ ```
493
+ routes/
494
+ ├── users.ts # createRouter("users")
495
+ ├── orders.ts # createRouter("orders")
496
+ ├── products.ts # createRouter("products")
497
+ └── index.ts # Export all routers
498
+ ```
499
+
500
+ ### 2. Use Descriptive Route Names
501
+
502
+ ```ts
503
+ // Good - clear action
504
+ router.route("list");
505
+ router.route("get");
506
+ router.route("create");
507
+ router.route("update");
508
+ router.route("delete");
509
+
510
+ // Bad - ambiguous
511
+ router.route("data");
512
+ router.route("do");
513
+ router.route("handle");
514
+ ```
515
+
516
+ ### 3. Validate All Input
517
+
518
+ ```ts
519
+ // Good - always validate
520
+ router.route("create").typed({
521
+ input: z.object({ email: z.string().email() }),
522
+ handle: async (input, ctx) => { ... },
523
+ });
524
+
525
+ // Bad - trusting client input
526
+ router.route("create").typed({
527
+ handle: async (input, ctx) => {
528
+ // input is untyped `any`
529
+ },
530
+ });
531
+ ```
532
+
533
+ ### 4. Use Middleware for Cross-Cutting Concerns
534
+
535
+ ```ts
536
+ // Good - middleware for auth
537
+ router.middleware
538
+ .auth({ required: true })
539
+ .route("protected").typed({ ... });
540
+
541
+ // Bad - auth check in every handler
542
+ router.route("protected").typed({
543
+ handle: async (input, ctx) => {
544
+ if (!ctx.user) throw new Error("Unauthorized");
545
+ // ...
546
+ },
547
+ });
548
+ ```
549
+
550
+ ### 5. Keep Handlers Focused
551
+
552
+ ```ts
553
+ // Good - focused handler, delegates to service
554
+ router.route("create").typed({
555
+ handle: async (input, ctx) => {
556
+ return ctx.plugins.users.create(input);
557
+ },
558
+ });
559
+
560
+ // Bad - business logic in handler
561
+ router.route("create").typed({
562
+ handle: async (input, ctx) => {
563
+ // 100 lines of business logic...
564
+ },
565
+ });
566
+ ```