@donkeylabs/server 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +455 -0
- package/docs/database.md +815 -0
- package/docs/plugins.md +121 -0
- package/docs/testing.md +430 -0
- package/package.json +2 -1
package/docs/plugins.md
CHANGED
|
@@ -4,6 +4,7 @@ Plugins are the core building blocks of this framework. Each plugin encapsulates
|
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
+
- [When to Create a Plugin vs Route](#when-to-create-a-plugin-vs-route)
|
|
7
8
|
- [Creating a Plugin](#creating-a-plugin)
|
|
8
9
|
- [Plugin with Database Schema](#plugin-with-database-schema)
|
|
9
10
|
- [Plugin with Configuration](#plugin-with-configuration)
|
|
@@ -16,6 +17,126 @@ Plugins are the core building blocks of this framework. Each plugin encapsulates
|
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
20
|
+
## When to Create a Plugin vs Route
|
|
21
|
+
|
|
22
|
+
Understanding when to create a plugin versus a route is fundamental to building maintainable applications.
|
|
23
|
+
|
|
24
|
+
### The Core Principle
|
|
25
|
+
|
|
26
|
+
**Plugins = Reusable Business Logic** | **Routes = App-Specific API Endpoints**
|
|
27
|
+
|
|
28
|
+
Think of plugins as your application's "services layer" and routes as your "API layer".
|
|
29
|
+
|
|
30
|
+
### Create a Plugin When:
|
|
31
|
+
|
|
32
|
+
1. **The logic could be reused** across multiple routes or applications
|
|
33
|
+
- Example: User authentication, email sending, payment processing
|
|
34
|
+
|
|
35
|
+
2. **You need database tables** for a domain concept
|
|
36
|
+
- Example: A "users" plugin that owns the users table and provides CRUD methods
|
|
37
|
+
|
|
38
|
+
3. **The functionality is self-contained** with its own data and operations
|
|
39
|
+
- Example: A "notifications" plugin with its own tables, delivery logic, and scheduling
|
|
40
|
+
|
|
41
|
+
4. **You want to share state or connections** (database, external APIs)
|
|
42
|
+
- Example: A "stripe" plugin that manages the Stripe SDK connection
|
|
43
|
+
|
|
44
|
+
5. **You're building cross-cutting concerns** like auth middleware, rate limiting, or logging
|
|
45
|
+
|
|
46
|
+
### Create a Route When:
|
|
47
|
+
|
|
48
|
+
1. **Exposing plugin functionality** to the outside world via HTTP
|
|
49
|
+
- Example: A `users.create` route that calls `ctx.plugins.users.create()`
|
|
50
|
+
|
|
51
|
+
2. **Combining multiple plugins** for a specific use case
|
|
52
|
+
- Example: A checkout route that uses cart, payment, and inventory plugins
|
|
53
|
+
|
|
54
|
+
3. **App-specific endpoints** that don't need to be reused
|
|
55
|
+
- Example: A dashboard summary endpoint specific to your app
|
|
56
|
+
|
|
57
|
+
4. **Simple operations** that don't warrant a full plugin
|
|
58
|
+
- Example: A health check endpoint
|
|
59
|
+
|
|
60
|
+
### Decision Flowchart
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Is this logic reusable across routes or apps?
|
|
64
|
+
├── Yes → Create a Plugin
|
|
65
|
+
└── No
|
|
66
|
+
├── Does it need its own database tables?
|
|
67
|
+
│ └── Yes → Create a Plugin (with hasSchema: true)
|
|
68
|
+
└── Is it just exposing existing functionality via HTTP?
|
|
69
|
+
└── Yes → Create a Route that uses existing plugins
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Example: Building a Task Management System
|
|
73
|
+
|
|
74
|
+
**Step 1: Identify reusable domains → Create Plugins**
|
|
75
|
+
- `tasks` plugin - owns tasks table, CRUD methods
|
|
76
|
+
- `users` plugin - owns users table, auth methods
|
|
77
|
+
- `notifications` plugin - handles email/push notifications
|
|
78
|
+
|
|
79
|
+
**Step 2: Create app-specific routes that use plugins**
|
|
80
|
+
```ts
|
|
81
|
+
// routes/tasks/index.ts
|
|
82
|
+
router.route("create").typed({
|
|
83
|
+
input: CreateTaskInput,
|
|
84
|
+
handle: async (input, ctx) => {
|
|
85
|
+
// Use tasks plugin for business logic
|
|
86
|
+
const task = await ctx.plugins.tasks.create(input);
|
|
87
|
+
|
|
88
|
+
// Use notifications plugin for side effects
|
|
89
|
+
await ctx.plugins.notifications.notify(
|
|
90
|
+
task.assigneeId,
|
|
91
|
+
`New task: ${task.title}`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return task;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Anti-Patterns to Avoid
|
|
100
|
+
|
|
101
|
+
❌ **Don't put business logic in routes** - Routes should be thin; delegate to plugins
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// BAD: Business logic in route
|
|
105
|
+
router.route("create").typed({
|
|
106
|
+
handle: async (input, ctx) => {
|
|
107
|
+
// 50 lines of validation, database calls, notifications...
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// GOOD: Thin route, logic in plugin
|
|
112
|
+
router.route("create").typed({
|
|
113
|
+
handle: async (input, ctx) => {
|
|
114
|
+
return ctx.plugins.tasks.create(input);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
❌ **Don't create a plugin for every route** - Only for reusable logic
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// BAD: Plugin just wrapping a single database call
|
|
123
|
+
export const dashboardPlugin = createPlugin.define({
|
|
124
|
+
name: "dashboard",
|
|
125
|
+
service: async (ctx) => ({
|
|
126
|
+
getSummary: () => ctx.db.selectFrom("stats").execute(),
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// GOOD: Just make it a route
|
|
131
|
+
router.route("dashboard.summary").typed({
|
|
132
|
+
handle: async (_, ctx) => {
|
|
133
|
+
return ctx.db.selectFrom("stats").execute();
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
19
140
|
## Creating a Plugin
|
|
20
141
|
|
|
21
142
|
The simplest plugin exports a service that becomes available to all route handlers:
|
package/docs/testing.md
ADDED
|
@@ -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
|
+
"version": "0.4.4",
|
|
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",
|