@donkeylabs/server 0.3.0 → 0.4.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.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/docs/api-client.md +7 -7
  3. package/docs/cache.md +1 -74
  4. package/docs/core-services.md +4 -116
  5. package/docs/cron.md +1 -1
  6. package/docs/errors.md +2 -2
  7. package/docs/events.md +3 -98
  8. package/docs/handlers.md +13 -48
  9. package/docs/logger.md +3 -58
  10. package/docs/middleware.md +2 -2
  11. package/docs/plugins.md +13 -64
  12. package/docs/project-structure.md +4 -142
  13. package/docs/rate-limiter.md +4 -136
  14. package/docs/router.md +6 -14
  15. package/docs/sse.md +1 -99
  16. package/docs/sveltekit-adapter.md +420 -0
  17. package/package.json +8 -11
  18. package/registry.d.ts +15 -14
  19. package/src/core/cache.ts +0 -75
  20. package/src/core/cron.ts +3 -96
  21. package/src/core/errors.ts +78 -11
  22. package/src/core/events.ts +1 -47
  23. package/src/core/index.ts +0 -4
  24. package/src/core/jobs.ts +0 -112
  25. package/src/core/logger.ts +12 -79
  26. package/src/core/rate-limiter.ts +29 -108
  27. package/src/core/sse.ts +1 -84
  28. package/src/core.ts +13 -104
  29. package/src/generator/index.ts +566 -0
  30. package/src/generator/zod-to-ts.ts +114 -0
  31. package/src/handlers.ts +14 -110
  32. package/src/index.ts +30 -24
  33. package/src/middleware.ts +2 -5
  34. package/src/registry.ts +4 -0
  35. package/src/router.ts +47 -1
  36. package/src/server.ts +618 -332
  37. package/README.md +0 -254
  38. package/cli/commands/dev.ts +0 -134
  39. package/cli/commands/generate.ts +0 -605
  40. package/cli/commands/init.ts +0 -205
  41. package/cli/commands/interactive.ts +0 -417
  42. package/cli/commands/plugin.ts +0 -192
  43. package/cli/commands/route.ts +0 -195
  44. package/cli/donkeylabs +0 -2
  45. package/cli/index.ts +0 -114
  46. package/docs/svelte-frontend.md +0 -324
  47. package/docs/testing.md +0 -438
  48. package/mcp/donkeylabs-mcp +0 -3238
  49. package/mcp/server.ts +0 -3238
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 DonkeyLabs
3
+ Copyright (c) 2025 DonkeyLabs
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -9,7 +9,7 @@ Code-generated, fully-typed API client for consuming routes with TypeScript. Sup
9
9
  // bun run gen:client server.ts
10
10
 
11
11
  // Import and use
12
- import { createApiClient } from "@donkeylabs/server/client";
12
+ import { createApiClient } from "./client";
13
13
 
14
14
  const api = createApiClient({ baseUrl: "http://localhost:3000" });
15
15
 
@@ -159,14 +159,14 @@ const blob = await response.blob();
159
159
  ## Error Handling
160
160
 
161
161
  ```ts
162
- import { ApiError, ValidationError } from "@donkeylabs/server/client";
162
+ import { ApiError, ValidationError } from "./client";
163
163
 
164
164
  try {
165
165
  const user = await api.users.create({ email: "invalid" });
166
166
  } catch (error) {
167
167
  if (error instanceof ValidationError) {
168
168
  // Zod validation failed (400)
169
- console.log("Validation errors:", error.validationDetails);
169
+ console.log("Validation errors:", error.details);
170
170
  // [{ path: ["email"], message: "Invalid email" }]
171
171
  } else if (error instanceof ApiError) {
172
172
  // HTTP error
@@ -324,7 +324,7 @@ The generator merges all plugin client configs to determine defaults.
324
324
  Works out of the box with native `fetch` and `EventSource`:
325
325
 
326
326
  ```ts
327
- import { createApiClient } from "@donkeylabs/server/client";
327
+ import { createApiClient } from "./client";
328
328
  const api = createApiClient({ baseUrl: "http://localhost:3000" });
