@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
@@ -5,7 +5,7 @@ Request/response middleware for cross-cutting concerns like authentication, rate
5
5
  ## Quick Start
6
6
 
7
7
  ```ts
8
- import { createMiddleware } from "@donkeylabs/server";
8
+ import { createMiddleware } from "./middleware";
9
9
 
10
10
  // Create middleware with config
11
11
  const authMiddleware = createMiddleware<{ required: boolean }>(
@@ -66,7 +66,7 @@ function createMiddleware<TConfig = void>(
66
66
 
67
67
  ```ts
68
68
  // middleware/auth.ts
69
- import { createMiddleware } from "@donkeylabs/server";
69
+ import { createMiddleware } from "../middleware";
70
70
 
71
71
  interface AuthConfig {
72
72
  required?: boolean;
package/docs/plugins.md CHANGED
@@ -22,7 +22,7 @@ The simplest plugin exports a service that becomes available to all route handle
22
22
 
23
23
  ```ts
24
24
  // plugins/greeter/index.ts
25
- import { createPlugin } from "@donkeylabs/server";
25
+ import { createPlugin } from "../../core";
26
26
 
27
27
  export const greeterPlugin = createPlugin.define({
28
28
  name: "greeter",
@@ -88,7 +88,7 @@ This creates `plugins/users/schema.ts` with Kysely types.
88
88
 
89
89
  ```ts
90
90
  // plugins/users/index.ts
91
- import { createPlugin } from "@donkeylabs/server";
91
+ import { createPlugin } from "../../core";
92
92
  import type { DB as UsersSchema } from "./schema";
93
93
 
94
94
  export const usersPlugin = createPlugin
@@ -131,7 +131,7 @@ Plugins can accept configuration at registration time using the factory pattern:
131
131
 
132
132
  ```ts
133
133
  // plugins/email/index.ts
134
- import { createPlugin } from "@donkeylabs/server";
134
+ import { createPlugin } from "../../core";
135
135
 
136
136
  export interface EmailConfig {
137
137
  apiKey: string;
@@ -193,7 +193,7 @@ Plugins can depend on other plugins. Dependencies are resolved automatically in
193
193
 
194
194
  ```ts
195
195
  // plugins/notifications/index.ts
196
- import { createPlugin } from "@donkeylabs/server";
196
+ import { createPlugin } from "../../core";
197
197
 
198
198
  export const notificationsPlugin = createPlugin.define({
199
199
  name: "notifications",
@@ -242,8 +242,9 @@ Plugins can define custom request handlers for specialized processing:
242
242
 
243
243
  ```ts
244
244
  // plugins/api/index.ts
245
- import { createPlugin, createHandler } from "@donkeylabs/server";
246
- import type { ServerContext } from "@donkeylabs/server";
245
+ import { createPlugin } from "../../core";
246
+ import { createHandler } from "../../handlers";
247
+ import type { ServerContext } from "../../router";
247
248
 
248
249
  // Define handler signature
249
250
  type XMLHandler = (xmlBody: string, ctx: ServerContext) => Promise<string>;
@@ -369,16 +370,12 @@ The `PluginContext` passed to the service function provides:
369
370
 
370
371
  ```ts
371
372
  interface PluginContext<Deps, Schema, Config> {
372
- // Convenience getters (auto-namespaced for this plugin)
373
- logger: Logger; // Auto-namespaced with { plugin: "pluginName" }
374
- cache: NamespacedCache; // Auto-prefixed with "pluginName:"
375
-
376
373
  // Core services (always available)
377
374
  core: {
378
375
  db: Kysely<any>;
379
376
  config: Record<string, any>;
380
- logger: Logger; // Root logger (use ctx.logger for plugin-scoped)
381
- cache: Cache; // Root cache (use ctx.cache for plugin-scoped)
377
+ logger: Logger;
378
+ cache: Cache;
382
379
  events: Events;
383
380
  cron: Cron;
384
381
  jobs: Jobs;
@@ -397,42 +394,6 @@ interface PluginContext<Deps, Schema, Config> {
397
394
  }
398
395
  ```
399
396
 
400
- **Example using convenience getters:**
401
-
402
- ```ts
403
- export const paymentsPlugin = createPlugin.define({
404
- name: "payments",
405
- service: async (ctx) => {
406
- // ctx.logger is auto-namespaced with { plugin: "payments" }
407
- const logger = ctx.logger;
408
-
409
- // ctx.cache is auto-prefixed with "payments:"
410
- const cache = ctx.cache;
411
-
412
- logger.info("Payment plugin initialized");
413
- // Output: [14:30:45.123 CST][payments][INFO] Payment plugin initialized
414
-
415
- return {
416
- async processPayment(orderId: string) {
417
- // Check cache (key becomes "payments:order:123")
418
- const cached = await cache.get(`order:${orderId}`);
419
- if (cached) {
420
- logger.debug("Cache hit", { orderId });
421
- return cached;
422
- }
423
-
424
- logger.info("Processing payment", { orderId });
425
- // Output: [14:30:45.456 CST][payments][INFO] Processing payment orderId=123
426
-
427
- const result = await chargeCard(orderId);
428
- await cache.set(`order:${orderId}`, result, 60000);
429
- return result;
430
- },
431
- };
432
- },
433
- });
434
- ```
435
-
436
397
  **Example using all context features:**
437
398
 
438
399
  ```ts
@@ -443,13 +404,9 @@ export const analyticsPlugin = createPlugin
443
404
  name: "analytics",
444
405
  dependencies: ["users"] as const,
445
406
  service: async (ctx) => {
446
- // Use plugin-scoped logger and cache
447
- const logger = ctx.logger;
448
- const cache = ctx.cache;
449
-
450
407
  // Schedule daily report
451
408
  ctx.core.cron.schedule("0 0 * * *", async () => {
452
- logger.info("Running daily analytics");
409
+ ctx.core.logger.info("Running daily analytics");
453
410
  // ...
454
411
  });
455
412
 
@@ -462,16 +419,8 @@ export const analyticsPlugin = createPlugin
462
419
  });
463
420
 
464
421
  return {
465
- async track(event: string, data: any) {
466
- // Cache recent events to dedupe
467
- const cacheKey = `event:${event}:${JSON.stringify(data)}`;
468
- if (await cache.has(cacheKey)) {
469
- logger.debug("Duplicate event ignored", { event });
470
- return;
471
- }
472
- await cache.set(cacheKey, true, 5000);
473
-
474
- await ctx.core.events.emit(`analytics.${event}`, {
422
+ track(event: string, data: any) {
423
+ ctx.core.events.emit(`analytics.${event}`, {
475
424
  trackingId: ctx.config.trackingId,
476
425
  ...data,
477
426
  });
@@ -495,7 +444,7 @@ import {
495
444
  InferMiddleware,
496
445
  InferDependencies,
497
446
  InferConfig,
498
- } from "@donkeylabs/server";
447
+ } from "./core";
499
448
 
500
449
  import { usersPlugin } from "./plugins/users";
501
450
 
@@ -94,7 +94,7 @@ plugins/<name>/
94
94
 
95
95
  ```ts
96
96
  // plugins/<name>/index.ts
97
- import { createPlugin } from "@donkeylabs/server";
97
+ import { createPlugin } from "../../core";
98
98
  import type { DB } from "./schema"; // If using database
99
99
 
100
100
  // Configuration type (if plugin is configurable)
@@ -144,7 +144,7 @@ routes/
144
144
 
145
145
  ```ts
146
146
  // routes/users.ts
147
- import { createRouter } from "@donkeylabs/server";
147
+ import { createRouter } from "../router";
148
148
  import { z } from "zod";
149
149
 
150
150
  export const usersRouter = createRouter("users")
@@ -173,144 +173,6 @@ export const usersRouter = createRouter("users")
173
173
 
174
174
  ---
175
175
 
176
- ## Advanced Route Organization
177
-
178
- For larger projects, the starter template provides a more structured route organization:
179
-
180
- ```
181
- routes/<namespace>/<route>/
182
- ├── index.ts # Route definition
183
- ├── schema.ts # Zod schemas (Input, Output)
184
- ├── models/
185
- │ └── model.ts # Handler class
186
- └── tests/
187
- ├── unit.test.ts # Unit tests
188
- └── integ.test.ts # Integration tests
189
- ```
190
-
191
- ### Handler Class Pattern
192
-
193
- Instead of inline handlers, use the `Handler<T>` interface with model classes:
194
-
195
- ```ts
196
- // routes/health/ping/schema.ts
197
- import { z } from "zod";
198
-
199
- export const Input = z.object({ message: z.string() });
200
- export const Output = z.object({ response: z.string() });
201
-
202
- export type Input = z.infer<typeof Input>;
203
- export type Output = z.infer<typeof Output>;
204
- ```
205
-
206
- ```ts
207
- // routes/health/ping/models/model.ts
208
- import type { Handler } from "@donkeylabs/server";
209
- import type { Health } from "$server/routes"; // Generated types
210
- import type { AppContext } from "$server/context";
211
-
212
- export class PingModel implements Handler<Health.Ping> {
213
- private ctx: AppContext;
214
-
215
- constructor(ctx: AppContext) {
216
- this.ctx = ctx;
217
- }
218
-
219
- handle(input: Health.Ping.Input): Health.Ping.Output {
220
- return { response: "pong" };
221
- }
222
- }
223
- ```
224
-
225
- ```ts
226
- // routes/health/ping/index.ts
227
- import { createRoute } from "@donkeylabs/server";
228
- import { Input, Output } from "./schema";
229
- import { PingModel } from "./models/model";
230
-
231
- export const pingRoute = createRoute.typed({
232
- input: Input,
233
- output: Output,
234
- handle: async (input, ctx) => new PingModel(ctx).handle(input),
235
- });
236
- ```
237
-
238
- This pattern separates concerns and makes testing easier.
239
-
240
- ---
241
-
242
- ## Path Aliases
243
-
244
- Configure tsconfig.json path aliases for cleaner imports:
245
-
246
- ```json
247
- {
248
- "compilerOptions": {
249
- "paths": {
250
- "$server/*": [".@donkeylabs/server/*"],
251
- "$api": [".@donkeylabs/server/client"]
252
- }
253
- }
254
- }
255
- ```
256
-
257
- **Usage:**
258
-
259
- ```ts
260
- // Instead of relative paths
261
- import type { AppContext } from "../../../.@donkeylabs/server/context";
262
-
263
- // Use path aliases
264
- import type { AppContext } from "$server/context";
265
- import type { Health } from "$server/routes";
266
- import { api } from "$api";
267
- ```
268
-
269
- ---
270
-
271
- ## Generated Files
272
-
273
- The framework generates type definition files in `.@donkeylabs/server/`:
274
-
275
- | File | Purpose |
276
- |------|---------|
277
- | `registry.d.ts` | Plugin handlers and middleware types |
278
- | `context.d.ts` | Merged `AppContext` with all plugin schemas |
279
- | `routes.d.ts` | Route type definitions for `Handler<T>` |
280
- | `client.ts` | Type-safe API client |
281
-
282
- **Important:**
283
- - These files are auto-generated by `donkeylabs generate`
284
- - Add `.@donkeylabs/` to `.gitignore`
285
- - Regenerate after any plugin or route changes
286
- - Never edit these files manually
287
-
288
- ---
289
-
290
- ## AppContext vs ServerContext
291
-
292
- The framework provides two context types:
293
-
294
- | Type | Source | Use When |
295
- |------|--------|----------|
296
- | `ServerContext` | Library export | Generic library code |
297
- | `AppContext` | Generated | Application code with full typing |
298
-
299
- ```ts
300
- // ServerContext - library type, uses PluginRegistry
301
- import type { ServerContext } from "@donkeylabs/server";
302
-
303
- // AppContext - generated, includes merged schemas
304
- import type { AppContext } from "$server/context";
305
-
306
- // In your handlers/models, prefer AppContext for full typing
307
- export class MyModel {
308
- constructor(private ctx: AppContext) {}
309
- }
310
- ```
311
-
312
- ---
313
-
314
176
  ## Do's and Don'ts
315
177
 
316
178
  ### DO: Use the Plugin System
@@ -554,7 +416,7 @@ export async function down(db: Kysely<any>): Promise<void> {
554
416
 
555
417
  ```ts
556
418
  // test/plugins/auth.test.ts
557
- import { createTestHarness } from "@donkeylabs/server/harness";
419
+ import { createTestHarness } from "../../harness";
558
420
  import { authPlugin } from "../../plugins/auth";
559
421
 
560
422
  describe("Auth Plugin", () => {
@@ -580,7 +442,7 @@ describe("Auth Plugin", () => {
580
442
 
581
443
  ```ts
582
444
  // test/integration.test.ts
583
- import { createTestHarness } from "@donkeylabs/server/harness";
445
+ import { createTestHarness } from "../harness";
584
446
  import { authPlugin } from "../plugins/auth";
585
447
  import { ordersPlugin } from "../plugins/orders";
586
448
 
@@ -24,16 +24,8 @@ if (!result.allowed) {
24
24
 
25
25
  ```ts
26
26
  interface RateLimiter {
27
- // Manual rate limiting
28
27
  check(key: string, limit: number, windowMs: number): Promise<RateLimitResult>;
29
28
  reset(key: string): Promise<void>;
30
-
31
- // Declarative rules (auto-enforced)
32
- registerRule(pattern: string, rule: RateLimitRule): void;
33
- registerRules(rules: Record<string, RateLimitRule>): void;
34
- matchRoute(route: string): RateLimitRule | undefined;
35
- checkRoute(route: string, ctx: { ip: string; user?: any }): Promise<RateLimitResult | null>;
36
- listRules(): Array<{ pattern: string; limit: number; window: string | number }>;
37
29
  }
38
30
 
39
31
  interface RateLimitResult {
@@ -43,14 +35,6 @@ interface RateLimitResult {
43
35
  resetAt: Date; // When window resets
44
36
  retryAfter?: number; // Seconds until retry (if blocked)
45
37
  }
46
-
47
- interface RateLimitRule {
48
- limit: number;
49
- window: string | number; // "1m", "10s", or milliseconds
50
- keyFn?: (ctx: { ip: string; route: string; user?: any }) => string;
51
- skip?: (ctx: { ip: string; route: string; user?: any }) => boolean;
52
- message?: string;
53
- }
54
38
  ```
55
39
 
56
40
  ### Methods
@@ -59,11 +43,6 @@ interface RateLimitRule {
59
43
  |--------|-------------|
60
44
  | `check(key, limit, windowMs)` | Check and increment counter for key |
61
45
  | `reset(key)` | Reset counter for key |
62
- | `registerRule(pattern, rule)` | Register declarative rate limit rule |
63
- | `registerRules(rules)` | Register multiple rules at once |
64
- | `matchRoute(route)` | Get rule matching a route |
65
- | `checkRoute(route, ctx)` | Check rate limit for route (auto-uses registered rules) |
66
- | `listRules()` | List all registered rules |
67
46
 
68
47
  ---
69
48
 
@@ -87,117 +66,6 @@ router.route("protected").typed({
87
66
 
88
67
  ---
89
68
 
90
- ## Declarative Rate Limits
91
-
92
- Register rate limit rules declaratively and let the framework enforce them automatically.
93
-
94
- ### Server-Level Registration
95
-
96
- ```ts
97
- const server = new AppServer({ db });
98
-
99
- // Register individual rules
100
- server.registerRateLimit("auth.login", { limit: 5, window: "15m" });
101
- server.registerRateLimit("api.*", { limit: 100, window: "1m" });
102
- server.registerRateLimit("billing.*", { limit: 10, window: "1h" });
103
-
104
- // Register multiple at once
105
- server.registerRateLimits({
106
- "uploads.create": { limit: 10, window: "1h" },
107
- "exports.*": { limit: 5, window: "1h" },
108
- });
109
- ```
110
-
111
- ### Pattern Matching
112
-
113
- Rules support glob patterns:
114
-
115
- ```ts
116
- // Exact match
117
- server.registerRateLimit("auth.login", { limit: 5, window: "15m" });
118
-
119
- // Wildcard - matches api.users.list, api.posts.create, etc.
120
- server.registerRateLimit("api.*", { limit: 100, window: "1m" });
121
-
122
- // Exact matches take precedence over patterns
123
- server.registerRateLimit("api.search", { limit: 20, window: "1m" }); // More restrictive for search
124
- ```
125
-
126
- ### Skip Function
127
-
128
- Bypass rate limits for certain requests:
129
-
130
- ```ts
131
- server.registerRateLimit("api.*", {
132
- limit: 100,
133
- window: "1m",
134
- skip: (ctx) => {
135
- // Skip for admins
136
- return ctx.user?.role === "admin";
137
- },
138
- });
139
-
140
- server.registerRateLimit("webhooks.*", {
141
- limit: 1000,
142
- window: "1m",
143
- skip: (ctx) => {
144
- // Skip for internal IPs
145
- return ctx.ip.startsWith("10.0.");
146
- },
147
- });
148
- ```
149
-
150
- ### Custom Key Function
151
-
152
- Control how rate limits are grouped:
153
-
154
- ```ts
155
- server.registerRateLimit("api.*", {
156
- limit: 100,
157
- window: "1m",
158
- keyFn: (ctx) => {
159
- // Rate limit per user instead of per IP
160
- return ctx.user?.id ? `user:${ctx.user.id}` : `ip:${ctx.ip}`;
161
- },
162
- });
163
- ```
164
-
165
- ### Auto-Enforcement
166
-
167
- Registered rules are automatically checked before route handlers. If a request exceeds the limit, a 429 response is returned:
168
-
169
- ```json
170
- {
171
- "error": "TOO_MANY_REQUESTS",
172
- "retryAfter": 45
173
- }
174
- ```
175
-
176
- With headers:
177
- ```
178
- Retry-After: 45
179
- X-RateLimit-Limit: 100
180
- X-RateLimit-Remaining: 0
181
- X-RateLimit-Reset: 2024-01-15T12:35:00.000Z
182
- ```
183
-
184
- ### Introspection
185
-
186
- ```ts
187
- // Check which rule applies to a route
188
- const rule = ctx.core.rateLimiter.matchRoute("api.users.list");
189
- // { limit: 100, window: "1m", ... }
190
-
191
- // List all registered rules
192
- const rules = ctx.core.rateLimiter.listRules();
193
- // [
194
- // { pattern: "auth.login", limit: 5, window: "15m" },
195
- // { pattern: "api.*", limit: 100, window: "1m" },
196
- // ]
197
- ```
198
-
199
- ---
200
-
201
69
  ## Usage Examples
202
70
 
203
71
  ### Basic Rate Limiting
@@ -379,7 +247,7 @@ router.middleware
379
247
  Convert duration strings to milliseconds:
380
248
 
381
249
  ```ts
382
- import { parseDuration } from "@donkeylabs/server";
250
+ import { parseDuration } from "./core/rate-limiter";
383
251
 
384
252
  parseDuration("100ms"); // 100
385
253
  parseDuration("30s"); // 30000
@@ -393,7 +261,7 @@ parseDuration("1d"); // 86400000
393
261
  Build consistent rate limit keys:
394
262
 
395
263
  ```ts
396
- import { createRateLimitKey } from "@donkeylabs/server";
264
+ import { createRateLimitKey } from "./core/rate-limiter";
397
265
 
398
266
  const key = createRateLimitKey("api.users.list", "192.168.1.1");
399
267
  // "ratelimit:api.users.list:192.168.1.1"
@@ -404,7 +272,7 @@ const key = createRateLimitKey("api.users.list", "192.168.1.1");
404
272
  Manual IP extraction if needed:
405
273
 
406
274
  ```ts
407
- import { extractClientIP } from "@donkeylabs/server";
275
+ import { extractClientIP } from "./core/rate-limiter";
408
276
 
409
277
  const ip = extractClientIP(req, socketAddr);
410
278
  ```
@@ -555,7 +423,7 @@ interface RateLimitAdapter {
555
423
  ### Redis Adapter Example
556
424
 
557
425
  ```ts
558
- import { createRateLimiter, type RateLimitAdapter } from "@donkeylabs/server";
426
+ import { createRateLimiter, type RateLimitAdapter } from "./core/rate-limiter";
559
427
  import Redis from "ioredis";
560
428
 
561
429
  class RedisRateLimitAdapter implements RateLimitAdapter {
package/docs/router.md CHANGED
@@ -5,7 +5,7 @@ Fluent API for defining type-safe routes with handler selection and middleware c
5
5
  ## Quick Start
6
6
 
7
7
  ```ts
8
- import { createRouter } from "@donkeylabs/server";
8
+ import { createRouter } from "./router";
9
9
  import { z } from "zod";
10
10
 
11
11
  const router = createRouter("api")
@@ -166,18 +166,10 @@ router.route("example").typed({
166
166
  // Plugin services
167
167
  const data = await ctx.plugins.myPlugin.getData();
168
168
 
169
- // Core services (logger, cache, events, cron, jobs, sse, rateLimiter)
169
+ // Core services
170
170
  ctx.core.logger.info("Processing request", { input });
171
171
  const cached = await ctx.core.cache.get("key");
172
172
 
173
- // Error factories
174
- if (!users.length) {
175
- throw ctx.errors.NotFound("No users found");
176
- }
177
-
178
- // Application config
179
- console.log(ctx.config.env); // "development" | "production"
180
-
181
173
  // Request info
182
174
  console.log(ctx.ip); // Client IP
183
175
  console.log(ctx.requestId); // Unique request ID
@@ -475,18 +467,18 @@ router.route("stream").raw({
475
467
  Register routes with the server:
476
468
 
477
469
  ```ts
478
- import { AppServer } from "@donkeylabs/server";
470
+ import { AppServer } from "./server";
479
471
  import { userRouter } from "./routes/users";
480
472
  import { orderRouter } from "./routes/orders";
481
473
 
482
474
  const server = new AppServer({ db, port: 3000 });
483
475
 
484
476
  // Register single router
485
- server.router(userRouter);
477
+ server.use(userRouter);
486
478
 
487
479
  // Register multiple routers
488
- server.router(userRouter);
489
- server.router(orderRouter);
480
+ server.use(userRouter);
481
+ server.use(orderRouter);
490
482
 
491
483
  await server.start();
492
484
  ```