@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/README.md DELETED
@@ -1,254 +0,0 @@
1
- # @donkeylabs/server
2
-
3
- A **type-safe plugin system** for building RPC-style APIs with Bun. Features automatic dependency resolution, database schema merging, middleware chains, and built-in core services.
4
-
5
- ```ts
6
- // Define a plugin
7
- const notesPlugin = createPlugin.define({
8
- name: "notes",
9
- service: async (ctx) => ({
10
- create: (title: string) => ctx.db.insertInto("notes").values({ title }).execute(),
11
- list: () => ctx.db.selectFrom("notes").selectAll().execute(),
12
- }),
13
- });
14
-
15
- // Create routes
16
- const api = server.router("api");
17
- api.route("notes.create").typed({
18
- input: z.object({ title: z.string() }),
19
- handle: async (input, ctx) => ctx.plugins.notes.create(input.title),
20
- });
21
-
22
- // Full type safety end-to-end
23
- ```
24
-
25
- ## Features
26
-
27
- - **Plugin System** - Encapsulate business logic with automatic dependency resolution
28
- - **Type-Safe Routes** - Zod validation with full TypeScript inference
29
- - **Schema Merging** - Plugin database schemas automatically merge into global context
30
- - **Core Services** - Logger, Cache, Events, Jobs, Cron, SSE, Rate Limiter built-in
31
- - **CLI Tools** - Project scaffolding, type generation, plugin creation
32
- - **Client Generation** - Auto-generate typed API clients for your frontend
33
-
34
- ## Quick Start
35
-
36
- ### Install
37
-
38
- ```sh
39
- bun add @donkeylabs/server kysely zod
40
- ```
41
-
42
- ### Create a Project
43
-
44
- ```sh
45
- bunx @donkeylabs/server init my-app
46
- cd my-app
47
- bun run dev
48
- ```
49
-
50
- ### Or Start from Scratch
51
-
52
- ```ts
53
- // src/index.ts
54
- import { AppServer, createPlugin } from "@donkeylabs/server";
55
- import { z } from "zod";
56
- import Database from "bun:sqlite";
57
- import { Kysely, SqliteDialect } from "kysely";
58
-
59
- // Database setup
60
- const db = new Kysely({ dialect: new SqliteDialect({ database: new Database("app.db") }) });
61
-
62
- // Create plugin
63
- const greeterPlugin = createPlugin.define({
64
- name: "greeter",
65
- service: async () => ({
66
- greet: (name: string) => `Hello, ${name}!`,
67
- }),
68
- });
69
-
70
- // Create server
71
- const server = new AppServer({ db, port: 3000 });
72
- server.registerPlugin(greeterPlugin);
73
-
74
- // Define routes
75
- server.router("api")
76
- .route("greet").typed({
77
- input: z.object({ name: z.string() }),
78
- output: z.object({ message: z.string() }),
79
- handle: async (input, ctx) => ({
80
- message: ctx.plugins.greeter.greet(input.name),
81
- }),
82
- });
83
-
84
- await server.start();
85
- // Server running at http://localhost:3000
86
- ```
87
-
88
- ### Make a Request
89
-
90
- ```sh
91
- curl -X POST http://localhost:3000/api.greet \
92
- -H "Content-Type: application/json" \
93
- -d '{"name": "World"}'
94
-
95
- # {"message": "Hello, World!"}
96
- ```
97
-
98
- ## Core Services
99
-
100
- Every route handler has access to built-in services via `ctx.core`:
101
-
102
- ```ts
103
- handle: async (input, ctx) => {
104
- // Logging
105
- ctx.core.logger.info("Processing request", { input });
106
-
107
- // Caching
108
- const cached = await ctx.core.cache.getOrSet("key", async () => {
109
- return await expensiveOperation();
110
- }, 60000);
111
-
112
- // Events
113
- await ctx.core.events.emit("user.created", { id: user.id });
114
-
115
- // Background jobs
116
- await ctx.core.jobs.enqueue("sendEmail", { to: user.email });
117
-
118
- // Rate limiting
119
- const result = await ctx.core.rateLimiter.check(`api:${ctx.ip}`, 100, 60000);
120
-
121
- // Real-time updates
122
- ctx.core.sse.broadcast(`user:${userId}`, "notification", { message: "Hello" });
123
- }
124
- ```
125
-
126
- | Service | Purpose |
127
- |---------|---------|
128
- | `logger` | Structured logging with levels |
129
- | `cache` | Key-value store with TTL |
130
- | `events` | Pub/sub event system |
131
- | `jobs` | Background job queue |
132
- | `cron` | Scheduled tasks |
133
- | `sse` | Server-Sent Events |
134
- | `rateLimiter` | Request throttling |
135
-
136
- ## Plugins with Database
137
-
138
- Plugins can define their own database tables with full type safety:
139
-
140
- ```ts
141
- // plugins/users/index.ts
142
- import { createPlugin } from "@donkeylabs/server";
143
- import type { DB as UsersSchema } from "./schema";
144
-
145
- export const usersPlugin = createPlugin
146
- .withSchema<UsersSchema>()
147
- .define({
148
- name: "users",
149
- service: async (ctx) => ({
150
- create: async (email: string, name: string) => {
151
- return ctx.db
152
- .insertInto("users")
153
- .values({ email, name })
154
- .returningAll()
155
- .executeTakeFirstOrThrow();
156
- },
157
- findByEmail: (email: string) => {
158
- return ctx.db
159
- .selectFrom("users")
160
- .selectAll()
161
- .where("email", "=", email)
162
- .executeTakeFirst();
163
- },
164
- }),
165
- });
166
- ```
167
-
168
- ## Middleware
169
-
170
- Add authentication, logging, or other cross-cutting concerns:
171
-
172
- ```ts
173
- // In plugin
174
- export const authPlugin = createPlugin.define({
175
- name: "auth",
176
- service: async (ctx) => ({
177
- validateToken: async (token: string) => { /* ... */ },
178
- }),
179
- middleware: (ctx, service) => ({
180
- requireAuth: createMiddleware(async (req, reqCtx, next) => {
181
- const token = req.headers.get("Authorization")?.replace("Bearer ", "");
182
- if (!token) return Response.json({ error: "Unauthorized" }, { status: 401 });
183
-
184
- reqCtx.user = await service.validateToken(token);
185
- return next();
186
- }),
187
- }),
188
- });
189
-
190
- // In routes
191
- server.router("api")
192
- .middleware.requireAuth()
193
- .route("protected")
194
- .typed({ handle: (input, ctx) => ({ user: ctx.user }) });
195
- ```
196
-
197
- ## CLI Commands
198
-
199
- ```sh
200
- donkeylabs # Interactive menu
201
- donkeylabs init # Create new project
202
- donkeylabs generate # Generate types
203
- donkeylabs plugin # Create new plugin
204
- donkeylabs dev # Dev server with hot reload
205
- ```
206
-
207
- ## Client Generation
208
-
209
- Generate a fully-typed API client for your frontend:
210
-
211
- ```sh
212
- bun run gen:client --output ./frontend/src/lib/api.ts
213
- ```
214
-
215
- ```ts
216
- // Frontend usage
217
- import { createClient } from "./lib/api";
218
-
219
- const api = createClient({ baseUrl: "http://localhost:3000" });
220
-
221
- const result = await api.greet({ name: "World" });
222
- // result is typed as { message: string }
223
- ```
224
-
225
- ## Testing
226
-
227
- ```ts
228
- import { createTestHarness } from "@donkeylabs/server/harness";
229
- import { myPlugin } from "./plugins/myPlugin";
230
-
231
- const { manager, db, core } = await createTestHarness(myPlugin);
232
-
233
- // Test with real in-memory SQLite + all core services
234
- const service = manager.getServices().myPlugin;
235
- expect(service.greet("Test")).toBe("Hello, Test!");
236
- ```
237
-
238
- ## Documentation
239
-
240
- - [Plugins](docs/plugins.md) - Creating plugins with schemas and dependencies
241
- - [Router](docs/router.md) - Defining routes and handlers
242
- - [Middleware](docs/middleware.md) - Authentication and cross-cutting concerns
243
- - [Core Services](docs/core-services.md) - Logger, Cache, Events, Jobs, Cron, SSE
244
- - [Testing](docs/testing.md) - Test harness and patterns
245
- - [API Client](docs/api-client.md) - Client generation and usage
246
-
247
- ## Requirements
248
-
249
- - [Bun](https://bun.sh) v1.0+
250
- - TypeScript 5+
251
-
252
- ## License
253
-
254
- MIT
@@ -1,134 +0,0 @@
1
- /**
2
- * Dev Command
3
- *
4
- * Start development server with auto-regeneration on file changes
5
- *
6
- * Usage:
7
- * donkeylabs dev
8
- */
9
-
10
- import { watch } from "node:fs";
11
- import { join } from "node:path";
12
- import { existsSync } from "node:fs";
13
- import { spawn, type Subprocess } from "bun";
14
- import pc from "picocolors";
15
-
16
- let generateTimeout: Timer | null = null;
17
- let serverProcess: Subprocess | null = null;
18
-
19
- export async function devCommand(_args: string[]): Promise<void> {
20
- console.log(pc.magenta(pc.bold("\n @donkeylabs/server dev\n")));
21
-
22
- const cwd = process.cwd();
23
- const configPath = join(cwd, "donkeylabs.config.ts");
24
-
25
- if (!existsSync(configPath)) {
26
- console.error(pc.red("donkeylabs.config.ts not found. Run 'donkeylabs init' first."));
27
- process.exit(1);
28
- }
29
-
30
- // Initial generate
31
- console.log(pc.dim("Running initial type generation..."));
32
- await runGenerate();
33
-
34
- // Start the server
35
- startServer();
36
-
37
- // Watch directories
38
- const watchPaths = [
39
- join(cwd, "src/routes"),
40
- join(cwd, "src/plugins"),
41
- ];
42
-
43
- console.log(pc.cyan("\nWatching for changes..."));
44
- console.log(pc.gray(" - src/routes/"));
45
- console.log(pc.gray(" - src/plugins/"));
46
- console.log(pc.gray("\nPress Ctrl+C to stop.\n"));
47
-
48
- for (const watchPath of watchPaths) {
49
- if (!existsSync(watchPath)) continue;
50
-
51
- watchRecursive(watchPath, (eventType, filename) => {
52
- if (!filename) return;
53
- if (filename.includes(".@donkeylabs") || filename.includes("node_modules")) return;
54
- if (!filename.endsWith(".ts")) return;
55
-
56
- if (generateTimeout) clearTimeout(generateTimeout);
57
-
58
- generateTimeout = setTimeout(async () => {
59
- console.log(pc.dim(`\n[${filename}]`));
60
- await runGenerate();
61
- }, 300);
62
- });
63
- }
64
-
65
- // Keep process alive
66
- await new Promise(() => {});
67
- }
68
-
69
- async function runGenerate(): Promise<void> {
70
- try {
71
- const { generateCommand } = await import("./generate");
72
- await generateCommand([]);
73
- } catch (error: any) {
74
- console.error(pc.red("Generate failed:"), error.message);
75
- }
76
- }
77
-
78
- function startServer(): void {
79
- const cwd = process.cwd();
80
- const entryPoint = join(cwd, "src/index.ts");
81
-
82
- if (!existsSync(entryPoint)) {
83
- console.log(pc.yellow("No src/index.ts found, skipping server start."));
84
- return;
85
- }
86
-
87
- console.log(pc.green("Starting server..."));
88
-
89
- serverProcess = spawn({
90
- cmd: ["bun", "--watch", entryPoint],
91
- cwd,
92
- stdout: "inherit",
93
- stderr: "inherit",
94
- env: { ...process.env, NODE_ENV: "development" },
95
- });
96
- }
97
-
98
- function restartServer(): void {
99
- if (serverProcess) {
100
- console.log(pc.yellow("Restarting server..."));
101
- serverProcess.kill();
102
- }
103
- startServer();
104
- }
105
-
106
- function watchRecursive(
107
- dir: string,
108
- callback: (eventType: string, filename: string | null) => void
109
- ): void {
110
- try {
111
- watch(dir, { recursive: true }, (eventType, filename) => {
112
- callback(eventType, filename);
113
- });
114
- } catch (error: any) {
115
- // Fallback for systems that don't support recursive watch
116
- console.log(pc.yellow(`Warning: Could not watch ${dir} recursively`));
117
- }
118
- }
119
-
120
- // Handle graceful shutdown
121
- process.on("SIGINT", () => {
122
- console.log(pc.gray("\n\nShutting down..."));
123
- if (serverProcess) {
124
- serverProcess.kill();
125
- }
126
- process.exit(0);
127
- });
128
-
129
- process.on("SIGTERM", () => {
130
- if (serverProcess) {
131
- serverProcess.kill();
132
- }
133
- process.exit(0);
134
- });