329
329
  ```
330
330
 
@@ -333,7 +333,7 @@ const api = createApiClient({ baseUrl: "http://localhost:3000" });
333
333
  Native fetch is available in Bun and Node.js 18+:
334
334
 
335
335
  ```ts
336
- import { createApiClient } from "@donkeylabs/server/client";
336
+ import { createApiClient } from "./client";
337
337
 
338
338
  const api = createApiClient({
339
339
  baseUrl: "http://localhost:3000",
@@ -355,7 +355,7 @@ api.connect();
355
355
  ## Complete Example
356
356
 
357
357
  ```ts
358
- import { createApiClient, ApiError, ValidationError } from "@donkeylabs/server/client";
358
+ import { createApiClient, ApiError, ValidationError } from "./client";
359
359
 
360
360
  // Create client
361
361
  const api = createApiClient({ baseUrl: "http://localhost:3000" });
@@ -390,7 +390,7 @@ async function main() {
390
390
 
391
391
  } catch (error) {
392
392
  if (error instanceof ValidationError) {
393
- console.error("Validation failed:", error.validationDetails);
393
+ console.error("Validation failed:", error.details);
394
394
  } else if (error instanceof ApiError) {
395
395
  console.error(`API error ${error.status}:`, error.body);
396
396
  } else {
package/docs/cache.md CHANGED
@@ -30,19 +30,6 @@ interface Cache {
30
30
  clear(): Promise<void>;
31
31
  keys(pattern?: string): Promise<string[]>;
32
32
  getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T>;
33
- namespace(prefix: string): NamespacedCache;
34
- }
35
-
36
- interface NamespacedCache {
37
- readonly prefix: string;
38
- get<T>(key: string): Promise<T | null>;
39
- set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
40
- delete(key: string): Promise<boolean>;
41
- has(key: string): Promise<boolean>;
42
- clear(): Promise<void>;
43
- keys(pattern?: string): Promise<string[]>;
44
- getOrSet<T>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T>;
45
- clearNamespace(): Promise<void>; // Clear only keys in this namespace
46
33
  }
47
34
  ```
48
35
 
@@ -57,7 +44,6 @@ interface NamespacedCache {
57
44
  | `clear()` | Remove all keys |
58
45
  | `keys(pattern?)` | List keys matching glob pattern |
59
46
  | `getOrSet(key, factory, ttl?)` | Get existing or compute and cache |
60
- | `namespace(prefix)` | Create namespaced cache with auto-prefixed keys |
61
47
 
62
48
  ---
63
49
 
@@ -185,65 +171,6 @@ const allKeys = await cache.keys();
185
171
 
186
172
  ---
187
173
 
188
- ## Namespaced Cache
189
-
190
- Create isolated cache namespaces for plugins or features:
191
-
192
- ```ts
193
- // Create namespaced caches
194
- const authCache = cache.namespace("auth");
195
- const userCache = cache.namespace("users");
196
-
197
- // Keys are automatically prefixed
198
- await authCache.set("token:123", { userId: 1 }); // Stored as "auth:token:123"
199
- await userCache.set("profile:1", { name: "Alice" }); // Stored as "users:profile:1"
200
-
201
- // Each namespace is isolated
202
- await authCache.get("token:123"); // Returns token
203
- await userCache.get("token:123"); // Returns null (different namespace)
204
- ```
205
-
206
- ### Plugin Cache (Recommended)
207
-
208
- Plugins get an auto-namespaced cache via `ctx.cache`:
209
-
210
- ```ts
211
- service: async (ctx) => {
212
- // ctx.cache is automatically namespaced with plugin name
213
- const cache = ctx.cache; // prefix: "pluginName:"
214
-
215
- return {
216
- async getUser(id: number) {
217
- return cache.getOrSet(`user:${id}`, async () => {
218
- return ctx.db.selectFrom("users").where("id", "=", id).executeTakeFirst();
219
- }, 60000);
220
- },
221
- };
222
- };
223
- ```
224
-
225
- ### Clear Namespace
226
-
227
- Clear all keys in a namespace without affecting other namespaces:
228
-
229
- ```ts
230
- const sessionCache = cache.namespace("sessions");
231
-
232
- // Store many sessions
233
- await sessionCache.set("user:1", session1);
234
- await sessionCache.set("user:2", session2);
235
- await sessionCache.set("user:3", session3);
236
-
237
- // Clear only session cache, leave other caches intact
238
- await sessionCache.clearNamespace();
239
-
240
- // Verify
241
- await sessionCache.keys(); // []
242
- await cache.keys("users:*"); // Still has user data
243
- ```
244
-
245
- ---
246
-
247
174
  ## Real-World Examples
248
175
 
249
176
  ### User Session Caching
@@ -398,7 +325,7 @@ interface CacheAdapter {
398
325
  ### Redis Adapter Example
399
326
 
400
327
  ```ts
401
- import { createCache, type CacheAdapter } from "@donkeylabs/server";
328
+ import { createCache, type CacheAdapter } from "./core/cache";
402
329
  import Redis from "ioredis";
403
330
 
404
331
  class RedisCacheAdapter implements CacheAdapter {
@@ -112,7 +112,7 @@ interface CoreServices {
112
112
  Configure services when creating the server:
113
113
 
114
114
  ```ts
115
- import { AppServer } from "@donkeylabs/server";
115
+ import { AppServer } from "./server";
116
116
 
117
117
  const server = new AppServer({
118
118
  db: database,
@@ -126,7 +126,7 @@ const server = new AppServer({
126
126
 
127
127
  cache: {
128
128
  defaultTtlMs: 300000, // 5 minutes
129
- maxSize: 1000, // LRU max items (default: 1000)
129
+ maxSize: 10000, // LRU max items
130
130
  },
131
131
 
132
132
  events: {
@@ -156,118 +156,6 @@ const server = new AppServer({
156
156
 
157
157
  ---
158
158
 
159
- ## Server Introspection
160
-
161
- Get a complete overview of your server's configuration with `server.inspect()`:
162
-
163
- ```ts
164
- const info = server.inspect();
165
-
166
- console.log(info);
167
- // {
168
- // port: 3000,
169
- // plugins: [
170
- // { name: "auth", hasMigrations: true, dependencies: [] },
171
- // { name: "notifications", hasMigrations: false, dependencies: ["auth"] },
172
- // ],
173
- // routes: [
174
- // { name: "api.auth.login", handler: "typed" },
175
- // { name: "api.users.list", handler: "typed" },
176
- // ],
177
- // handlers: ["typed", "raw", "xml"],
178
- // events: ["user.created", "user.updated", "order.paid"],
179
- // sseChannels: ["notifications", "dashboard", "user:*"],
180
- // jobs: [
181
- // { name: "sendEmail", pluginName: "notifications", description: "Send email" },
182
- // ],
183
- // cronTasks: [
184
- // { name: "daily-cleanup", pluginName: "maintenance", expression: "0 0 * * *" },
185
- // ],
186
- // rateLimitRules: [
187
- // { pattern: "auth.login", limit: 5, window: "15m" },
188
- // { pattern: "api.*", limit: 100, window: "1m" },
189
- // ],
190
- // }
191
- ```
192
-
193
- ### ServerInspection Interface
194
-
195
- ```ts
196
- interface ServerInspection {
197
- port: number;
198
- plugins: Array<{
199
- name: string;
200
- hasMigrations: boolean;
201
- dependencies: string[];
202
- }>;
203
- routes: Array<{
204
- name: string;
205
- handler: string;
206
- }>;
207
- handlers: string[];
208
- events: string[];
209
- sseChannels: string[];
210
- jobs: Array<{
211
- name: string;
212
- pluginName?: string;
213
- description?: string;
214
- }>;
215
- cronTasks: Array<{
216
- name: string;
217
- pluginName?: string;
218
- expression: string;
219
- description?: string;
220
- enabled: boolean;
221
- }>;
222
- rateLimitRules: Array<{
223
- pattern: string;
224
- limit: number;
225
- window: string | number;
226
- }>;
227
- }
228
- ```
229
-
230
- ### Use Cases
231
-
232
- **Debug Route:** Expose server info for debugging:
233
-
234
- ```ts
235
- router.route("admin.debug").typed({
236
- handle: async (input, ctx) => {
237
- // Only in development
238
- if (process.env.NODE_ENV === "production") {
239
- throw ctx.errors.NotFound();
240
- }
241
- return server.inspect();
242
- },
243
- });
244
- ```
245
-
246
- **Documentation Generation:** Generate API docs from inspection:
247
-
248
- ```ts
249
- const info = server.inspect();
250
- const markdown = generateDocs(info.routes, info.events);
251
- ```
252
-
253
- **Health Checks:** Include server stats in health endpoints:
254
-
255
- ```ts
256
- router.route("health").typed({
257
- handle: async (input, ctx) => {
258
- const info = server.inspect();
259
- return {
260
- status: "healthy",
261
- plugins: info.plugins.length,
262
- routes: info.routes.length,
263
- uptime: process.uptime(),
264
- };
265
- },
266
- });
267
- ```
268
-
269
- ---
270
-
271
159
  ## Service Lifecycle
272
160
 
273
161
  ### Startup
@@ -406,7 +294,7 @@ router.route("upload").typed({
406
294
  The test harness automatically creates all core services:
407
295
 
408
296
  ```ts
409
- import { createTestHarness } from "@donkeylabs/server/harness";
297
+ import { createTestHarness } from "./harness";
410
298
 
411
299
  const { core } = await createTestHarness(myPlugin);
412
300
 
@@ -428,7 +316,7 @@ core.cron.start();
428
316
  Each service supports custom adapters for different backends:
429
317
 
430
318
  ```ts
431
- import { createCache, type CacheAdapter } from "@donkeylabs/server";
319
+ import { createCache, type CacheAdapter } from "./core/cache";
432
320
 
433
321
  // Implement custom adapter (e.g., Redis)
434
322
  class RedisCacheAdapter implements CacheAdapter {
package/docs/cron.md CHANGED
@@ -359,7 +359,7 @@ ctx.core.cron.schedule("0 * * * *", async () => {
359
359
  ## Testing Cron Tasks
360
360
 
361
361
  ```ts
362
- import { createCron } from "@donkeylabs/server";
362
+ import { createCron } from "./core/cron";
363
363
 
364
364
  describe("Cron Tasks", () => {
365
365
  it("should execute task on trigger", async () => {
package/docs/errors.md CHANGED
@@ -164,7 +164,7 @@ declare module "../core/errors" {
164
164
  For Zod validation failures, use `createValidationError`:
165
165
 
166
166
  ```ts
167
- import { createValidationError } from "@donkeylabs/server";
167
+ import { createValidationError } from "./core/errors";
168
168
 
169
169
  // Convert Zod errors to HTTP error
170
170
  const result = schema.safeParse(data);
@@ -193,7 +193,7 @@ Response format:
193
193
  The API client provides typed error handling:
194
194
 
195
195
  ```ts
196
- import { createApiClient, ApiError, ValidationError, ErrorCodes } from "@donkeylabs/server/client";
196
+ import { createApiClient, ApiError, ValidationError, ErrorCodes } from "./client";
197
197
 
198
198
  const api = createApiClient("http://localhost:3000");
199
199
 
package/docs/events.md CHANGED
@@ -5,114 +5,23 @@ Asynchronous pub/sub event system with pattern matching, history tracking, and s
5
5
  ## Quick Start
6
6
 
7
7
  ```ts
8
- // Register events with schemas (required)
9
- ctx.core.events.register("user.created", z.object({
10
- id: z.number(),
11
- email: z.string().email(),
12
- }));
13
-
14
8
  // Subscribe to events
15
9
  ctx.core.events.on("user.created", async (user) => {
16
10
  console.log("New user:", user.email);
17
11
  });
18
12
 
19
- // Emit events (validates against schema)
13
+ // Emit events
20
14
  await ctx.core.events.emit("user.created", { id: 1, email: "alice@example.com" });
21
15
  ```
22
16
 
23
17
  ---
24
18
 
25
- ## Event Registration
26
-
27
- **Events must be registered before they can be emitted.** This ensures type safety and validates data at runtime.
28
-
29
- ### Server-Level Registration
30
-
31
- ```ts
32
- import { z } from "zod";
33
-
34
- const server = new AppServer({ db });
35
-
36
- // Register individual events
37
- server.registerEvent("user.created", z.object({
38
- id: z.number(),
39
- email: z.string().email(),
40
- }));
41
-
42
- server.registerEvent("order.paid", z.object({
43
- orderId: z.string(),
44
- amount: z.number(),
45
- currency: z.string(),
46
- }));
47
- ```
48
-
49
- ### Bulk Registration
50
-
51
- ```ts
52
- // Register multiple events at once
53
- ctx.core.events.registerMany({
54
- "user.created": z.object({ id: z.number(), email: z.string() }),
55
- "user.updated": z.object({ id: z.number(), changes: z.record(z.any()) }),
56
- "user.deleted": z.object({ id: z.number() }),
57
- });
58
- ```
59
-
60
- ### Plugin Events
61
-
62
- Plugins can declare their events in the plugin definition:
63
-
64
- ```ts
65
- export const ordersPlugin = createPlugin.define({
66
- name: "orders",
67
- events: {
68
- "order.created": z.object({ id: z.string(), total: z.number() }),
69
- "order.paid": z.object({ id: z.string(), paidAt: z.string() }),
70
- "order.shipped": z.object({ id: z.string(), trackingNumber: z.string() }),
71
- },
72
- service: async (ctx) => ({ /* ... */ }),
73
- });
74
- ```
75
-
76
- ### Validation
77
-
78
- Emitting unregistered events or invalid data throws an error:
79
-
80
- ```ts
81
- // Throws: Event 'unknown.event' is not registered
82
- await ctx.core.events.emit("unknown.event", { data: 1 });
83
-
84
- // Throws: Event 'user.created' validation failed
85
- await ctx.core.events.emit("user.created", { id: "not-a-number" });
86
- ```
87
-
88
- ### Introspection
89
-
90
- ```ts
91
- // Check if event is registered
92
- if (ctx.core.events.isRegistered("user.created")) {
93
- await ctx.core.events.emit("user.created", userData);
94
- }
95
-
96
- // List all registered events
97
- const events = ctx.core.events.listRegistered();
98
- // ["user.created", "user.updated", "order.paid", ...]
99
- ```
100
-
101
- ---
102
-
103
19
  ## API Reference
104
20
 
105
21
  ### Interface
106
22
 
107
23
  ```ts
108
24
  interface Events {
109
- // Registration (required before emitting)
110
- register<T>(event: string, schema: z.ZodType<T>): void;
111
- registerMany(schemas: Record<string, z.ZodType<any>>): void;
112
- isRegistered(event: string): boolean;
113
- listRegistered(): string[];
114
-
115
- // Pub/Sub
116
25
  emit<T = any>(event: string, data: T): Promise<void>;
117
26
  on<T = any>(event: string, handler: EventHandler<T>): Subscription;
118
27
  once<T = any>(event: string, handler: EventHandler<T>): Subscription;
@@ -135,11 +44,7 @@ interface EventRecord {
135
44
 
136
45
  | Method | Description |
137
46
  |--------|-------------|
138
- | `register(event, schema)` | Register event with Zod schema (required before emit) |
139
- | `registerMany(schemas)` | Register multiple events at once |
140
- | `isRegistered(event)` | Check if event is registered |
141
- | `listRegistered()` | List all registered event names |
142
- | `emit(event, data)` | Emit event to all subscribers (validates against schema) |
47
+ | `emit(event, data)` | Emit event to all subscribers |
143
48
  | `on(event, handler)` | Subscribe to event, returns subscription |
144
49
  | `once(event, handler)` | Subscribe to single occurrence |
145
50
  | `off(event, handler?)` | Unsubscribe handler or all handlers |
@@ -452,7 +357,7 @@ interface EventAdapter {
452
357
  ### Redis Pub/Sub Adapter
453
358
 
454
359
  ```ts
455
- import { createEvents, type EventAdapter } from "@donkeylabs/server";
360
+ import { createEvents, type EventAdapter } from "./core/events";
456
361
  import Redis from "ioredis";
457
362
 
458
363
  class RedisEventAdapter implements EventAdapter {
package/docs/handlers.md CHANGED
@@ -5,8 +5,8 @@ Request handlers define how routes process HTTP requests. Built-in handlers cove
5
5
  ## Quick Start
6
6
 
7
7
  ```ts
8
- import { createHandler } from "@donkeylabs/server";
9
- import type { ServerContext } from "@donkeylabs/server";
8
+ import { createHandler } from "./handlers";
9
+ import type { ServerContext } from "./router";
10
10
 
11
11
  // Define handler function signature
12
12
  type MyFn = (data: MyInput, ctx: ServerContext) => Promise<MyOutput>;
@@ -133,7 +133,7 @@ type EchoFn = (body: any, ctx: ServerContext) => Promise<{ echo: any }>;
133
133
  ### Step 2: Create Handler
134
134
 
135
135
  ```ts
136
- import { createHandler } from "@donkeylabs/server";
136
+ import { createHandler } from "./handlers";
137
137
 
138
138
  export const EchoHandler = createHandler<EchoFn>(async (req, def, handle, ctx) => {
139
139
  // 1. Process the request
@@ -150,7 +150,7 @@ export const EchoHandler = createHandler<EchoFn>(async (req, def, handle, ctx) =
150
150
  ### Step 3: Register in Plugin
151
151
 
152
152
  ```ts
153
- import { createPlugin } from "@donkeylabs/server";
153
+ import { createPlugin } from "./core";
154
154
  import { EchoHandler } from "./handlers/echo";
155
155
 
156
156
  export const echoPlugin = createPlugin.define({
@@ -188,7 +188,7 @@ router.route("test").echo({
188
188
  Accept and return XML:
189
189
 
190
190
  ```ts
191
- import { createHandler } from "@donkeylabs/server";
191
+ import { createHandler } from "./handlers";
192
192
  import { parseXML, buildXML } from "./utils/xml";
193
193
 
194
194
  type XMLFn = (data: object, ctx: ServerContext) => Promise<object>;
@@ -404,53 +404,18 @@ TypedHandler returns structured validation errors:
404
404
 
405
405
  ---
406
406
 
407
- ## Server-Level Handler Registration
408
-
409
- Register custom handlers directly on the server without creating a plugin:
410
-
411
- ```ts
412
- import { AppServer, createHandler, type ServerContext } from "@donkeylabs/server";
413
-
414
- // Define custom handler
415
- type EchoFn = (body: any, ctx: ServerContext) => Promise<{ echo: any }>;
416
- const EchoHandler = createHandler<EchoFn>(async (req, def, handle, ctx) => {
417
- const body = await req.json();
418
- const result = await handle(body, ctx);
419
- return Response.json(result);
420
- });
421
-
422
- // Register on server
423
- const server = new AppServer({ db, port: 3000 });
424
- server.registerHandler("echo", EchoHandler);
425
-
426
- // Use in routes
427
- router.route("test").echo({
428
- handle: async (body, ctx) => ({ echo: body }),
429
- });
430
- ```
431
-
432
- **Note:** For typed autocomplete on custom handlers registered this way, you'll need to manually extend the `HandlerRegistry` type or use plugin-based registration which auto-generates types.
433
-
434
- ---
435
-
436
407
  ## Handler Resolution
437
408
 
438
- The server resolves handlers at runtime in this order:
409
+ The server resolves handlers at runtime:
439
410
 
440
- 1. **Built-in handlers** (`typed`, `raw`)
441
- 2. **User-registered handlers** (`server.registerHandler()`)
442
- 3. **Plugin handlers** (`plugin.handlers`)
443
-
444
- When a route specifies a handler name, the server looks it up in this order until found:
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
445
414
 
446
415
  ```ts
447
- // Route specifies handler
448
- router.route("test").echo({ handle: ... });
449
-
450
- // Server resolves "echo" by checking:
451
- // 1. Handlers.echo (built-in) - not found
452
- // 2. customHandlers.get("echo") (user-registered) - found or not
453
- // 3. plugin.handlers.echo (from plugins) - found or not
416
+ // In server.ts (simplified)
417
+ const handler = handlers[route.handler];
418
+ const response = await handler.execute(req, route, route.handle, ctx);
454
419
  ```
455
420
 
456
421
  ---
@@ -482,7 +447,7 @@ bun run gen:registry
482
447
  This generates `registry.d.ts` which augments `IRouteBuilder`:
483
448
 
484
449
  ```ts
485
- declare module "@donkeylabs/server" {
450
+ declare module "./router" {
486
451
  interface IRouteBuilder<TRouter> {
487
452
  echo(config: { handle: EchoFn }): TRouter;
488
453
  // ... other handlers
package/docs/logger.md CHANGED
@@ -47,36 +47,11 @@ const server = new AppServer({
47
47
  logger: {
48
48
  level: "info", // Minimum level to log (default: "info")
49
49
  format: "pretty", // "pretty" or "json" (default: "pretty")
50
- timezone: "America/Chicago", // Timezone for timestamps (optional)
51
50
  transports: [], // Custom transports (optional)
52
51
  },
53
52
  });
54
53
  ```
55
54
 
56
- ### Timezone Support
57
-
58
- Configure timezone for log timestamps:
59
-
60
- ```ts
61
- const server = new AppServer({
62
- db,
63
- logger: {
64
- timezone: "America/New_York", // EST/EDT
65
- },
66
- });
67
- // Output: [14:30:45.123 EST][INFO] Server started
68
-
69
- const server = new AppServer({
70
- db,
71
- logger: {
72
- timezone: "UTC",
73
- },
74
- });
75
- // Output: [19:30:45.123 UTC][INFO] Server started
76
- ```
77
-
78
- Without timezone config, timestamps use local time.
79
-
80
55
  ---
81
56
 
82
57
  ## Usage Examples
@@ -131,29 +106,6 @@ service: async (ctx) => {
131
106
  };
132
107
  ```
133
108
 
134
- ### Plugin Logger (Recommended)
135
-
136
- Plugins get an auto-namespaced logger via `ctx.logger`:
137
-
138
- ```ts
139
- service: async (ctx) => {
140
- // ctx.logger is automatically namespaced with { plugin: "pluginName" }
141
- const logger = ctx.logger;
142
-
143
- logger.info("Plugin initialized");
144
- // Output: [14:30:45.123 CST][payments][INFO] Plugin initialized
145
-
146
- return {
147
- async process() {
148
- logger.info("Processing request");
149
- // Output: [14:30:45.456 CST][payments][INFO] Processing request
150
- },
151
- };
152
- };
153
- ```
154
-
155
- This is cleaner than manually creating child loggers with plugin context.
156
-
157
109
  ### Request Logging Middleware
158
110
 
159
111
  ```ts
@@ -197,15 +149,8 @@ const requestLogger = createMiddleware(async (req, ctx, next) => {
197
149
  Human-readable colored output for development:
198
150
 
199
151
  ```
200
- [12:34:56.789][INFO] User logged in userId=123
201
- [12:34:56.790][ERROR] Payment failed orderId=456 error="Insufficient funds"
202
- ```
203
-
204
- With plugin context and timezone:
205
-
206
- ```
207
- [12:34:56.789 CST][payments][INFO] Payment processed route=api.checkout duration=45.23ms
208
- [12:34:56.790 CST][auth][WARN] Invalid token attempt ip=192.168.1.1
152
+ [12:34:56.789] INFO User logged in {"userId":123}
153
+ [12:34:56.790] ERROR Payment failed {"orderId":456,"error":"Insufficient funds"}
209
154
  ```
210
155
 
211
156
  ### JSON Format
@@ -224,7 +169,7 @@ Structured JSON for production log aggregation:
224
169
  Create custom transports to send logs to external services:
225
170
 
226
171
  ```ts
227
- import { createLogger, type LogTransport, type LogEntry } from "@donkeylabs/server";
172
+ import { createLogger, type LogTransport, type LogEntry } from "./core/logger";
228
173
 
229
174
  // Custom transport for external service
230
175
  class DatadogTransport implements LogTransport {