@donkeylabs/server 0.1.0 → 0.1.2

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.
@@ -0,0 +1,524 @@
1
+ # Plugins
2
+
3
+ Plugins are the core building blocks of this framework. Each plugin encapsulates database schema, business logic, custom handlers, and middleware into a self-contained module.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Creating a Plugin](#creating-a-plugin)
8
+ - [Plugin with Database Schema](#plugin-with-database-schema)
9
+ - [Plugin with Configuration](#plugin-with-configuration)
10
+ - [Plugin Dependencies](#plugin-dependencies)
11
+ - [Custom Handlers](#custom-handlers)
12
+ - [Custom Middleware](#custom-middleware)
13
+ - [Plugin Context](#plugin-context)
14
+ - [Type Inference](#type-inference)
15
+ - [Plugin Lifecycle](#plugin-lifecycle)
16
+
17
+ ---
18
+
19
+ ## Creating a Plugin
20
+
21
+ The simplest plugin exports a service that becomes available to all route handlers:
22
+
23
+ ```ts
24
+ // plugins/greeter/index.ts
25
+ import { createPlugin } from "../../core";
26
+
27
+ export const greeterPlugin = createPlugin.define({
28
+ name: "greeter",
29
+ service: async (ctx) => {
30
+ // Return the service object
31
+ return {
32
+ sayHello: (name: string) => `Hello, ${name}!`,
33
+ sayGoodbye: (name: string) => `Goodbye, ${name}!`,
34
+ };
35
+ },
36
+ });
37
+ ```
38
+
39
+ **Usage in routes:**
40
+
41
+ ```ts
42
+ router.route("greet").typed({
43
+ input: z.object({ name: z.string() }),
44
+ handle: async (input, ctx) => {
45
+ // Access via ctx.plugins.<pluginName>
46
+ return { message: ctx.plugins.greeter.sayHello(input.name) };
47
+ },
48
+ });
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Plugin with Database Schema
54
+
55
+ Plugins can define their own database tables. The framework automatically merges all plugin schemas for full type safety.
56
+
57
+ ### Step 1: Create Migration
58
+
59
+ ```ts
60
+ // plugins/users/migrations/001_create_users.ts
61
+ import type { Kysely } from "kysely";
62
+
63
+ export async function up(db: Kysely<any>) {
64
+ await db.schema
65
+ .createTable("users")
66
+ .ifNotExists()
67
+ .addColumn("id", "integer", (c) => c.primaryKey().autoIncrement())
68
+ .addColumn("email", "text", (c) => c.notNull().unique())
69
+ .addColumn("name", "text", (c) => c.notNull())
70
+ .addColumn("created_at", "text", (c) => c.defaultTo("CURRENT_TIMESTAMP"))
71
+ .execute();
72
+ }
73
+
74
+ export async function down(db: Kysely<any>) {
75
+ await db.schema.dropTable("users").execute();
76
+ }
77
+ ```
78
+
79
+ ### Step 2: Generate Schema Types
80
+
81
+ ```sh
82
+ bun scripts/generate-types.ts users
83
+ ```
84
+
85
+ This creates `plugins/users/schema.ts` with Kysely types.
86
+
87
+ ### Step 3: Define Plugin with Schema
88
+
89
+ ```ts
90
+ // plugins/users/index.ts
91
+ import { createPlugin } from "../../core";
92
+ import type { DB as UsersSchema } from "./schema";
93
+
94
+ export const usersPlugin = createPlugin
95
+ .withSchema<UsersSchema>() // Enable typed database access
96
+ .define({
97
+ name: "users",
98
+ service: async (ctx) => {
99
+ return {
100
+ async create(email: string, name: string) {
101
+ // ctx.db is fully typed with UsersSchema!
102
+ const result = await ctx.db
103
+ .insertInto("users")
104
+ .values({ email, name })
105
+ .returning(["id", "email", "name"])
106
+ .executeTakeFirstOrThrow();
107
+ return result;
108
+ },
109
+
110
+ async findByEmail(email: string) {
111
+ return ctx.db
112
+ .selectFrom("users")
113
+ .selectAll()
114
+ .where("email", "=", email)
115
+ .executeTakeFirst();
116
+ },
117
+
118
+ async list() {
119
+ return ctx.db.selectFrom("users").selectAll().execute();
120
+ },
121
+ };
122
+ },
123
+ });
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Plugin with Configuration
129
+
130
+ Plugins can accept configuration at registration time using the factory pattern:
131
+
132
+ ```ts
133
+ // plugins/email/index.ts
134
+ import { createPlugin } from "../../core";
135
+
136
+ export interface EmailConfig {
137
+ apiKey: string;
138
+ fromAddress: string;
139
+ sandbox?: boolean;
140
+ }
141
+
142
+ export const emailPlugin = createPlugin
143
+ .withConfig<EmailConfig>() // Declare config type
144
+ .define({
145
+ name: "email",
146
+ service: async (ctx) => {
147
+ // Access config via ctx.config
148
+ const { apiKey, fromAddress, sandbox } = ctx.config;
149
+
150
+ return {
151
+ async send(to: string, subject: string, body: string) {
152
+ if (sandbox) {
153
+ console.log(`[Sandbox] Would send to ${to}: ${subject}`);
154
+ return { id: "sandbox-" + Date.now() };
155
+ }
156
+
157
+ // Real implementation using apiKey
158
+ const response = await fetch("https://api.email.com/send", {
159
+ method: "POST",
160
+ headers: { Authorization: `Bearer ${apiKey}` },
161
+ body: JSON.stringify({ from: fromAddress, to, subject, body }),
162
+ });
163
+
164
+ return response.json();
165
+ },
166
+
167
+ getConfig() {
168
+ return { fromAddress, sandbox };
169
+ },
170
+ };
171
+ },
172
+ });
173
+ ```
174
+
175
+ **Registering with config:**
176
+
177
+ ```ts
178
+ // The plugin becomes a factory function when using withConfig
179
+ server.registerPlugin(
180
+ emailPlugin({
181
+ apiKey: process.env.EMAIL_API_KEY!,
182
+ fromAddress: "noreply@example.com",
183
+ sandbox: process.env.NODE_ENV !== "production",
184
+ })
185
+ );
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Plugin Dependencies
191
+
192
+ Plugins can depend on other plugins. Dependencies are resolved automatically in topological order.
193
+
194
+ ```ts
195
+ // plugins/notifications/index.ts
196
+ import { createPlugin } from "../../core";
197
+
198
+ export const notificationsPlugin = createPlugin.define({
199
+ name: "notifications",
200
+ dependencies: ["users", "email"] as const, // Declare dependencies
201
+ service: async (ctx) => {
202
+ // Access dependency services via ctx.deps
203
+ const { users, email } = ctx.deps;
204
+
205
+ return {
206
+ async notifyUser(userId: number, message: string) {
207
+ // Use the users plugin service
208
+ const user = await users.findById(userId);
209
+ if (!user) throw new Error("User not found");
210
+
211
+ // Use the email plugin service
212
+ await email.send(user.email, "Notification", message);
213
+
214
+ // Log using core services
215
+ ctx.core.logger.info("Notification sent", { userId, message });
216
+ },
217
+ };
218
+ },
219
+ });
220
+ ```
221
+
222
+ ### Dependency Rules
223
+
224
+ 1. **Dependencies must be registered first** - The framework validates this at startup
225
+ 2. **No circular dependencies** - TypeScript will show an error at compile time
226
+ 3. **No self-dependencies** - Compile-time error if plugin depends on itself
227
+
228
+ ```ts
229
+ // This will cause a TypeScript error:
230
+ createPlugin.define({
231
+ name: "foo",
232
+ dependencies: ["foo"] as const, // Error: Plugin 'foo' cannot depend on itself
233
+ // ...
234
+ });
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Custom Handlers
240
+
241
+ Plugins can define custom request handlers for specialized processing:
242
+
243
+ ```ts
244
+ // plugins/api/index.ts
245
+ import { createPlugin } from "../../core";
246
+ import { createHandler } from "../../handlers";
247
+ import type { ServerContext } from "../../router";
248
+
249
+ // Define handler signature
250
+ type XMLHandler = (xmlBody: string, ctx: ServerContext) => Promise<string>;
251
+
252
+ // Create handler implementation
253
+ const XMLRequestHandler = createHandler<XMLHandler>(
254
+ async (req, def, handle, ctx) => {
255
+ const body = await req.text();
256
+
257
+ // Validate XML content type
258
+ if (!req.headers.get("content-type")?.includes("xml")) {
259
+ return new Response("Content-Type must be application/xml", { status: 400 });
260
+ }
261
+
262
+ const result = await handle(body, ctx);
263
+
264
+ return new Response(result, {
265
+ headers: { "Content-Type": "application/xml" },
266
+ });
267
+ }
268
+ );
269
+
270
+ export const apiPlugin = createPlugin.define({
271
+ name: "api",
272
+ handlers: {
273
+ xml: XMLRequestHandler, // Register handler
274
+ },
275
+ service: async () => ({}),
276
+ });
277
+ ```
278
+
279
+ **After adding handlers, regenerate registry:**
280
+
281
+ ```sh
282
+ bun run gen:registry
283
+ ```
284
+
285
+ **Using custom handler in routes:**
286
+
287
+ ```ts
288
+ router.route("process").xml({
289
+ handle: async (xmlBody, ctx) => {
290
+ // Process XML and return XML response
291
+ return `<response><status>ok</status></response>`;
292
+ },
293
+ });
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Custom Middleware
299
+
300
+ Plugins can provide middleware that can be applied to routes:
301
+
302
+ ```ts
303
+ // plugins/auth/index.ts
304
+ import { createPlugin } from "../../core";
305
+ import { createMiddleware } from "../../middleware";
306
+
307
+ export interface AuthRequiredConfig {
308
+ roles?: string[];
309
+ }
310
+
311
+ const AuthRequiredMiddleware = createMiddleware<AuthRequiredConfig>(
312
+ async (req, ctx, next, config) => {
313
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "");
314
+
315
+ if (!token) {
316
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
317
+ status: 401,
318
+ headers: { "Content-Type": "application/json" },
319
+ });
320
+ }
321
+
322
+ // Verify token and set user
323
+ const user = await verifyToken(token);
324
+ ctx.user = user;
325
+
326
+ // Check roles if specified
327
+ if (config?.roles && !config.roles.some((r) => user.roles.includes(r))) {
328
+ return new Response(JSON.stringify({ error: "Forbidden" }), {
329
+ status: 403,
330
+ headers: { "Content-Type": "application/json" },
331
+ });
332
+ }
333
+
334
+ return next();
335
+ }
336
+ );
337
+
338
+ export const authPlugin = createPlugin.define({
339
+ name: "auth",
340
+ middleware: {
341
+ authRequired: AuthRequiredMiddleware,
342
+ },
343
+ service: async (ctx) => ({
344
+ // Auth service methods...
345
+ }),
346
+ });
347
+ ```
348
+
349
+ **Using middleware in routes:**
350
+
351
+ ```ts
352
+ // After running bun run gen:registry, middleware methods appear on router.middleware
353
+
354
+ router.middleware
355
+ .authRequired({ roles: ["admin"] })
356
+ .route("admin-only")
357
+ .typed({
358
+ handle: async (input, ctx) => {
359
+ // ctx.user is guaranteed to be set and have admin role
360
+ return { user: ctx.user };
361
+ },
362
+ });
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Plugin Context
368
+
369
+ The `PluginContext` passed to the service function provides:
370
+
371
+ ```ts
372
+ interface PluginContext<Deps, Schema, Config> {
373
+ // Core services (always available)
374
+ core: {
375
+ db: Kysely<any>;
376
+ config: Record<string, any>;
377
+ logger: Logger;
378
+ cache: Cache;
379
+ events: Events;
380
+ cron: Cron;
381
+ jobs: Jobs;
382
+ sse: SSE;
383
+ rateLimiter: RateLimiter;
384
+ };
385
+
386
+ // Typed database (if using withSchema)
387
+ db: Kysely<Schema>;
388
+
389
+ // Dependency services (based on dependencies array)
390
+ deps: Deps;
391
+
392
+ // Plugin configuration (if using withConfig)
393
+ config: Config;
394
+ }
395
+ ```
396
+
397
+ **Example using all context features:**
398
+
399
+ ```ts
400
+ export const analyticsPlugin = createPlugin
401
+ .withSchema<AnalyticsSchema>()
402
+ .withConfig<{ trackingId: string }>()
403
+ .define({
404
+ name: "analytics",
405
+ dependencies: ["users"] as const,
406
+ service: async (ctx) => {
407
+ // Schedule daily report
408
+ ctx.core.cron.schedule("0 0 * * *", async () => {
409
+ ctx.core.logger.info("Running daily analytics");
410
+ // ...
411
+ });
412
+
413
+ // Listen for events
414
+ ctx.core.events.on("user.created", async (data) => {
415
+ await ctx.db.insertInto("events").values({
416
+ type: "user_created",
417
+ data: JSON.stringify(data),
418
+ }).execute();
419
+ });
420
+
421
+ return {
422
+ track(event: string, data: any) {
423
+ ctx.core.events.emit(`analytics.${event}`, {
424
+ trackingId: ctx.config.trackingId,
425
+ ...data,
426
+ });
427
+ },
428
+ };
429
+ },
430
+ });
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Type Inference
436
+
437
+ The framework provides helpers to extract types from plugins:
438
+
439
+ ```ts
440
+ import {
441
+ InferService,
442
+ InferSchema,
443
+ InferHandlers,
444
+ InferMiddleware,
445
+ InferDependencies,
446
+ InferConfig,
447
+ } from "./core";
448
+
449
+ import { usersPlugin } from "./plugins/users";
450
+
451
+ // Extract types
452
+ type UsersService = InferService<typeof usersPlugin>;
453
+ type UsersSchema = InferSchema<typeof usersPlugin>;
454
+ type UsersDeps = InferDependencies<typeof usersPlugin>;
455
+
456
+ // Use in your code
457
+ function processUser(service: UsersService) {
458
+ return service.findByEmail("test@example.com");
459
+ }
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Plugin Lifecycle
465
+
466
+ 1. **Registration** - `server.registerPlugin(plugin)` adds plugin to manager
467
+ 2. **Validation** - At startup, dependencies are validated
468
+ 3. **Migration** - `await manager.migrate()` runs all plugin migrations in order
469
+ 4. **Initialization** - `await manager.init()` calls each plugin's `service()` function
470
+ 5. **Runtime** - Services available via `ctx.plugins` in route handlers
471
+
472
+ ```
473
+ Registration → Validation → Migration → Initialization → Runtime
474
+ ↓ ↓ ↓ ↓ ↓
475
+ register() check deps run SQL service() ctx.plugins
476
+ ```
477
+
478
+ ---
479
+
480
+ ## Best Practices
481
+
482
+ ### 1. Keep Plugins Focused
483
+ Each plugin should have a single responsibility:
484
+ - `users` - User management
485
+ - `auth` - Authentication
486
+ - `email` - Email sending
487
+ - `analytics` - Event tracking
488
+
489
+ ### 2. Use Dependencies for Composition
490
+ Instead of importing other plugins directly, declare them as dependencies:
491
+
492
+ ```ts
493
+ // Good - explicit dependency
494
+ dependencies: ["users"] as const,
495
+ service: (ctx) => {
496
+ ctx.deps.users.findById(1);
497
+ }
498
+
499
+ // Bad - direct import (bypasses dependency resolution)
500
+ import { usersPlugin } from "../users";
501
+ ```
502
+
503
+ ### 3. Leverage Core Services
504
+ Use built-in core services instead of reinventing:
505
+
506
+ ```ts
507
+ // Good - use built-in cache
508
+ const user = await ctx.core.cache.getOrSet(`user:${id}`, () => fetchUser(id));
509
+
510
+ // Bad - manual caching
511
+ const cached = userCache.get(id);
512
+ if (!cached) { ... }
513
+ ```
514
+
515
+ ### 4. Type Everything
516
+ Use `withSchema()` and `withConfig()` for full type safety:
517
+
518
+ ```ts
519
+ // Fully typed plugin
520
+ createPlugin
521
+ .withSchema<MySchema>()
522
+ .withConfig<MyConfig>()
523
+ .define({ ... });
524
+ ```