@donkeylabs/server 0.4.3 → 0.4.5

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/CLAUDE.md ADDED
@@ -0,0 +1,455 @@
1
+ ---
2
+ description: Plugin system for Bun with type-safe handlers, core services, and auto-generated registries.
3
+ globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # @donkeylabs/server
8
+
9
+ A **type-safe plugin system** for building RPC-style APIs with Bun. Features automatic dependency resolution, database schema merging, custom handlers, middleware, and built-in core services.
10
+
11
+ ---
12
+
13
+ ## AI Assistant Instructions
14
+
15
+ **IMPORTANT: Follow these guidelines when working with this codebase.**
16
+
17
+ ### 1. Use MCP Tools First
18
+
19
+ When the `donkeylabs` MCP server is available, **always use MCP tools** instead of writing code manually:
20
+
21
+ | Task | Use MCP Tool |
22
+ |------|--------------|
23
+ | Create a plugin | `create_plugin` |
24
+ | Add a route | `add_route` |
25
+ | Add database migration | `add_migration` |
26
+ | Add service method | `add_service_method` |
27
+ | Generate types | `generate_types` |
28
+
29
+ MCP tools ensure correct file structure, naming conventions, and patterns.
30
+
31
+ ### 2. Read Docs Before Implementing
32
+
33
+ Before implementing any feature, **read the relevant documentation**:
34
+
35
+ | Feature | Read First |
36
+ |---------|------------|
37
+ | Testing | [docs/testing.md](docs/testing.md) - Test harness, unit & integration tests |
38
+ | Database queries | [docs/database.md](docs/database.md) - Use Kysely, NOT raw SQL |
39
+ | Creating plugins | [docs/plugins.md](docs/plugins.md) - Includes plugin vs route decision |
40
+ | Adding routes | [docs/router.md](docs/router.md) |
41
+ | Migrations | [docs/database.md](docs/database.md) - Use Kysely schema builder |
42
+ | Middleware | [docs/middleware.md](docs/middleware.md) |
43
+ | Background jobs | [docs/jobs.md](docs/jobs.md) |
44
+ | Cron tasks | [docs/cron.md](docs/cron.md) |
45
+
46
+ ### 3. Key Patterns to Follow
47
+
48
+ - **Plugins vs Routes**: Plugins = reusable business logic; Routes = API endpoints. See [docs/plugins.md](docs/plugins.md)
49
+ - **Kysely for DB**: Always use Kysely query builder, never raw SQL. See [docs/database.md](docs/database.md)
50
+ - **Migrations**: Use TypeScript migrations with Kysely schema builder (NOT `sql` tagged templates)
51
+ - **Type generation**: Run `donkeylabs generate` after adding plugins/migrations
52
+ - **Thin routes**: Keep route handlers thin; delegate business logic to plugin services
53
+
54
+ ### 4. Write Tests
55
+
56
+ **REQUIRED: Write tests for new functionality.** See [docs/testing.md](docs/testing.md)
57
+
58
+ ```ts
59
+ import { createTestHarness } from "@donkeylabs/server/harness";
60
+ import { myPlugin } from "./plugins/myPlugin";
61
+
62
+ const { manager, db, core } = await createTestHarness(myPlugin);
63
+ const service = manager.getServices().myPlugin;
64
+ ```
65
+
66
+ - **Unit tests**: Test plugin service methods in isolation
67
+ - **Integration tests**: Test plugins working together
68
+ - **Place tests next to code**: `plugins/users/tests/unit.test.ts`
69
+
70
+ ### 5. Verify Before Committing
71
+
72
+ **REQUIRED: Always run these checks before finishing:**
73
+
74
+ ```sh
75
+ # 1. Type check - catch type errors
76
+ bun --bun tsc --noEmit
77
+
78
+ # 2. Run tests - ensure nothing is broken
79
+ bun test
80
+
81
+ # 3. Generate types - if you added plugins/migrations
82
+ donkeylabs generate
83
+ ```
84
+
85
+ **Do NOT skip these steps.** Type errors and failing tests must be fixed before completion.
86
+
87
+ ---
88
+
89
+ ## Bun-First Development
90
+
91
+ Always use Bun instead of Node.js:
92
+
93
+ ```sh
94
+ bun <file> # Instead of node/ts-node
95
+ bun test # Instead of jest/vitest
96
+ bun install # Instead of npm/yarn/pnpm install
97
+ bun run <script> # Instead of npm run
98
+ ```
99
+
100
+ Bun automatically loads `.env` - don't use dotenv.
101
+
102
+ ---
103
+
104
+ ## Package Structure
105
+
106
+ ```
107
+ @donkeylabs/server/
108
+ ├── src/ # Library source code
109
+ │ ├── index.ts # Main exports
110
+ │ ├── core.ts # Plugin system, PluginManager, type helpers
111
+ │ ├── router.ts # Route builder, handler registry
112
+ │ ├── handlers.ts # TypedHandler, RawHandler, createHandler
113
+ │ ├── middleware.ts # Middleware system
114
+ │ ├── server.ts # AppServer, HTTP handling, core services init
115
+ │ ├── harness.ts # Test harness with in-memory DB
116
+ │ ├── client/ # API client base
117
+ │ │ └── base.ts # Client base class
118
+ │ └── core/ # Core services
119
+ │ ├── index.ts # Re-exports all services
120
+ │ ├── logger.ts # Logger service
121
+ │ ├── cache.ts # Cache service
122
+ │ ├── events.ts # Events service
123
+ │ ├── cron.ts # Cron service
124
+ │ ├── jobs.ts # Jobs service
125
+ │ ├── sse.ts # SSE service
126
+ │ ├── rate-limiter.ts # Rate limiter service
127
+ │ └── errors.ts # Error factories
128
+ ├── cli/ # CLI commands
129
+ │ ├── index.ts # CLI entry point (donkeylabs command)
130
+ │ └── commands/
131
+ │ ├── init.ts # Project scaffolding
132
+ │ ├── generate.ts # Type generation
133
+ │ └── plugin.ts # Plugin creation
134
+ ├── templates/ # Templates for init and plugin commands
135
+ │ ├── init/ # New project templates
136
+ │ └── plugin/ # Plugin scaffolding templates
137
+ ├── examples/ # Example projects
138
+ │ └── starter/ # Complete starter template
139
+ │ ├── src/index.ts
140
+ │ ├── src/plugins/ # Example plugins (stats with middleware)
141
+ │ ├── src/routes/ # Example routes with typing
142
+ │ └── donkeylabs.config.ts
143
+ ├── scripts/ # Build and generation scripts
144
+ ├── test/ # Test files
145
+ ├── registry.d.ts # Auto-generated plugin/handler registry
146
+ └── context.d.ts # Auto-generated GlobalContext type
147
+ ```
148
+
149
+ ### Generated Files (DO NOT EDIT)
150
+
151
+ - `registry.d.ts` - Plugin and handler type registry
152
+ - `context.d.ts` - Server context with merged schemas
153
+ - `.@donkeylabs/server/` - Generated types in user projects (gitignored)
154
+
155
+ ---
156
+
157
+ ## User Project Structure
158
+
159
+ After running `donkeylabs init`:
160
+
161
+ ```
162
+ my-project/
163
+ ├── src/
164
+ │ ├── index.ts # Server entry point
165
+ │ └── plugins/ # Your plugins
166
+ │ └── myPlugin/
167
+ │ ├── index.ts # Plugin definition
168
+ │ ├── schema.ts # Generated DB types
169
+ │ └── migrations/ # SQL migrations
170
+ ├── .@donkeylabs/server/ # Generated types (gitignored)
171
+ │ ├── registry.d.ts
172
+ │ └── context.d.ts
173
+ ├── donkeylabs.config.ts # Configuration file
174
+ ├── package.json
175
+ └── tsconfig.json
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Quick Start
181
+
182
+ ### 1. Create a Plugin
183
+
184
+ ```ts
185
+ // src/plugins/myPlugin/index.ts
186
+ import { createPlugin } from "@donkeylabs/server";
187
+
188
+ export const myPlugin = createPlugin.define({
189
+ name: "myPlugin",
190
+ service: async (ctx) => ({
191
+ greet: (name: string) => `Hello, ${name}!`
192
+ })
193
+ });
194
+ ```
195
+
196
+ ### 2. Create Routes
197
+
198
+ ```ts
199
+ // src/index.ts
200
+ import { createRouter } from "@donkeylabs/server";
201
+ import { z } from "zod";
202
+
203
+ const router = createRouter("api")
204
+ .route("greet").typed({
205
+ input: z.object({ name: z.string() }),
206
+ handle: async (input, ctx) => {
207
+ return { message: ctx.plugins.myPlugin.greet(input.name) };
208
+ }
209
+ });
210
+ ```
211
+
212
+ ### 3. Start Server
213
+
214
+ ```ts
215
+ // src/index.ts
216
+ import { AppServer } from "@donkeylabs/server";
217
+ import { myPlugin } from "./plugins/myPlugin";
218
+
219
+ const server = new AppServer({
220
+ db: createDatabase(),
221
+ port: 3000,
222
+ });
223
+
224
+ server.registerPlugin(myPlugin);
225
+ server.use(router);
226
+ await server.start();
227
+ ```
228
+
229
+ ### 4. Make Requests
230
+
231
+ ```sh
232
+ curl -X POST http://localhost:3000/api.greet \
233
+ -H "Content-Type: application/json" \
234
+ -d '{"name": "World"}'
235
+ # {"message": "Hello, World!"}
236
+ ```
237
+
238
+ ---
239
+
240
+ ## CLI Commands
241
+
242
+ ```sh
243
+ donkeylabs # Interactive menu (context-aware)
244
+ donkeylabs init # Create new project
245
+ donkeylabs generate # Generate types from plugins
246
+ donkeylabs plugin create # Interactive plugin creation
247
+ ```
248
+
249
+ ### Interactive Mode
250
+
251
+ Running `donkeylabs` with no arguments launches an interactive menu:
252
+
253
+ **From project root:**
254
+ - Create New Plugin
255
+ - Initialize New Project
256
+ - Generate Types
257
+ - Generate Registry
258
+ - Generate Server Context
259
+
260
+ **From inside a plugin directory (`src/plugins/<name>/`):**
261
+ - Generate Schema Types
262
+ - Create Migration
263
+ - Back to Global Menu
264
+
265
+ ### Development Commands
266
+
267
+ ```sh
268
+ bun run gen:registry # Regenerate registry.d.ts
269
+ bun run gen:server # Regenerate context.d.ts
270
+ bun run cli # Interactive CLI
271
+ bun test # Run all tests
272
+ bun --bun tsc --noEmit # Type check
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Server Context
278
+
279
+ Every route handler receives `ServerContext`:
280
+
281
+ ```ts
282
+ interface ServerContext {
283
+ db: Kysely<MergedSchema>; // Database with all plugin schemas
284
+ plugins: { // All plugin services
285
+ myPlugin: MyPluginService;
286
+ auth: AuthService;
287
+ // ... auto-generated
288
+ };
289
+ core: CoreServices; // Logger, cache, events, etc.
290
+ errors: Errors; // Error factories (BadRequest, NotFound, etc.)
291
+ ip: string; // Client IP address
292
+ requestId: string; // Unique request ID
293
+ user?: any; // Set by auth middleware
294
+ }
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Configuration File
300
+
301
+ ```ts
302
+ // donkeylabs.config.ts
303
+ import { defineConfig } from "@donkeylabs/server";
304
+
305
+ export default defineConfig({
306
+ plugins: ["./src/plugins/**/index.ts"], // Plugin glob patterns
307
+ outDir: ".@donkeylabs/server", // Generated types directory
308
+ client: { // Optional client generation
309
+ output: "./src/client/api.ts",
310
+ },
311
+ });
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Testing
317
+
318
+ ```ts
319
+ import { createTestHarness } from "@donkeylabs/server/harness";
320
+ import { myPlugin } from "./plugins/myPlugin";
321
+
322
+ const { manager, db, core } = await createTestHarness(myPlugin);
323
+
324
+ // Test with real in-memory SQLite + all core services
325
+ const service = manager.getServices().myPlugin;
326
+ expect(service.greet("Test")).toBe("Hello, Test!");
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Package Exports
332
+
333
+ ```ts
334
+ // Main exports
335
+ import { createPlugin, AppServer, createRouter } from "@donkeylabs/server";
336
+
337
+ // Client base class
338
+ import { RpcClient } from "@donkeylabs/server/client";
339
+
340
+ // Test harness
341
+ import { createTestHarness } from "@donkeylabs/server/harness";
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Common Issues
347
+
348
+ ### Handler autocomplete not working
349
+ 1. Run `donkeylabs generate` or `bun run gen:registry`
350
+ 2. Restart TypeScript language server (Cmd+Shift+P > "Restart TS Server")
351
+
352
+ ### Plugin types not recognized
353
+ 1. Ensure `.@donkeylabs/server` is in your tsconfig's `include` array
354
+ 2. Run `donkeylabs generate`
355
+
356
+ ### ctx.plugins shows as `any`
357
+ 1. Make sure `service` comes BEFORE `middleware` in plugin definition
358
+ 2. Run `donkeylabs generate` to regenerate types
359
+ 3. Restart TypeScript language server
360
+
361
+ ### Core services undefined
362
+ 1. Check `ServerConfig` has required `db` property
363
+ 2. Core services are auto-initialized in `AppServer` constructor
364
+
365
+ ---
366
+
367
+ ## Bun APIs
368
+
369
+ Use Bun's built-in APIs instead of npm packages:
370
+
371
+ | Use | Instead of |
372
+ |-----|------------|
373
+ | `Bun.serve()` | express, fastify |
374
+ | `bun:sqlite` | better-sqlite3 |
375
+ | `Bun.redis` | ioredis |
376
+ | `Bun.sql` | pg, postgres.js |
377
+ | `WebSocket` | ws |
378
+ | `Bun.file()` | fs.readFile |
379
+ | `Bun.$\`cmd\`` | execa |
380
+
381
+ See `node_modules/bun-types/docs/**.md` for full API documentation.
382
+
383
+ ---
384
+
385
+ ## Documentation
386
+
387
+ Detailed documentation is available in the `docs/` directory:
388
+
389
+ | Document | Description |
390
+ |----------|-------------|
391
+ | [testing.md](docs/testing.md) | Test harness, unit tests, integration tests, mocking |
392
+ | [database.md](docs/database.md) | Kysely queries, CRUD operations, joins, transactions, migrations |
393
+ | [plugins.md](docs/plugins.md) | Creating plugins, schemas, dependencies, middleware, and init hooks |
394
+ | [router.md](docs/router.md) | Routes, handlers, input/output validation, middleware chains |
395
+ | [middleware.md](docs/middleware.md) | Creating and using middleware with typed configuration |
396
+ | [handlers.md](docs/handlers.md) | Custom handlers (typed, raw, plugin handlers) |
397
+ | [core-services.md](docs/core-services.md) | Overview of all core services |
398
+ | [logger.md](docs/logger.md) | Structured logging with child loggers |
399
+ | [cache.md](docs/cache.md) | In-memory caching with TTL |
400
+ | [events.md](docs/events.md) | Pub/sub event system |
401
+ | [cron.md](docs/cron.md) | Scheduled tasks |
402
+ | [jobs.md](docs/jobs.md) | Background job queue |
403
+ | [sse.md](docs/sse.md) | Server-sent events |
404
+ | [rate-limiter.md](docs/rate-limiter.md) | Request rate limiting |
405
+ | [errors.md](docs/errors.md) | Error factories and custom errors |
406
+ | [api-client.md](docs/api-client.md) | Generated API client usage |
407
+ | [project-structure.md](docs/project-structure.md) | Recommended project organization |
408
+ | [cli.md](docs/cli.md) | CLI commands and interactive mode |
409
+ | [sveltekit-adapter.md](docs/sveltekit-adapter.md) | SvelteKit adapter integration |
410
+
411
+ ---
412
+
413
+ ## MCP Server (AI Integration)
414
+
415
+ An MCP server is available for AI assistants to create and manage plugins following project conventions.
416
+
417
+ ### Available Tools
418
+
419
+ | Tool | Description |
420
+ |------|-------------|
421
+ | `create_plugin` | Create a new plugin with correct structure |
422
+ | `add_route` | Add a route to a router with proper typing |
423
+ | `add_migration` | Create a numbered migration file |
424
+ | `add_service_method` | Add a method to a plugin's service |
425
+ | `generate_types` | Run type generation |
426
+ | `list_plugins` | List all plugins with their methods |
427
+ | `get_project_info` | Get project structure info |
428
+
429
+ ### Configuration
430
+
431
+ Add to your Claude Code MCP settings:
432
+
433
+ ```json
434
+ {
435
+ "mcpServers": {
436
+ "donkeylabs": {
437
+ "command": "bun",
438
+ "args": ["packages/mcp/src/server.ts"]
439
+ }
440
+ }
441
+ }
442
+ ```
443
+
444
+ The MCP server lives in the `packages/mcp/` directory of the monorepo.
445
+
446
+ ### Example Usage
447
+
448
+ AI can call these tools to scaffold code correctly:
449
+
450
+ ```
451
+ Tool: create_plugin
452
+ Args: { "name": "notifications", "hasSchema": true, "dependencies": ["auth"] }
453
+
454
+ Result: Creates src/plugins/notifications/ with index.ts, schema.ts, migrations/
455
+ ```
@@ -0,0 +1,430 @@
1
+ # Testing
2
+
3
+ This guide covers testing plugins and routes using the built-in test harness.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Test Harness](#test-harness)
8
+ - [Unit Testing Plugins](#unit-testing-plugins)
9
+ - [Integration Testing](#integration-testing)
10
+ - [Testing Routes](#testing-routes)
11
+ - [Mocking Core Services](#mocking-core-services)
12
+ - [Test Organization](#test-organization)
13
+ - [Running Tests](#running-tests)
14
+
15
+ ---
16
+
17
+ ## Test Harness
18
+
19
+ The test harness creates a fully functional in-memory testing environment with real SQLite, migrations, and all core services.
20
+
21
+ ```ts
22
+ import { createTestHarness } from "@donkeylabs/server/harness";
23
+ import { myPlugin } from "./plugins/myPlugin";
24
+
25
+ const { manager, db, core } = await createTestHarness(myPlugin);
26
+
27
+ // Access plugin services
28
+ const service = manager.getServices().myPlugin;
29
+
30
+ // Access database directly
31
+ const rows = await db.selectFrom("my_table").selectAll().execute();
32
+
33
+ // Access core services
34
+ core.logger.info("Test log");
35
+ core.cache.set("key", "value");
36
+ ```
37
+
38
+ ### With Dependencies
39
+
40
+ If your plugin depends on other plugins, pass them as the second argument:
41
+
42
+ ```ts
43
+ import { createTestHarness } from "@donkeylabs/server/harness";
44
+ import { ordersPlugin } from "./plugins/orders";
45
+ import { usersPlugin } from "./plugins/users";
46
+
47
+ // ordersPlugin depends on usersPlugin
48
+ const { manager } = await createTestHarness(ordersPlugin, [usersPlugin]);
49
+
50
+ const orders = manager.getServices().orders;
51
+ const users = manager.getServices().users;
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Unit Testing Plugins
57
+
58
+ Unit tests verify individual plugin methods in isolation.
59
+
60
+ ### Basic Plugin Test
61
+
62
+ ```ts
63
+ // plugins/calculator/calculator.test.ts
64
+ import { describe, test, expect } from "bun:test";
65
+ import { createTestHarness } from "@donkeylabs/server/harness";
66
+ import { calculatorPlugin } from "./index";
67
+
68
+ describe("calculatorPlugin", () => {
69
+ test("add() returns correct sum", async () => {
70
+ const { manager } = await createTestHarness(calculatorPlugin);
71
+ const calc = manager.getServices().calculator;
72
+
73
+ expect(calc.add(2, 3)).toBe(5);
74
+ expect(calc.add(-1, 1)).toBe(0);
75
+ });
76
+
77
+ test("divide() throws on zero", async () => {
78
+ const { manager } = await createTestHarness(calculatorPlugin);
79
+ const calc = manager.getServices().calculator;
80
+
81
+ expect(() => calc.divide(10, 0)).toThrow("Cannot divide by zero");
82
+ });
83
+ });
84
+ ```
85
+
86
+ ### Testing Database Operations
87
+
88
+ ```ts
89
+ // plugins/users/users.test.ts
90
+ import { describe, test, expect, beforeEach } from "bun:test";
91
+ import { createTestHarness } from "@donkeylabs/server/harness";
92
+ import { usersPlugin } from "./index";
93
+
94
+ describe("usersPlugin", () => {
95
+ let users: ReturnType<typeof manager.getServices>["users"];
96
+ let db: Awaited<ReturnType<typeof createTestHarness>>["db"];
97
+
98
+ beforeEach(async () => {
99
+ const harness = await createTestHarness(usersPlugin);
100
+ users = harness.manager.getServices().users;
101
+ db = harness.db;
102
+ });
103
+
104
+ test("create() inserts user into database", async () => {
105
+ const user = await users.create({
106
+ email: "test@example.com",
107
+ name: "Test User",
108
+ });
109
+
110
+ expect(user.id).toBeDefined();
111
+ expect(user.email).toBe("test@example.com");
112
+
113
+ // Verify in database
114
+ const dbUser = await db
115
+ .selectFrom("users")
116
+ .where("id", "=", user.id)
117
+ .selectAll()
118
+ .executeTakeFirst();
119
+
120
+ expect(dbUser).toBeDefined();
121
+ expect(dbUser?.email).toBe("test@example.com");
122
+ });
123
+
124
+ test("findByEmail() returns null for non-existent user", async () => {
125
+ const user = await users.findByEmail("notfound@example.com");
126
+ expect(user).toBeNull();
127
+ });
128
+
129
+ test("findByEmail() returns user when exists", async () => {
130
+ await users.create({ email: "exists@example.com", name: "Exists" });
131
+
132
+ const user = await users.findByEmail("exists@example.com");
133
+ expect(user).not.toBeNull();
134
+ expect(user?.name).toBe("Exists");
135
+ });
136
+ });
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Integration Testing
142
+
143
+ Integration tests verify multiple plugins working together.
144
+
145
+ ```ts
146
+ // tests/checkout.integ.test.ts
147
+ import { describe, test, expect, beforeEach } from "bun:test";
148
+ import { createTestHarness } from "@donkeylabs/server/harness";
149
+ import { ordersPlugin } from "../plugins/orders";
150
+ import { usersPlugin } from "../plugins/users";
151
+ import { inventoryPlugin } from "../plugins/inventory";
152
+
153
+ describe("Checkout Integration", () => {
154
+ let services: {
155
+ orders: ReturnType<typeof manager.getServices>["orders"];
156
+ users: ReturnType<typeof manager.getServices>["users"];
157
+ inventory: ReturnType<typeof manager.getServices>["inventory"];
158
+ };
159
+
160
+ beforeEach(async () => {
161
+ const { manager } = await createTestHarness(ordersPlugin, [
162
+ usersPlugin,
163
+ inventoryPlugin,
164
+ ]);
165
+ services = manager.getServices() as typeof services;
166
+ });
167
+
168
+ test("checkout reduces inventory and creates order", async () => {
169
+ // Setup: Create user and add inventory
170
+ const user = await services.users.create({
171
+ email: "buyer@example.com",
172
+ name: "Buyer",
173
+ });
174
+ await services.inventory.add("SKU-001", 10);
175
+
176
+ // Action: Checkout
177
+ const order = await services.orders.checkout({
178
+ userId: user.id,
179
+ items: [{ sku: "SKU-001", quantity: 2 }],
180
+ });
181
+
182
+ // Assert: Order created
183
+ expect(order.status).toBe("completed");
184
+ expect(order.items).toHaveLength(1);
185
+
186
+ // Assert: Inventory reduced
187
+ const stock = await services.inventory.getStock("SKU-001");
188
+ expect(stock).toBe(8);
189
+ });
190
+
191
+ test("checkout fails when insufficient inventory", async () => {
192
+ const user = await services.users.create({
193
+ email: "buyer@example.com",
194
+ name: "Buyer",
195
+ });
196
+ await services.inventory.add("SKU-002", 1);
197
+
198
+ await expect(
199
+ services.orders.checkout({
200
+ userId: user.id,
201
+ items: [{ sku: "SKU-002", quantity: 5 }],
202
+ })
203
+ ).rejects.toThrow("Insufficient inventory");
204
+ });
205
+ });
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Testing Routes
211
+
212
+ For route testing, use Bun's built-in fetch or create a test server.
213
+
214
+ ### Direct Handler Testing
215
+
216
+ ```ts
217
+ // routes/users/users.test.ts
218
+ import { describe, test, expect, beforeEach } from "bun:test";
219
+ import { createTestHarness } from "@donkeylabs/server/harness";
220
+ import { usersPlugin } from "../../plugins/users";
221
+ import { CreateUserHandler } from "./handlers/create-user";
222
+
223
+ describe("CreateUserHandler", () => {
224
+ let ctx: Awaited<ReturnType<typeof createTestHarness>>["core"] & {
225
+ plugins: ReturnType<typeof manager.getServices>;
226
+ };
227
+
228
+ beforeEach(async () => {
229
+ const { manager, core } = await createTestHarness(usersPlugin);
230
+ ctx = {
231
+ ...core,
232
+ plugins: manager.getServices(),
233
+ };
234
+ });
235
+
236
+ test("creates user with valid input", async () => {
237
+ const handler = new CreateUserHandler(ctx as any);
238
+ const result = await handler.handle({
239
+ email: "new@example.com",
240
+ name: "New User",
241
+ });
242
+
243
+ expect(result.id).toBeDefined();
244
+ expect(result.email).toBe("new@example.com");
245
+ });
246
+ });
247
+ ```
248
+
249
+ ### Full HTTP Testing
250
+
251
+ ```ts
252
+ // tests/api.test.ts
253
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
254
+ import { AppServer } from "@donkeylabs/server";
255
+ import { usersPlugin } from "../plugins/users";
256
+ import { usersRouter } from "../routes/users";
257
+
258
+ describe("Users API", () => {
259
+ let server: AppServer;
260
+ let baseUrl: string;
261
+
262
+ beforeAll(async () => {
263
+ server = new AppServer({
264
+ db: createTestDb(),
265
+ port: 0, // Random available port
266
+ });
267
+ server.registerPlugin(usersPlugin);
268
+ server.use(usersRouter);
269
+ await server.start();
270
+ baseUrl = `http://localhost:${server.port}`;
271
+ });
272
+
273
+ afterAll(async () => {
274
+ await server.stop();
275
+ });
276
+
277
+ test("POST /users.create creates a user", async () => {
278
+ const response = await fetch(`${baseUrl}/users.create`, {
279
+ method: "POST",
280
+ headers: { "Content-Type": "application/json" },
281
+ body: JSON.stringify({
282
+ email: "api@example.com",
283
+ name: "API User",
284
+ }),
285
+ });
286
+
287
+ expect(response.status).toBe(200);
288
+ const data = await response.json();
289
+ expect(data.id).toBeDefined();
290
+ expect(data.email).toBe("api@example.com");
291
+ });
292
+
293
+ test("POST /users.create returns 400 for invalid email", async () => {
294
+ const response = await fetch(`${baseUrl}/users.create`, {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json" },
297
+ body: JSON.stringify({
298
+ email: "not-an-email",
299
+ name: "Bad User",
300
+ }),
301
+ });
302
+
303
+ expect(response.status).toBe(400);
304
+ });
305
+ });
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Mocking Core Services
311
+
312
+ The test harness provides real implementations, but you can mock specific services:
313
+
314
+ ```ts
315
+ import { describe, test, expect, mock } from "bun:test";
316
+ import { createTestHarness } from "@donkeylabs/server/harness";
317
+ import { notificationsPlugin } from "./index";
318
+
319
+ describe("notificationsPlugin with mocked email", () => {
320
+ test("sendEmail() is called with correct args", async () => {
321
+ const { manager, core } = await createTestHarness(notificationsPlugin);
322
+
323
+ // Mock the email sending function
324
+ const sendEmailMock = mock(() => Promise.resolve());
325
+ const notifications = manager.getServices().notifications;
326
+ notifications.sendEmail = sendEmailMock;
327
+
328
+ await notifications.notifyUser("user-123", "Hello!");
329
+
330
+ expect(sendEmailMock).toHaveBeenCalledTimes(1);
331
+ expect(sendEmailMock).toHaveBeenCalledWith(
332
+ expect.objectContaining({
333
+ to: expect.any(String),
334
+ subject: expect.stringContaining("Hello"),
335
+ })
336
+ );
337
+ });
338
+ });
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Test Organization
344
+
345
+ ### Recommended Structure
346
+
347
+ ```
348
+ src/
349
+ ├── plugins/
350
+ │ └── users/
351
+ │ ├── index.ts
352
+ │ ├── schema.ts
353
+ │ ├── migrations/
354
+ │ └── tests/
355
+ │ ├── unit.test.ts # Unit tests for service methods
356
+ │ └── integ.test.ts # Integration tests with other plugins
357
+ ├── routes/
358
+ │ └── users/
359
+ │ ├── index.ts
360
+ │ ├── handlers/
361
+ │ └── tests/
362
+ │ └── api.test.ts # Route/API tests
363
+ └── tests/
364
+ └── e2e/ # End-to-end tests
365
+ └── checkout.test.ts
366
+ ```
367
+
368
+ ### Naming Conventions
369
+
370
+ - `*.test.ts` - Unit tests (run with `bun test`)
371
+ - `*.integ.test.ts` - Integration tests
372
+ - `*.e2e.test.ts` - End-to-end tests
373
+
374
+ ---
375
+
376
+ ## Running Tests
377
+
378
+ ```sh
379
+ # Run all tests
380
+ bun test
381
+
382
+ # Run tests for a specific plugin
383
+ bun test plugins/users
384
+
385
+ # Run tests matching a pattern
386
+ bun test --grep "create"
387
+
388
+ # Run tests in watch mode
389
+ bun test --watch
390
+
391
+ # Run with coverage
392
+ bun test --coverage
393
+ ```
394
+
395
+ ### Type Checking
396
+
397
+ Always run type checking before committing:
398
+
399
+ ```sh
400
+ bun --bun tsc --noEmit
401
+ ```
402
+
403
+ ### CI Pipeline Example
404
+
405
+ ```yaml
406
+ # .github/workflows/test.yml
407
+ name: Test
408
+ on: [push, pull_request]
409
+ jobs:
410
+ test:
411
+ runs-on: ubuntu-latest
412
+ steps:
413
+ - uses: actions/checkout@v4
414
+ - uses: oven-sh/setup-bun@v1
415
+ - run: bun install
416
+ - run: bun --bun tsc --noEmit
417
+ - run: bun test
418
+ ```
419
+
420
+ ---
421
+
422
+ ## Best Practices
423
+
424
+ 1. **Use fresh harness per test** - Create a new harness in `beforeEach` to ensure test isolation
425
+ 2. **Test the public API** - Focus on testing service methods, not internal implementation
426
+ 3. **Use realistic data** - Create test data that resembles production data
427
+ 4. **Test edge cases** - Empty inputs, null values, boundary conditions
428
+ 5. **Test error cases** - Verify proper error throwing and handling
429
+ 6. **Keep tests fast** - In-memory SQLite is fast; avoid unnecessary delays
430
+ 7. **Run type checks** - Always run `tsc --noEmit` before committing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -30,6 +30,7 @@
30
30
  "files": [
31
31
  "src",
32
32
  "docs",
33
+ "CLAUDE.md",
33
34
  "context.d.ts",
34
35
  "registry.d.ts",
35
36
  "LICENSE",
package/src/core.ts CHANGED
@@ -360,9 +360,13 @@ export class PluginManager {
360
360
  for (const plugin of sortedPlugins) {
361
361
  const pluginName = plugin.name;
362
362
  const possibleMigrationDirs = [
363
- join(process.cwd(), "examples/basic-server/src/plugins", pluginName, "migrations"),
363
+ // SvelteKit adapter location
364
+ join(process.cwd(), "src/server/plugins", pluginName, "migrations"),
365
+ // Standard locations
364
366
  join(process.cwd(), "src/plugins", pluginName, "migrations"),
365
367
  join(process.cwd(), "plugins", pluginName, "migrations"),
368
+ // Legacy/example location
369
+ join(process.cwd(), "examples/basic-server/src/plugins", pluginName, "migrations"),
366
370
  ];
367
371
 
368
372
  let migrationDir = "";