@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/docs/testing.md DELETED
@@ -1,438 +0,0 @@
1
- # Testing
2
-
3
- Testing utilities for @donkeylabs/server plugins and routes.
4
-
5
- ## Test Harness
6
-
7
- The framework provides `createTestHarness` for testing plugins with real services:
8
-
9
- ```ts
10
- import { createTestHarness } from "@donkeylabs/server/harness";
11
- import { myPlugin } from "./plugins/myPlugin";
12
- import { depPlugin } from "./plugins/depPlugin";
13
-
14
- const { manager, db, core } = await createTestHarness(myPlugin, [depPlugin]);
15
-
16
- // Access plugin service
17
- const service = manager.getServices().myPlugin;
18
-
19
- // Use real in-memory database
20
- await db.insertInto("users").values({ name: "Test" }).execute();
21
-
22
- // Use real core services
23
- core.logger.info("Test running");
24
- await core.cache.set("key", "value");
25
- ```
26
-
27
- ---
28
-
29
- ## What the Harness Provides
30
-
31
- | Component | Description |
32
- |-----------|-------------|
33
- | In-memory SQLite | Real Kysely database, auto-runs migrations |
34
- | Logger | Configured for `warn` level (less verbose) |
35
- | Cache | Full in-memory cache service |
36
- | Events | Pub/sub event system |
37
- | Cron | Scheduled task service |
38
- | Jobs | Background job queue |
39
- | SSE | Server-sent events service |
40
- | RateLimiter | Rate limiting service |
41
- | Errors | Error factory functions |
42
-
43
- ---
44
-
45
- ## When to Use
46
-
47
- | Scenario | Use Harness | Use Manual Mock |
48
- |----------|-------------|-----------------|
49
- | Testing plugin service methods | Yes | |
50
- | Testing database queries | Yes | |
51
- | Testing core service integration | Yes | |
52
- | Unit testing pure functions | | Yes |
53
- | Testing HTTP endpoints | | Yes (real server) |
54
- | Testing middleware | | Yes (mock request/response) |
55
-
56
- ---
57
-
58
- ## Unit Tests
59
-
60
- Test plugin services with the harness:
61
-
62
- ```ts
63
- import { describe, it, expect } from "bun:test";
64
- import { createTestHarness } from "@donkeylabs/server/harness";
65
- import { usersPlugin } from "../plugins/users";
66
-
67
- describe("users plugin", () => {
68
- it("creates a user", async () => {
69
- const { manager, db } = await createTestHarness(usersPlugin);
70
- const users = manager.getServices().users;
71
-
72
- const user = await users.create("test@example.com", "Test User");
73
-
74
- expect(user.email).toBe("test@example.com");
75
-
76
- // Verify in database
77
- const found = await db.selectFrom("users").selectAll().execute();
78
- expect(found).toHaveLength(1);
79
- });
80
-
81
- it("finds user by email", async () => {
82
- const { manager, db } = await createTestHarness(usersPlugin);
83
- const users = manager.getServices().users;
84
-
85
- // Seed data directly
86
- await db.insertInto("users").values({
87
- email: "existing@example.com",
88
- name: "Existing",
89
- }).execute();
90
-
91
- const found = await users.findByEmail("existing@example.com");
92
-
93
- expect(found?.name).toBe("Existing");
94
- });
95
- });
96
- ```
97
-
98
- ---
99
-
100
- ## Testing Plugins with Dependencies
101
-
102
- ```ts
103
- import { createTestHarness } from "@donkeylabs/server/harness";
104
- import { notificationsPlugin } from "../plugins/notifications";
105
- import { usersPlugin } from "../plugins/users";
106
- import { emailPlugin } from "../plugins/email";
107
-
108
- describe("notifications plugin", () => {
109
- it("sends notification to user", async () => {
110
- // notificationsPlugin depends on users and email
111
- const { manager } = await createTestHarness(notificationsPlugin, [
112
- usersPlugin,
113
- emailPlugin({ apiKey: "test", fromAddress: "test@test.com", sandbox: true }),
114
- ]);
115
-
116
- const notifications = manager.getServices().notifications;
117
-
118
- // Dependencies are resolved and available
119
- await notifications.notifyUser(1, "Hello!");
120
- });
121
- });
122
- ```
123
-
124
- ---
125
-
126
- ## Testing with Events
127
-
128
- ```ts
129
- import { createTestHarness } from "@donkeylabs/server/harness";
130
- import { statsPlugin } from "../plugins/stats";
131
-
132
- describe("stats plugin", () => {
133
- it("records events", async () => {
134
- const { manager, core } = await createTestHarness(statsPlugin);
135
- const stats = manager.getServices().stats;
136
-
137
- // Emit event that plugin listens to
138
- core.events.emit("request.completed", {
139
- route: "users.list",
140
- duration: 50,
141
- });
142
-
143
- // Give async handlers time to process
144
- await new Promise((r) => setTimeout(r, 10));
145
-
146
- const summary = stats.getStats();
147
- expect(summary.totalRequests).toBe(1);
148
- });
149
- });
150
- ```
151
-
152
- ---
153
-
154
- ## Integration Tests
155
-
156
- For HTTP endpoint testing, run the actual server:
157
-
158
- ```ts
159
- import { describe, it, expect, beforeAll, afterAll } from "bun:test";
160
- import { spawn } from "bun";
161
-
162
- const BASE_URL = "http://localhost:3001";
163
- let serverProcess: ReturnType<typeof spawn>;
164
-
165
- beforeAll(async () => {
166
- // Start server on test port
167
- serverProcess = spawn({
168
- cmd: ["bun", "run", "src/index.ts"],
169
- env: { ...process.env, PORT: "3001" },
170
- });
171
-
172
- // Wait for server to be ready
173
- await new Promise((r) => setTimeout(r, 1000));
174
- });
175
-
176
- afterAll(() => {
177
- serverProcess.kill();
178
- });
179
-
180
- describe("users API", () => {
181
- it("POST /users.create returns user", async () => {
182
- const res = await fetch(`${BASE_URL}/users.create`, {
183
- method: "POST",
184
- headers: { "Content-Type": "application/json" },
185
- body: JSON.stringify({ email: "test@example.com", name: "Test" }),
186
- });
187
-
188
- expect(res.ok).toBe(true);
189
- const user = await res.json();
190
- expect(user.email).toBe("test@example.com");
191
- });
192
-
193
- it("returns validation error for invalid input", async () => {
194
- const res = await fetch(`${BASE_URL}/users.create`, {
195
- method: "POST",
196
- headers: { "Content-Type": "application/json" },
197
- body: JSON.stringify({ email: "not-an-email" }),
198
- });
199
-
200
- expect(res.status).toBe(400);
201
- const error = await res.json();
202
- expect(error.error).toBe("Validation Failed");
203
- });
204
- });
205
- ```
206
-
207
- ---
208
-
209
- ## Testing Middleware
210
-
211
- Test middleware in isolation:
212
-
213
- ```ts
214
- import { describe, it, expect, vi } from "bun:test";
215
- import { authMiddleware } from "./middleware/auth";
216
-
217
- describe("auth middleware", () => {
218
- it("rejects request without token when required", async () => {
219
- const req = new Request("http://test", {
220
- method: "POST",
221
- });
222
-
223
- const ctx = { user: undefined } as any;
224
- const next = vi.fn();
225
-
226
- const response = await authMiddleware.execute(
227
- req,
228
- ctx,
229
- next,
230
- { required: true }
231
- );
232
-
233
- expect(response.status).toBe(401);
234
- expect(next).not.toHaveBeenCalled();
235
- });
236
-
237
- it("allows request with valid token", async () => {
238
- const req = new Request("http://test", {
239
- method: "POST",
240
- headers: { Authorization: "Bearer valid-token" },
241
- });
242
-
243
- const ctx = { user: undefined } as any;
244
- const next = vi.fn().mockResolvedValue(Response.json({ ok: true }));
245
-
246
- // Mock token validation
247
- vi.mock("./auth-service", () => ({
248
- verifyToken: vi.fn().mockResolvedValue({ id: 1, name: "Test" }),
249
- }));
250
-
251
- const response = await authMiddleware.execute(
252
- req,
253
- ctx,
254
- next,
255
- { required: true }
256
- );
257
-
258
- expect(next).toHaveBeenCalled();
259
- expect(ctx.user).toBeDefined();
260
- });
261
- });
262
- ```
263
-
264
- ---
265
-
266
- ## Testing Handlers
267
-
268
- Test custom handlers directly:
269
-
270
- ```ts
271
- import { describe, it, expect } from "bun:test";
272
- import { EchoHandler } from "./handlers/echo";
273
-
274
- describe("EchoHandler", () => {
275
- it("echoes request body", async () => {
276
- const req = new Request("http://test", {
277
- method: "POST",
278
- body: JSON.stringify({ hello: "world" }),
279
- });
280
-
281
- const mockHandle = async (body: any) => ({ echo: body });
282
- const ctx = {} as any;
283
-
284
- const response = await EchoHandler.execute(req, {}, mockHandle, ctx);
285
- const json = await response.json();
286
-
287
- expect(json).toEqual({ echo: { hello: "world" } });
288
- });
289
-
290
- it("handles invalid JSON", async () => {
291
- const req = new Request("http://test", {
292
- method: "POST",
293
- body: "not json",
294
- });
295
-
296
- const mockHandle = async () => ({});
297
- const ctx = {} as any;
298
-
299
- const response = await EchoHandler.execute(req, {}, mockHandle, ctx);
300
-
301
- expect(response.status).toBe(400);
302
- });
303
- });
304
- ```
305
-
306
- ---
307
-
308
- ## Route File Organization
309
-
310
- The starter project organizes tests alongside route handlers:
311
-
312
- ```
313
- routes/<namespace>/<route>/
314
- ├── index.ts # Route definition
315
- ├── schema.ts # Zod schemas (Input, Output)
316
- ├── models/
317
- │ └── model.ts # Handler class
318
- └── tests/
319
- ├── unit.test.ts # Unit tests (harness)
320
- └── integ.test.ts # Integration tests (real HTTP)
321
- ```
322
-
323
- Example test structure:
324
-
325
- ```ts
326
- // routes/health/ping/tests/unit.test.ts
327
- import { describe, it, expect } from "bun:test";
328
- import { PingModel } from "../models/model";
329
-
330
- describe("PingModel", () => {
331
- it("returns pong message", () => {
332
- const ctx = {} as any; // Minimal mock
333
- const model = new PingModel(ctx);
334
-
335
- const result = model.handle({ message: "ping" });
336
-
337
- expect(result.response).toBe("pong");
338
- });
339
- });
340
- ```
341
-
342
- ```ts
343
- // routes/health/ping/tests/integ.test.ts
344
- import { describe, it, expect } from "bun:test";
345
-
346
- describe("health.ping endpoint", () => {
347
- it("responds to ping", async () => {
348
- const res = await fetch("http://localhost:3000/health.ping", {
349
- method: "POST",
350
- headers: { "Content-Type": "application/json" },
351
- body: JSON.stringify({ message: "ping" }),
352
- });
353
-
354
- expect(res.ok).toBe(true);
355
- const data = await res.json();
356
- expect(data.response).toBe("pong");
357
- });
358
- });
359
- ```
360
-
361
- ---
362
-
363
- ## Best Practices
364
-
365
- ### 1. Use Harness for Plugin Tests
366
-
367
- ```ts
368
- // Good - uses harness for real DB
369
- const { manager, db } = await createTestHarness(usersPlugin);
370
- const users = manager.getServices().users;
371
- await users.create("test@test.com", "Test");
372
-
373
- // Bad - manual mocking that may miss real behavior
374
- const mockDb = { insertInto: vi.fn() };
375
- const users = createUsersService({ db: mockDb });
376
- ```
377
-
378
- ### 2. Isolate Each Test
379
-
380
- ```ts
381
- // Good - each test gets fresh harness
382
- describe("users", () => {
383
- it("test 1", async () => {
384
- const { manager } = await createTestHarness(usersPlugin);
385
- // ...
386
- });
387
-
388
- it("test 2", async () => {
389
- const { manager } = await createTestHarness(usersPlugin);
390
- // Clean slate, no data from test 1
391
- });
392
- });
393
- ```
394
-
395
- ### 3. Test Error Cases
396
-
397
- ```ts
398
- it("throws on duplicate email", async () => {
399
- const { manager, db } = await createTestHarness(usersPlugin);
400
- const users = manager.getServices().users;
401
-
402
- await users.create("test@test.com", "First");
403
-
404
- await expect(users.create("test@test.com", "Second"))
405
- .rejects.toThrow();
406
- });
407
- ```
408
-
409
- ### 4. Use Descriptive Test Names
410
-
411
- ```ts
412
- // Good - describes scenario and expectation
413
- it("returns empty array when no users exist", async () => {});
414
- it("throws NotFound when user does not exist", async () => {});
415
- it("updates only provided fields", async () => {});
416
-
417
- // Bad - vague
418
- it("works", async () => {});
419
- it("handles edge case", async () => {});
420
- ```
421
-
422
- ---
423
-
424
- ## Running Tests
425
-
426
- ```sh
427
- # Run all tests
428
- bun test
429
-
430
- # Run specific test file
431
- bun test src/plugins/users/tests/unit.test.ts
432
-
433
- # Run tests matching pattern
434
- bun test --filter "users"
435
-
436
- # Watch mode
437
- bun test --watch
438
- ```