@donkeylabs/server 0.3.0 → 0.3.1
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/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +6 -6
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +551 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +19 -23
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/server.ts +354 -337
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
package/LICENSE
CHANGED
package/docs/api-client.md
CHANGED
|
@@ -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 "
|
|
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 "
|
|
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.
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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.
|
|
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 "
|
|
328
|
+
import { createCache, type CacheAdapter } from "./core/cache";
|
|
402
329
|
import Redis from "ioredis";
|
|
403
330
|
|
|
404
331
|
class RedisCacheAdapter implements CacheAdapter {
|
package/docs/core-services.md
CHANGED
|
@@ -112,7 +112,7 @@ interface CoreServices {
|
|
|
112
112
|
Configure services when creating the server:
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
|
-
import { AppServer } from "
|
|
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:
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
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
|
-
| `
|
|
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 "
|
|
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 "
|
|
9
|
-
import type { ServerContext } from "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
409
|
+
The server resolves handlers at runtime:
|
|
439
410
|
|
|
440
|
-
1.
|
|
441
|
-
2.
|
|
442
|
-
3.
|
|
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
|
-
//
|
|
448
|
-
|
|
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 "
|
|
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]
|
|
201
|
-
[12:34:56.790]
|
|
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 "
|
|
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 {
|