@donkeylabs/server 0.1.3 → 0.1.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/examples/starter/node_modules/@donkeylabs/server/README.md +15 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/generate.ts +461 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/init.ts +476 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/interactive.ts +223 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/commands/plugin.ts +192 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/donkeylabs +106 -0
- package/examples/starter/node_modules/@donkeylabs/server/cli/index.ts +100 -0
- package/examples/starter/node_modules/@donkeylabs/server/context.d.ts +17 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/api-client.md +520 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cache.md +437 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cli.md +353 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/core-services.md +338 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/cron.md +465 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/errors.md +303 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/events.md +460 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/handlers.md +549 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/jobs.md +556 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/logger.md +316 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/middleware.md +682 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/plugins.md +524 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/project-structure.md +493 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/rate-limiter.md +525 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/router.md +566 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/sse.md +542 -0
- package/examples/starter/node_modules/@donkeylabs/server/docs/svelte-frontend.md +324 -0
- package/examples/starter/node_modules/@donkeylabs/server/mcp/donkeylabs-mcp +3238 -0
- package/examples/starter/node_modules/@donkeylabs/server/mcp/server.ts +3238 -0
- package/examples/starter/node_modules/@donkeylabs/server/package.json +77 -0
- package/examples/starter/node_modules/@donkeylabs/server/registry.d.ts +11 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/client/base.ts +481 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/client/index.ts +150 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/cache.ts +183 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/cron.ts +255 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/errors.ts +320 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/events.ts +163 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/index.ts +94 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/jobs.ts +334 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/logger.ts +131 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/rate-limiter.ts +193 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core/sse.ts +210 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/core.ts +428 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/handlers.ts +87 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/harness.ts +70 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/index.ts +38 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/middleware.ts +34 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/registry.ts +13 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/router.ts +155 -0
- package/examples/starter/node_modules/@donkeylabs/server/src/server.ts +234 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/init/donkeylabs.config.ts.template +14 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/init/index.ts.template +41 -0
- package/examples/starter/node_modules/@donkeylabs/server/templates/plugin/index.ts.template +25 -0
- package/examples/starter/src/routes/health/ping/models/model.ts +11 -7
- package/package.json +3 -3
- package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-native-jsx.d.ts +0 -32
- package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-shims-v4.d.ts +0 -290
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { RouteDefinition, ServerContext } from "./router";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export interface HandlerRuntime<Fn extends Function = Function> {
|
|
5
|
+
execute(
|
|
6
|
+
req: Request,
|
|
7
|
+
def: RouteDefinition<any, any>,
|
|
8
|
+
userHandle: Fn,
|
|
9
|
+
ctx: ServerContext
|
|
10
|
+
): Promise<Response>;
|
|
11
|
+
|
|
12
|
+
readonly __signature: Fn; // Required for type inference
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Factory function to create custom handlers without manual __signature.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* type EchoFn = (body: any, ctx: ServerContext) => Promise<{ echo: any }>;
|
|
20
|
+
*
|
|
21
|
+
* export const EchoHandler = createHandler<EchoFn>(async (req, def, handle, ctx) => {
|
|
22
|
+
* const body = await req.json();
|
|
23
|
+
* const result = await handle(body, ctx);
|
|
24
|
+
* return Response.json(result);
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export function createHandler<Fn extends Function>(
|
|
28
|
+
execute: (
|
|
29
|
+
req: Request,
|
|
30
|
+
def: RouteDefinition<any, any>,
|
|
31
|
+
handle: Fn,
|
|
32
|
+
ctx: ServerContext
|
|
33
|
+
) => Promise<Response>
|
|
34
|
+
): HandlerRuntime<Fn> {
|
|
35
|
+
return {
|
|
36
|
+
execute,
|
|
37
|
+
__signature: undefined as unknown as Fn
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type TypedFn = (input: any, ctx: ServerContext) => Promise<any> | any;
|
|
42
|
+
export type TypedHandler = HandlerRuntime<TypedFn>;
|
|
43
|
+
|
|
44
|
+
export const TypedHandler: TypedHandler = {
|
|
45
|
+
async execute(req, def, handle, ctx) {
|
|
46
|
+
if (req.method !== "POST") {
|
|
47
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let body: unknown = {};
|
|
51
|
+
try {
|
|
52
|
+
body = await req.json();
|
|
53
|
+
} catch {
|
|
54
|
+
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const input = def.input ? def.input.parse(body) : body;
|
|
59
|
+
const result = await handle(input, ctx);
|
|
60
|
+
const output = def.output ? def.output.parse(result) : result;
|
|
61
|
+
return Response.json(output);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(e);
|
|
64
|
+
if (e instanceof z.ZodError) {
|
|
65
|
+
return Response.json({ error: "Validation Failed", details: e.issues }, { status: 400 });
|
|
66
|
+
}
|
|
67
|
+
const message = e instanceof Error ? e.message : "Internal Error";
|
|
68
|
+
return Response.json({ error: message }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
__signature: undefined as unknown as TypedFn
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type RawFn = (req: Request, ctx: ServerContext) => Promise<Response> | Response;
|
|
75
|
+
export type RawHandler = HandlerRuntime<RawFn>;
|
|
76
|
+
|
|
77
|
+
export const RawHandler: RawHandler = {
|
|
78
|
+
async execute(req, def, handle, ctx) {
|
|
79
|
+
return await handle(req, ctx);
|
|
80
|
+
},
|
|
81
|
+
__signature: undefined as unknown as RawFn
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Handlers = {
|
|
85
|
+
typed: TypedHandler,
|
|
86
|
+
raw: RawHandler
|
|
87
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
import { PluginManager, type Plugin, type CoreServices } from "./core";
|
|
5
|
+
import {
|
|
6
|
+
createLogger,
|
|
7
|
+
createCache,
|
|
8
|
+
createEvents,
|
|
9
|
+
createCron,
|
|
10
|
+
createJobs,
|
|
11
|
+
createSSE,
|
|
12
|
+
createRateLimiter,
|
|
13
|
+
createErrors,
|
|
14
|
+
} from "./core/index";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a fully functional (in-memory) testing environment for a plugin.
|
|
18
|
+
*
|
|
19
|
+
* @param targetPlugin The plugin you want to test.
|
|
20
|
+
* @param dependencies Any other plugins this plugin needs (e.g. Auth).
|
|
21
|
+
*/
|
|
22
|
+
export async function createTestHarness(targetPlugin: Plugin, dependencies: Plugin[] = []) {
|
|
23
|
+
// 1. Setup In-Memory DB
|
|
24
|
+
const db = new Kysely<any>({
|
|
25
|
+
dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 2. Initialize Core Services
|
|
29
|
+
const logger = createLogger({ level: "warn" }); // Less verbose in tests
|
|
30
|
+
const cache = createCache();
|
|
31
|
+
const events = createEvents();
|
|
32
|
+
const cron = createCron();
|
|
33
|
+
const jobs = createJobs({ events });
|
|
34
|
+
const sse = createSSE();
|
|
35
|
+
const rateLimiter = createRateLimiter();
|
|
36
|
+
const errors = createErrors();
|
|
37
|
+
|
|
38
|
+
const core: CoreServices = {
|
|
39
|
+
db,
|
|
40
|
+
config: { env: "test" },
|
|
41
|
+
logger,
|
|
42
|
+
cache,
|
|
43
|
+
events,
|
|
44
|
+
cron,
|
|
45
|
+
jobs,
|
|
46
|
+
sse,
|
|
47
|
+
rateLimiter,
|
|
48
|
+
errors,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const manager = new PluginManager(core);
|
|
52
|
+
|
|
53
|
+
// 3. Register Deps + Target
|
|
54
|
+
for (const dep of dependencies) {
|
|
55
|
+
manager.register(dep);
|
|
56
|
+
}
|
|
57
|
+
manager.register(targetPlugin);
|
|
58
|
+
|
|
59
|
+
// 4. Run Migrations (Real Kysely Migrations!)
|
|
60
|
+
await manager.migrate();
|
|
61
|
+
|
|
62
|
+
// 5. Init Plugins
|
|
63
|
+
await manager.init();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
manager,
|
|
67
|
+
db,
|
|
68
|
+
core
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @donkeylabs/server - Main exports
|
|
2
|
+
|
|
3
|
+
// Server
|
|
4
|
+
export { AppServer, type ServerConfig } from "./server";
|
|
5
|
+
|
|
6
|
+
// Router
|
|
7
|
+
export { createRouter, type Router, type RouteBuilder } from "./router";
|
|
8
|
+
|
|
9
|
+
// Handlers
|
|
10
|
+
export { createHandler, TypedHandler, RawHandler } from "./handlers";
|
|
11
|
+
|
|
12
|
+
// Core Plugin System
|
|
13
|
+
export {
|
|
14
|
+
createPlugin,
|
|
15
|
+
PluginManager,
|
|
16
|
+
PluginContext,
|
|
17
|
+
type PluginRegistry,
|
|
18
|
+
type PluginHandlerRegistry,
|
|
19
|
+
type PluginMiddlewareRegistry,
|
|
20
|
+
type CoreServices,
|
|
21
|
+
type Register,
|
|
22
|
+
} from "./core";
|
|
23
|
+
|
|
24
|
+
// Middleware
|
|
25
|
+
export { createMiddleware } from "./middleware";
|
|
26
|
+
|
|
27
|
+
// Config helper
|
|
28
|
+
export function defineConfig(config: {
|
|
29
|
+
plugins: string[];
|
|
30
|
+
outDir: string;
|
|
31
|
+
routes?: string;
|
|
32
|
+
client?: { output: string };
|
|
33
|
+
}) {
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Re-export HttpError for custom error creation
|
|
38
|
+
export { HttpError } from "./core/errors";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ServerContext } from "./router";
|
|
2
|
+
|
|
3
|
+
// The next function - calls the next middleware or handler
|
|
4
|
+
export type NextFn = () => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
// Middleware function signature (what plugins implement)
|
|
7
|
+
export type MiddlewareFn<TConfig = void> = (
|
|
8
|
+
req: Request,
|
|
9
|
+
ctx: ServerContext,
|
|
10
|
+
next: NextFn,
|
|
11
|
+
config: TConfig
|
|
12
|
+
) => Promise<Response>;
|
|
13
|
+
|
|
14
|
+
// Runtime middleware structure (mirrors HandlerRuntime pattern)
|
|
15
|
+
export interface MiddlewareRuntime<TConfig = void> {
|
|
16
|
+
execute: MiddlewareFn<TConfig>;
|
|
17
|
+
readonly __config: TConfig; // Phantom type for config inference
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Factory to create middleware (mirrors createHandler pattern)
|
|
21
|
+
export function createMiddleware<TConfig = void>(
|
|
22
|
+
execute: MiddlewareFn<TConfig>
|
|
23
|
+
): MiddlewareRuntime<TConfig> {
|
|
24
|
+
return {
|
|
25
|
+
execute,
|
|
26
|
+
__config: undefined as unknown as TConfig,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Stored middleware definition (attached to routes)
|
|
31
|
+
export interface MiddlewareDefinition {
|
|
32
|
+
name: string;
|
|
33
|
+
config: any;
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Auto-generated by scripts/generate-registry.ts
|
|
2
|
+
// Runtime registration of custom handler and middleware methods
|
|
3
|
+
/// <reference path="../registry.d.ts" />
|
|
4
|
+
import { RouteBuilder, MiddlewareBuilder } from "./router";
|
|
5
|
+
|
|
6
|
+
// Handler registrations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// Middleware registrations
|
|
10
|
+
// Register "authRequired" middleware method from authPlugin
|
|
11
|
+
(MiddlewareBuilder.prototype as any).authRequired = function(config: any = {}) {
|
|
12
|
+
return this.addMiddleware("authRequired", config);
|
|
13
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/// <reference path="../registry.d.ts" />
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { GlobalContext } from "../context";
|
|
4
|
+
import type { PluginHandlerRegistry } from "./core";
|
|
5
|
+
import type { MiddlewareDefinition } from "./middleware";
|
|
6
|
+
|
|
7
|
+
export type ServerContext = GlobalContext;
|
|
8
|
+
|
|
9
|
+
export interface HandlerRegistry extends PluginHandlerRegistry {
|
|
10
|
+
typed: {
|
|
11
|
+
execute(req: Request, def: any, userHandle: Function, ctx: ServerContext): Promise<Response>;
|
|
12
|
+
readonly __signature: (input: any, ctx: ServerContext) => Promise<any> | any;
|
|
13
|
+
};
|
|
14
|
+
raw: {
|
|
15
|
+
execute(req: Request, def: any, userHandle: Function, ctx: ServerContext): Promise<Response>;
|
|
16
|
+
readonly __signature: (req: Request, ctx: ServerContext) => Promise<Response> | Response;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RouteDefinition<
|
|
21
|
+
T extends keyof HandlerRegistry = "typed",
|
|
22
|
+
I = any,
|
|
23
|
+
O = any
|
|
24
|
+
> = {
|
|
25
|
+
name: string;
|
|
26
|
+
handler: T;
|
|
27
|
+
input?: z.ZodType<I>;
|
|
28
|
+
output?: z.ZodType<O>;
|
|
29
|
+
middleware?: MiddlewareDefinition[];
|
|
30
|
+
handle: T extends "typed"
|
|
31
|
+
? (input: I, ctx: ServerContext) => Promise<O> | O
|
|
32
|
+
: T extends "raw"
|
|
33
|
+
? (req: Request, ctx: ServerContext) => Promise<Response> | Response
|
|
34
|
+
: HandlerRegistry[T]["__signature"];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface TypedRouteConfig<I = any, O = any> {
|
|
38
|
+
input?: z.ZodType<I>;
|
|
39
|
+
output?: z.ZodType<O>;
|
|
40
|
+
handle: (input: I, ctx: ServerContext) => Promise<O> | O;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RouteMetadata {
|
|
44
|
+
name: string;
|
|
45
|
+
handler: string;
|
|
46
|
+
inputSchema?: z.ZodType<any>;
|
|
47
|
+
outputSchema?: z.ZodType<any>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RawRouteConfig {
|
|
51
|
+
handle: (req: Request, ctx: ServerContext) => Promise<Response> | Response;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IRouteBuilderBase<TRouter> {
|
|
55
|
+
typed<I, O>(config: TypedRouteConfig<I, O>): TRouter;
|
|
56
|
+
raw(config: RawRouteConfig): TRouter;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IRouteBuilder<TRouter> extends IRouteBuilderBase<TRouter> {}
|
|
60
|
+
|
|
61
|
+
export interface IMiddlewareBuilderBase<TRouter> {
|
|
62
|
+
route(name: string): IRouteBuilder<TRouter>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IMiddlewareBuilder<TRouter> extends IMiddlewareBuilderBase<TRouter> {}
|
|
66
|
+
|
|
67
|
+
export class MiddlewareBuilder<TRouter extends Router> implements IMiddlewareBuilderBase<TRouter> {
|
|
68
|
+
private _middleware: MiddlewareDefinition[] = [];
|
|
69
|
+
|
|
70
|
+
constructor(private router: TRouter) {}
|
|
71
|
+
|
|
72
|
+
route(name: string): IRouteBuilder<TRouter> {
|
|
73
|
+
return new RouteBuilder(this.router, name, this._middleware) as unknown as IRouteBuilder<TRouter>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addMiddleware(name: string, config: any): this {
|
|
77
|
+
this._middleware.push({ name, config });
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class RouteBuilder<TRouter extends Router> implements IRouteBuilderBase<TRouter> {
|
|
83
|
+
constructor(
|
|
84
|
+
private router: TRouter,
|
|
85
|
+
private name: string,
|
|
86
|
+
private _middleware: MiddlewareDefinition[] = []
|
|
87
|
+
) {}
|
|
88
|
+
|
|
89
|
+
typed<I, O>(config: TypedRouteConfig<I, O>): TRouter {
|
|
90
|
+
return this.router.addRoute(this.name, "typed", config, this._middleware);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
raw(config: RawRouteConfig): TRouter {
|
|
94
|
+
return this.router.addRoute(this.name, "raw", config, this._middleware);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
addHandler(handler: string, config: any): TRouter {
|
|
98
|
+
return this.router.addRoute(this.name, handler, config, this._middleware);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface IRouter {
|
|
103
|
+
route(name: string): IRouteBuilder<this>;
|
|
104
|
+
middleware: IMiddlewareBuilder<this>;
|
|
105
|
+
getRoutes(): RouteDefinition<any, any, any>[];
|
|
106
|
+
getMetadata(): RouteMetadata[];
|
|
107
|
+
getPrefix(): string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class Router implements IRouter {
|
|
111
|
+
private routes: Map<string, RouteDefinition<any, any, any>> = new Map();
|
|
112
|
+
private prefix: string;
|
|
113
|
+
|
|
114
|
+
constructor(prefix: string = "") {
|
|
115
|
+
this.prefix = prefix;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
route(name: string): IRouteBuilder<this> {
|
|
119
|
+
return new RouteBuilder(this, name) as unknown as IRouteBuilder<this>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get middleware(): IMiddlewareBuilder<this> {
|
|
123
|
+
return new MiddlewareBuilder(this) as unknown as IMiddlewareBuilder<this>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
addRoute(name: string, handler: string, config: any, middleware: MiddlewareDefinition[] = []): this {
|
|
127
|
+
const fullName = this.prefix ? `${this.prefix}.${name}` : name;
|
|
128
|
+
this.routes.set(fullName, {
|
|
129
|
+
name: fullName,
|
|
130
|
+
handler,
|
|
131
|
+
middleware: middleware.length > 0 ? middleware : undefined,
|
|
132
|
+
...config
|
|
133
|
+
});
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getRoutes(): RouteDefinition<any, any, any>[] {
|
|
138
|
+
return Array.from(this.routes.values());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getMetadata(): RouteMetadata[] {
|
|
142
|
+
return this.getRoutes().map(route => ({
|
|
143
|
+
name: route.name,
|
|
144
|
+
handler: route.handler,
|
|
145
|
+
inputSchema: route.input,
|
|
146
|
+
outputSchema: route.output,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getPrefix(): string {
|
|
151
|
+
return this.prefix;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const createRouter = (prefix?: string): IRouter => new Router(prefix);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PluginManager, type CoreServices, type ConfiguredPlugin } from "./core";
|
|
3
|
+
import { type IRouter, type RouteDefinition, type ServerContext } from "./router";
|
|
4
|
+
import { Handlers } from "./handlers";
|
|
5
|
+
import type { MiddlewareRuntime, MiddlewareDefinition } from "./middleware";
|
|
6
|
+
import {
|
|
7
|
+
createLogger,
|
|
8
|
+
createCache,
|
|
9
|
+
createEvents,
|
|
10
|
+
createCron,
|
|
11
|
+
createJobs,
|
|
12
|
+
createSSE,
|
|
13
|
+
createRateLimiter,
|
|
14
|
+
createErrors,
|
|
15
|
+
extractClientIP,
|
|
16
|
+
HttpError,
|
|
17
|
+
type LoggerConfig,
|
|
18
|
+
type CacheConfig,
|
|
19
|
+
type EventsConfig,
|
|
20
|
+
type CronConfig,
|
|
21
|
+
type JobsConfig,
|
|
22
|
+
type SSEConfig,
|
|
23
|
+
type RateLimiterConfig,
|
|
24
|
+
type ErrorsConfig,
|
|
25
|
+
} from "./core/index";
|
|
26
|
+
|
|
27
|
+
export interface ServerConfig {
|
|
28
|
+
port?: number;
|
|
29
|
+
db: CoreServices["db"];
|
|
30
|
+
config?: Record<string, any>;
|
|
31
|
+
logger?: LoggerConfig;
|
|
32
|
+
cache?: CacheConfig;
|
|
33
|
+
events?: EventsConfig;
|
|
34
|
+
cron?: CronConfig;
|
|
35
|
+
jobs?: JobsConfig;
|
|
36
|
+
sse?: SSEConfig;
|
|
37
|
+
rateLimiter?: RateLimiterConfig;
|
|
38
|
+
errors?: ErrorsConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class AppServer {
|
|
42
|
+
private port: number;
|
|
43
|
+
private manager: PluginManager;
|
|
44
|
+
private routers: IRouter[] = [];
|
|
45
|
+
private routeMap: Map<string, RouteDefinition> = new Map();
|
|
46
|
+
private coreServices: CoreServices;
|
|
47
|
+
|
|
48
|
+
constructor(options: ServerConfig) {
|
|
49
|
+
this.port = options.port ?? 3000;
|
|
50
|
+
|
|
51
|
+
const logger = createLogger(options.logger);
|
|
52
|
+
const cache = createCache(options.cache);
|
|
53
|
+
const events = createEvents(options.events);
|
|
54
|
+
const cron = createCron(options.cron);
|
|
55
|
+
const jobs = createJobs({ ...options.jobs, events });
|
|
56
|
+
const sse = createSSE(options.sse);
|
|
57
|
+
const rateLimiter = createRateLimiter(options.rateLimiter);
|
|
58
|
+
const errors = createErrors(options.errors);
|
|
59
|
+
|
|
60
|
+
this.coreServices = {
|
|
61
|
+
db: options.db,
|
|
62
|
+
config: options.config ?? {},
|
|
63
|
+
logger,
|
|
64
|
+
cache,
|
|
65
|
+
events,
|
|
66
|
+
cron,
|
|
67
|
+
jobs,
|
|
68
|
+
sse,
|
|
69
|
+
rateLimiter,
|
|
70
|
+
errors,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
this.manager = new PluginManager(this.coreServices);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
registerPlugin(plugin: ConfiguredPlugin): this {
|
|
77
|
+
this.manager.register(plugin);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
use(router: IRouter): this {
|
|
82
|
+
this.routers.push(router);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getServices(): any {
|
|
87
|
+
return this.manager.getServices();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getDb(): CoreServices["db"] {
|
|
91
|
+
return this.manager.getCore().db;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private resolveMiddleware(name: string): MiddlewareRuntime<any> | undefined {
|
|
95
|
+
for (const plugin of this.manager.getPlugins()) {
|
|
96
|
+
if (plugin.middleware && plugin.middleware[name]) {
|
|
97
|
+
return plugin.middleware[name] as MiddlewareRuntime<any>;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async executeMiddlewareChain(
|
|
104
|
+
req: Request,
|
|
105
|
+
ctx: ServerContext,
|
|
106
|
+
stack: MiddlewareDefinition[],
|
|
107
|
+
finalHandler: () => Promise<Response>
|
|
108
|
+
): Promise<Response> {
|
|
109
|
+
let next = finalHandler;
|
|
110
|
+
|
|
111
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
112
|
+
const mwDef = stack[i];
|
|
113
|
+
if (!mwDef) continue;
|
|
114
|
+
|
|
115
|
+
const mwRuntime = this.resolveMiddleware(mwDef.name);
|
|
116
|
+
|
|
117
|
+
if (!mwRuntime) {
|
|
118
|
+
console.warn(`[Server] Middleware '${mwDef.name}' not found, skipping`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const currentNext = next;
|
|
123
|
+
const config = mwDef.config;
|
|
124
|
+
next = () => mwRuntime.execute(req, ctx, currentNext, config);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return next();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getCore(): CoreServices {
|
|
131
|
+
return this.coreServices;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async start(): Promise<void> {
|
|
135
|
+
const { logger } = this.coreServices;
|
|
136
|
+
|
|
137
|
+
await this.manager.migrate();
|
|
138
|
+
await this.manager.init();
|
|
139
|
+
|
|
140
|
+
this.coreServices.cron.start();
|
|
141
|
+
this.coreServices.jobs.start();
|
|
142
|
+
logger.info("Background services started (cron, jobs)");
|
|
143
|
+
|
|
144
|
+
for (const router of this.routers) {
|
|
145
|
+
for (const route of router.getRoutes()) {
|
|
146
|
+
if (this.routeMap.has(route.name)) {
|
|
147
|
+
logger.warn(`Duplicate route detected`, { route: route.name });
|
|
148
|
+
}
|
|
149
|
+
this.routeMap.set(route.name, route);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
logger.info(`Loaded ${this.routeMap.size} RPC routes`);
|
|
153
|
+
|
|
154
|
+
Bun.serve({
|
|
155
|
+
port: this.port,
|
|
156
|
+
fetch: async (req, server) => {
|
|
157
|
+
const url = new URL(req.url);
|
|
158
|
+
const ip = extractClientIP(req, server.requestIP(req)?.address);
|
|
159
|
+
|
|
160
|
+
if (req.method !== "POST") {
|
|
161
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const actionName = url.pathname.slice(1);
|
|
165
|
+
const route = this.routeMap.get(actionName);
|
|
166
|
+
|
|
167
|
+
if (!route) {
|
|
168
|
+
return new Response("Not Found", { status: 404 });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const type = route.handler || "typed";
|
|
172
|
+
let handler = Handlers[type as keyof typeof Handlers];
|
|
173
|
+
|
|
174
|
+
if (!handler) {
|
|
175
|
+
for (const plugin of this.manager.getPlugins()) {
|
|
176
|
+
if (plugin.handlers && plugin.handlers[type]) {
|
|
177
|
+
handler = plugin.handlers[type] as any;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!handler) {
|
|
184
|
+
logger.error("Handler not found", { handler: type, route: actionName });
|
|
185
|
+
return new Response("Handler Not Found", { status: 500 });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ctx: ServerContext = {
|
|
189
|
+
db: this.coreServices.db,
|
|
190
|
+
plugins: this.manager.getServices(),
|
|
191
|
+
core: this.coreServices,
|
|
192
|
+
errors: this.coreServices.errors,
|
|
193
|
+
config: this.coreServices.config,
|
|
194
|
+
ip,
|
|
195
|
+
requestId: crypto.randomUUID(),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const middlewareStack = route.middleware || [];
|
|
199
|
+
const finalHandler = async () => handler.execute(req, route, route.handle, ctx);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (middlewareStack.length > 0) {
|
|
203
|
+
return await this.executeMiddlewareChain(req, ctx, middlewareStack, finalHandler);
|
|
204
|
+
}
|
|
205
|
+
return await finalHandler();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof HttpError) {
|
|
208
|
+
logger.warn("HTTP error thrown", {
|
|
209
|
+
route: actionName,
|
|
210
|
+
status: error.status,
|
|
211
|
+
code: error.code,
|
|
212
|
+
message: error.message,
|
|
213
|
+
});
|
|
214
|
+
return Response.json(error.toJSON(), { status: error.status });
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
logger.info(`Server running at http://localhost:${this.port}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async shutdown(): Promise<void> {
|
|
225
|
+
const { logger } = this.coreServices;
|
|
226
|
+
logger.info("Shutting down server...");
|
|
227
|
+
|
|
228
|
+
this.coreServices.sse.shutdown();
|
|
229
|
+
await this.coreServices.jobs.stop();
|
|
230
|
+
await this.coreServices.cron.stop();
|
|
231
|
+
|
|
232
|
+
logger.info("Server shutdown complete");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from "@donkeylabs/server";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
// Glob patterns for plugin files
|
|
5
|
+
plugins: ["./src/plugins/**/index.ts"],
|
|
6
|
+
|
|
7
|
+
// Generated types output (hidden directory)
|
|
8
|
+
outDir: ".@donkeylabs/server",
|
|
9
|
+
|
|
10
|
+
// Optional: Client generation
|
|
11
|
+
// client: {
|
|
12
|
+
// output: "./src/client/api.ts",
|
|
13
|
+
// },
|
|
14
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Kysely,
|
|
3
|
+
DummyDriver,
|
|
4
|
+
SqliteAdapter,
|
|
5
|
+
SqliteIntrospector,
|
|
6
|
+
SqliteQueryCompiler,
|
|
7
|
+
} from "kysely";
|
|
8
|
+
import { AppServer, createRouter } from "@donkeylabs/server";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// Setup Database (replace with your actual database)
|
|
12
|
+
const db = new Kysely<any>({
|
|
13
|
+
dialect: {
|
|
14
|
+
createAdapter: () => new SqliteAdapter(),
|
|
15
|
+
createDriver: () => new DummyDriver(),
|
|
16
|
+
createIntrospector: (db) => new SqliteIntrospector(db),
|
|
17
|
+
createQueryCompiler: () => new SqliteQueryCompiler(),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Create Server
|
|
22
|
+
const server = new AppServer({
|
|
23
|
+
port: 3000,
|
|
24
|
+
db,
|
|
25
|
+
config: { env: process.env.NODE_ENV || "development" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Define Routes
|
|
29
|
+
const router = createRouter("api")
|
|
30
|
+
.route("hello").typed({
|
|
31
|
+
input: z.object({ name: z.string().optional() }),
|
|
32
|
+
output: z.object({ message: z.string() }),
|
|
33
|
+
handle: async (input, ctx) => {
|
|
34
|
+
return { message: `Hello, ${input.name || "World"}!` };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.use(router);
|
|
39
|
+
|
|
40
|
+
// Start Server
|
|
41
|
+
await server.start();
|