@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.
- 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 +8 -11
- 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 +566 -0
- package/src/generator/zod-to-ts.ts +114 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +30 -24
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/router.ts +47 -1
- package/src/server.ts +618 -332
- 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/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
|
package/cli/commands/dev.ts
DELETED
|
@@ -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
|
-
});
|