@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.
Files changed (47) 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 +6 -6
  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 +551 -0
  30. package/src/handlers.ts +14 -110
  31. package/src/index.ts +19 -23
  32. package/src/middleware.ts +2 -5
  33. package/src/registry.ts +4 -0
  34. package/src/server.ts +354 -337
  35. package/README.md +0 -254
  36. package/cli/commands/dev.ts +0 -134
  37. package/cli/commands/generate.ts +0 -605
  38. package/cli/commands/init.ts +0 -205
  39. package/cli/commands/interactive.ts +0 -417
  40. package/cli/commands/plugin.ts +0 -192
  41. package/cli/commands/route.ts +0 -195
  42. package/cli/donkeylabs +0 -2
  43. package/cli/index.ts +0 -114
  44. package/docs/svelte-frontend.md +0 -324
  45. package/docs/testing.md +0 -438
  46. package/mcp/donkeylabs-mcp +0 -3238
  47. package/mcp/server.ts +0 -3238
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Generator Building Blocks
3
+ *
4
+ * This module exports reusable functions for generating API clients.
5
+ * Adapters (like @donkeylabs/adapter-sveltekit) can import these functions
6
+ * and compose them with custom options to generate framework-specific clients.
7
+ */
8
+
9
+ // ==========================================
10
+ // Types
11
+ // ==========================================
12
+
13
+ export interface RouteInfo {
14
+ name: string;
15
+ prefix: string;
16
+ routeName: string;
17
+ handler: "typed" | "raw" | string;
18
+ inputSource?: string;
19
+ outputSource?: string;
20
+ }
21
+
22
+ export interface EventInfo {
23
+ name: string;
24
+ plugin: string;
25
+ schemaSource: string;
26
+ }
27
+
28
+ export interface ClientConfigInfo {
29
+ plugin: string;
30
+ credentials?: "include" | "same-origin" | "omit";
31
+ }
32
+
33
+ export interface GeneratorConfig {
34
+ routes: RouteInfo[] | ExtractedRoute[];
35
+ events?: EventInfo[];
36
+ clientConfigs?: ClientConfigInfo[];
37
+ }
38
+
39
+ export interface ExtractedRoute {
40
+ name: string;
41
+ handler: string;
42
+ }
43
+
44
+ export interface ClientGeneratorOptions {
45
+ /** Import statement for base class */
46
+ baseImport: string;
47
+ /** Base class name to extend */
48
+ baseClass: string;
49
+ /** Constructor parameters signature */
50
+ constructorSignature: string;
51
+ /** Constructor body implementation */
52
+ constructorBody: string;
53
+ /** Additional imports to include */
54
+ additionalImports?: string[];
55
+ /** Custom factory function (replaces default) */
56
+ factoryFunction?: string;
57
+ /** Additional class members to include */
58
+ additionalMembers?: string[];
59
+ }
60
+
61
+ /** Default options for standard HTTP-only client */
62
+ export const defaultGeneratorOptions: ClientGeneratorOptions = {
63
+ baseImport: 'import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";',
64
+ baseClass: "ApiClientBase<{}>",
65
+ constructorSignature: "baseUrl: string, options?: ApiClientOptions",
66
+ constructorBody: "super(baseUrl, options);",
67
+ factoryFunction: `export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
68
+ return new ApiClient(baseUrl, options);
69
+ }`,
70
+ };
71
+
72
+ // ==========================================
73
+ // Utility Functions
74
+ // ==========================================
75
+
76
+ export function toPascalCase(str: string): string {
77
+ return str
78
+ .split(/[-_.]/)
79
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
80
+ .join("");
81
+ }
82
+
83
+ export function toCamelCase(str: string): string {
84
+ const pascal = toPascalCase(str);
85
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
86
+ }
87
+
88
+ /**
89
+ * Split string by delimiter, respecting nested brackets
90
+ */
91
+ export function splitTopLevel(source: string, delimiter: string): string[] {
92
+ const result: string[] = [];
93
+ let current = "";
94
+ let depth = 0;
95
+
96
+ for (const char of source) {
97
+ if (char === "(" || char === "[" || char === "{") {
98
+ depth++;
99
+ current += char;
100
+ } else if (char === ")" || char === "]" || char === "}") {
101
+ depth--;
102
+ current += char;
103
+ } else if (char === delimiter && depth === 0) {
104
+ if (current.trim()) {
105
+ result.push(current.trim());
106
+ }
107
+ current = "";
108
+ } else {
109
+ current += char;
110
+ }
111
+ }
112
+
113
+ if (current.trim()) {
114
+ result.push(current.trim());
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Extract content between balanced parentheses starting at a given position
122
+ */
123
+ export function extractParenContent(source: string, startPos: number): string {
124
+ let depth = 0;
125
+ let start = -1;
126
+ let end = -1;
127
+
128
+ for (let i = startPos; i < source.length; i++) {
129
+ if (source[i] === "(") {
130
+ if (depth === 0) start = i;
131
+ depth++;
132
+ } else if (source[i] === ")") {
133
+ depth--;
134
+ if (depth === 0) {
135
+ end = i;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+
141
+ if (start !== -1 && end !== -1) {
142
+ return source.slice(start + 1, end);
143
+ }
144
+ return "";
145
+ }
146
+
147
+ // ==========================================
148
+ // Zod to TypeScript Conversion
149
+ // ==========================================
150
+
151
+ /**
152
+ * Parse object property definitions from Zod source
153
+ */
154
+ function parseObjectProps(
155
+ propsSource: string
156
+ ): { name: string; schema: string; optional: boolean }[] {
157
+ const props: { name: string; schema: string; optional: boolean }[] = [];
158
+ const entries = splitTopLevel(propsSource, ",");
159
+
160
+ for (const entry of entries) {
161
+ const colonIndex = entry.indexOf(":");
162
+ if (colonIndex === -1) continue;
163
+
164
+ const name = entry.slice(0, colonIndex).trim();
165
+ let schema = entry.slice(colonIndex + 1).trim();
166
+
167
+ const optional = schema.endsWith(".optional()");
168
+ if (optional) {
169
+ schema = schema.slice(0, -".optional()".length);
170
+ }
171
+
172
+ props.push({ name, schema, optional });
173
+ }
174
+
175
+ return props;
176
+ }
177
+
178
+ /**
179
+ * Convert Zod schema source to TypeScript type string
180
+ */
181
+ export function zodToTypeScript(zodSource: string | undefined): string {
182
+ if (!zodSource) return "unknown";
183
+
184
+ const typeMap: Record<string, string> = {
185
+ "z.string()": "string",
186
+ "z.number()": "number",
187
+ "z.boolean()": "boolean",
188
+ "z.null()": "null",
189
+ "z.undefined()": "undefined",
190
+ "z.void()": "void",
191
+ "z.any()": "any",
192
+ "z.unknown()": "unknown",
193
+ "z.never()": "never",
194
+ "z.date()": "Date",
195
+ "z.bigint()": "bigint",
196
+ };
197
+
198
+ if (typeMap[zodSource]) return typeMap[zodSource];
199
+
200
+ let source = zodSource;
201
+ let suffix = "";
202
+
203
+ if (source.endsWith(".optional()")) {
204
+ source = source.slice(0, -".optional()".length);
205
+ suffix = " | undefined";
206
+ } else if (source.endsWith(".nullable()")) {
207
+ source = source.slice(0, -".nullable()".length);
208
+ suffix = " | null";
209
+ }
210
+
211
+ // z.object({ ... })
212
+ if (source.startsWith("z.object(")) {
213
+ const innerContent = extractParenContent(source, 8);
214
+ const trimmed = innerContent.trim();
215
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
216
+ const propsSource = trimmed.slice(1, -1).trim();
217
+ const props = parseObjectProps(propsSource);
218
+ const typeProps = props
219
+ .map((p) => ` ${p.name}${p.optional ? "?" : ""}: ${zodToTypeScript(p.schema)};`)
220
+ .join("\n");
221
+ return `{\n${typeProps}\n}${suffix}`;
222
+ }
223
+ }
224
+
225
+ // z.array(...)
226
+ if (source.startsWith("z.array(")) {
227
+ const innerContent = extractParenContent(source, 7);
228
+ if (innerContent) {
229
+ return `${zodToTypeScript(innerContent.trim())}[]${suffix}`;
230
+ }
231
+ }
232
+
233
+ // z.enum([...])
234
+ const enumMatch = source.match(/z\.enum\s*\(\s*\[([^\]]+)\]\s*\)/);
235
+ if (enumMatch?.[1]) {
236
+ const values = enumMatch[1]
237
+ .split(",")
238
+ .map((v) => v.trim())
239
+ .filter(Boolean);
240
+ return values.join(" | ") + suffix;
241
+ }
242
+
243
+ // z.literal(...)
244
+ const literalMatch = source.match(/z\.literal\s*\(\s*([^)]+)\s*\)/);
245
+ if (literalMatch?.[1]) {
246
+ return literalMatch[1].trim() + suffix;
247
+ }
248
+
249
+ // z.union([...])
250
+ const unionMatch = source.match(/z\.union\s*\(\s*\[([^\]]+)\]\s*\)/);
251
+ if (unionMatch?.[1]) {
252
+ const schemas = splitTopLevel(unionMatch[1], ",");
253
+ return schemas.map((s) => zodToTypeScript(s.trim())).join(" | ") + suffix;
254
+ }
255
+
256
+ // z.record(...)
257
+ const recordMatch = source.match(/z\.record\s*\(\s*([^)]+)\s*\)/);
258
+ if (recordMatch?.[1]) {
259
+ const parts = splitTopLevel(recordMatch[1], ",");
260
+ if (parts.length === 2) {
261
+ return `Record<${zodToTypeScript(parts[0]?.trim())}, ${zodToTypeScript(parts[1]?.trim())}>${suffix}`;
262
+ }
263
+ return `Record<string, ${zodToTypeScript(recordMatch[1].trim())}>${suffix}`;
264
+ }
265
+
266
+ // z.tuple([...])
267
+ const tupleMatch = source.match(/z\.tuple\s*\(\s*\[([^\]]+)\]\s*\)/);
268
+ if (tupleMatch?.[1]) {
269
+ const schemas = splitTopLevel(tupleMatch[1], ",");
270
+ return `[${schemas.map((s) => zodToTypeScript(s.trim())).join(", ")}]${suffix}`;
271
+ }
272
+
273
+ // z.string().min/max/email/etc
274
+ if (source.startsWith("z.string()")) return "string" + suffix;
275
+ if (source.startsWith("z.number()")) return "number" + suffix;
276
+
277
+ return "unknown" + suffix;
278
+ }
279
+
280
+ // ==========================================
281
+ // Route Grouping
282
+ // ==========================================
283
+
284
+ /**
285
+ * Group routes by prefix for namespace organization
286
+ */
287
+ export function groupRoutesByPrefix(routes: RouteInfo[]): Map<string, RouteInfo[]> {
288
+ const groups = new Map<string, RouteInfo[]>();
289
+
290
+ for (const route of routes) {
291
+ const prefix = route.prefix || "_root";
292
+ if (!groups.has(prefix)) {
293
+ groups.set(prefix, []);
294
+ }
295
+ groups.get(prefix)!.push(route);
296
+ }
297
+
298
+ return groups;
299
+ }
300
+
301
+ // ==========================================
302
+ // Client Code Generation
303
+ // ==========================================
304
+
305
+ /**
306
+ * Generate client code from extracted routes (simple format)
307
+ * This is used by the CLI command which extracts routes by running the server
308
+ */
309
+ export function generateClientFromRoutes(
310
+ routes: ExtractedRoute[],
311
+ options: Partial<ClientGeneratorOptions> = {}
312
+ ): string {
313
+ const opts = { ...defaultGeneratorOptions, ...options };
314
+
315
+ // Group routes by namespace
316
+ const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
317
+
318
+ for (const route of routes) {
319
+ const parts = route.name.split(".");
320
+ if (parts.length < 2) {
321
+ const ns = "";
322
+ if (!tree.has(ns)) tree.set(ns, new Map());
323
+ const rootMethods = tree.get(ns)!;
324
+ if (!rootMethods.has("")) rootMethods.set("", []);
325
+ rootMethods.get("")!.push({ method: parts[0]!, fullName: route.name });
326
+ } else if (parts.length === 2) {
327
+ const [ns, method] = parts;
328
+ if (!tree.has(ns!)) tree.set(ns!, new Map());
329
+ const nsMethods = tree.get(ns!)!;
330
+ if (!nsMethods.has("")) nsMethods.set("", []);
331
+ nsMethods.get("")!.push({ method: method!, fullName: route.name });
332
+ } else {
333
+ const [ns, sub, ...rest] = parts;
334
+ const method = rest.join(".");
335
+ if (!tree.has(ns!)) tree.set(ns!, new Map());
336
+ const nsMethods = tree.get(ns!)!;
337
+ if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
338
+ nsMethods.get(sub!)!.push({ method: method || sub!, fullName: route.name });
339
+ }
340
+ }
341
+
342
+ // Generate method definitions
343
+ const namespaceBlocks: string[] = [];
344
+
345
+ for (const [namespace, subNamespaces] of tree) {
346
+ if (namespace === "") {
347
+ const rootMethods = subNamespaces.get("");
348
+ if (rootMethods && rootMethods.length > 0) {
349
+ for (const { method, fullName } of rootMethods) {
350
+ const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
351
+ namespaceBlocks.push(` ${methodName} = (input: any) => this.request("${fullName}", input);`);
352
+ }
353
+ }
354
+ continue;
355
+ }
356
+
357
+ const subBlocks: string[] = [];
358
+ for (const [sub, methods] of subNamespaces) {
359
+ if (sub === "") {
360
+ for (const { method, fullName } of methods) {
361
+ const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
362
+ subBlocks.push(` ${methodName}: (input: any) => this.request("${fullName}", input)`);
363
+ }
364
+ } else {
365
+ const subMethods = methods.map(({ method, fullName }) => {
366
+ const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
367
+ return ` ${methodName}: (input: any) => this.request("${fullName}", input)`;
368
+ });
369
+ subBlocks.push(` ${sub}: {\n${subMethods.join(",\n")}\n }`);
370
+ }
371
+ }
372
+
373
+ namespaceBlocks.push(` ${namespace} = {\n${subBlocks.join(",\n")}\n };`);
374
+ }
375
+
376
+ // Build additional imports
377
+ const additionalImportsStr = opts.additionalImports?.length
378
+ ? "\n" + opts.additionalImports.join("\n")
379
+ : "";
380
+
381
+ // Build additional members
382
+ const additionalMembersStr = opts.additionalMembers?.length
383
+ ? "\n\n" + opts.additionalMembers.join("\n\n")
384
+ : "";
385
+
386
+ return `// Auto-generated by donkeylabs generate
387
+ // DO NOT EDIT MANUALLY
388
+
389
+ ${opts.baseImport}${additionalImportsStr}
390
+
391
+ export class ApiClient extends ${opts.baseClass} {
392
+ constructor(${opts.constructorSignature}) {
393
+ ${opts.constructorBody}
394
+ }
395
+
396
+ ${namespaceBlocks.join("\n\n") || " // No routes defined"}${additionalMembersStr}
397
+ }
398
+
399
+ ${opts.factoryFunction}
400
+ `;
401
+ }
402
+
403
+ /**
404
+ * Generate fully-typed client code with route types (advanced format)
405
+ * This is used by the standalone script which parses source files
406
+ */
407
+ export function generateClientCode(
408
+ ctx: GeneratorConfig,
409
+ options: Partial<ClientGeneratorOptions> = {}
410
+ ): string {
411
+ const { routes, events = [], clientConfigs = [] } = ctx;
412
+ const opts = { ...defaultGeneratorOptions, ...options };
413
+
414
+ // Check if routes are simple ExtractedRoute format
415
+ if (routes.length > 0 && !("prefix" in routes[0])) {
416
+ return generateClientFromRoutes(routes as ExtractedRoute[], options);
417
+ }
418
+
419
+ const routeInfos = routes as RouteInfo[];
420
+ const defaultCredentials =
421
+ clientConfigs.find((c) => c.credentials)?.credentials || "include";
422
+
423
+ const routeGroups = groupRoutesByPrefix(routeInfos);
424
+
425
+ // Generate route type definitions
426
+ const routeTypeBlocks: string[] = [];
427
+ const routeNamespaceBlocks: string[] = [];
428
+
429
+ for (const [prefix, prefixRoutes] of routeGroups) {
430
+ const namespaceName = prefix === "_root" ? "Root" : toPascalCase(prefix);
431
+ const methodName = prefix === "_root" ? "_root" : prefix;
432
+
433
+ const typeEntries = prefixRoutes
434
+ .filter((r) => r.handler === "typed")
435
+ .map((r) => {
436
+ const inputType = zodToTypeScript(r.inputSource);
437
+ const outputType = zodToTypeScript(r.outputSource);
438
+ return ` export type ${toPascalCase(r.routeName)}Input = ${inputType};
439
+ export type ${toPascalCase(r.routeName)}Output = ${outputType};`;
440
+ });
441
+
442
+ if (typeEntries.length > 0) {
443
+ routeTypeBlocks.push(` export namespace ${namespaceName} {
444
+ ${typeEntries.join("\n\n")}
445
+ }`);
446
+ }
447
+
448
+ const methodEntries = prefixRoutes
449
+ .filter((r) => r.handler === "typed")
450
+ .map((r) => {
451
+ const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}Input`;
452
+ const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}Output`;
453
+ return ` ${toCamelCase(r.routeName)}: (input: ${inputType}, options?: RequestOptions): Promise<${outputType}> =>
454
+ this.request("${r.name}", input, options)`;
455
+ });
456
+
457
+ const rawMethodEntries = prefixRoutes
458
+ .filter((r) => r.handler === "raw")
459
+ .map((r) => {
460
+ return ` ${toCamelCase(r.routeName)}: (init?: RequestInit): Promise<Response> =>
461
+ this.rawRequest("${r.name}", init)`;
462
+ });
463
+
464
+ const allMethods = [...methodEntries, ...rawMethodEntries];
465
+
466
+ if (allMethods.length > 0) {
467
+ routeNamespaceBlocks.push(` ${methodName} = {
468
+ ${allMethods.join(",\n\n")}
469
+ };`);
470
+ }
471
+ }
472
+
473
+ // Generate event types
474
+ const eventTypeEntries = events.map((e) => {
475
+ const type = zodToTypeScript(e.schemaSource);
476
+ return ` "${e.name}": ${type};`;
477
+ });
478
+
479
+ const eventTypesBlock =
480
+ eventTypeEntries.length > 0
481
+ ? `export interface SSEEvents {
482
+ ${eventTypeEntries.join("\n")}
483
+ }`
484
+ : `export interface SSEEvents {}`;
485
+
486
+ // Build additional imports
487
+ const additionalImportsStr = opts.additionalImports?.length
488
+ ? "\n" + opts.additionalImports.join("\n")
489
+ : "";
490
+
491
+ return `// Auto-generated by scripts/generate-client.ts
492
+ // DO NOT EDIT MANUALLY
493
+
494
+ import {
495
+ ApiClientBase,
496
+ ApiError,
497
+ ValidationError,
498
+ type RequestOptions,
499
+ type ApiClientOptions,
500
+ type SSEOptions,
501
+ } from "./base";${additionalImportsStr}
502
+
503
+ // ============================================
504
+ // Route Types
505
+ // ============================================
506
+
507
+ export namespace Routes {
508
+ ${routeTypeBlocks.join("\n\n") || " // No typed routes found"}
509
+ }
510
+
511
+ // ============================================
512
+ // SSE Event Types
513
+ // ============================================
514
+
515
+ ${eventTypesBlock}
516
+
517
+ // ============================================
518
+ // API Client
519
+ // ============================================
520
+
521
+ export interface ApiClientConfig extends ApiClientOptions {
522
+ baseUrl: string;
523
+ }
524
+
525
+ export class ApiClient extends ApiClientBase<SSEEvents> {
526
+ constructor(config: ApiClientConfig) {
527
+ super(config.baseUrl, {
528
+ credentials: "${defaultCredentials}",
529
+ ...config,
530
+ });
531
+ }
532
+
533
+ // ==========================================
534
+ // Route Namespaces
535
+ // ==========================================
536
+
537
+ ${routeNamespaceBlocks.join("\n\n") || " // No routes defined"}
538
+ }
539
+
540
+ // ============================================
541
+ // Factory Function
542
+ // ============================================
543
+
544
+ export function createApiClient(config: ApiClientConfig): ApiClient {
545
+ return new ApiClient(config);
546
+ }
547
+
548
+ // Re-export base types for convenience
549
+ export { ApiError, ValidationError, type RequestOptions, type SSEOptions };
550
+ `;
551
+ }
package/src/handlers.ts CHANGED
@@ -1,101 +1,6 @@
1
1
  import type { RouteDefinition, ServerContext } from "./router";
2
2
  import { z } from "zod";
3
3
 
4
- // ============================================
5
- // Route and Handler Types for DX
6
- // ============================================
7
-
8
- /**
9
- * Route contract interface - generated routes export this shape
10
- */
11
- export interface RouteContract {
12
- input: any;
13
- output: any;
14
- }
15
-
16
- /**
17
- * Handler interface for model classes.
18
- *
19
- * @example
20
- * import type { Handler } from "@donkeylabs/server";
21
- * import type { Health } from "$server/routes";
22
- *
23
- * export class PingModel implements Handler<Health.Ping> {
24
- * handle(input: Health.Ping.Input): Health.Ping.Output {
25
- * return { status: "ok", timestamp: new Date().toISOString() };
26
- * }
27
- * }
28
- */
29
- export interface Handler<T extends RouteContract> {
30
- handle(input: T["input"]): T["output"] | Promise<T["output"]>;
31
- }
32
-
33
- /**
34
- * Typed route definition.
35
- * Use with generated route types: `const route: Route<Health.Ping> = { ... }`
36
- *
37
- * @example
38
- * import { Health } from ".@donkeylabs/server/routes";
39
- * import type { Route } from "@donkeylabs/server";
40
- *
41
- * export const pingRoute: Route<Health.Ping> = {
42
- * input: Health.Ping.Input,
43
- * output: Health.Ping.Output,
44
- * handle: async (input, ctx) => new PingModel(ctx).handle(input),
45
- * };
46
- */
47
- export interface Route<T extends RouteContract> {
48
- input: z.ZodType<T["input"]>;
49
- output: z.ZodType<T["output"]>;
50
- handle: (input: T["input"], ctx: ServerContext) => T["output"] | Promise<T["output"]>;
51
- }
52
-
53
- /**
54
- * Route configuration with inferred types from zod schemas.
55
- */
56
- export interface TypedRouteConfig<TInput, TOutput> {
57
- input: z.ZodType<TInput>;
58
- output: z.ZodType<TOutput>;
59
- handle: (input: TInput, ctx: ServerContext) => TOutput | Promise<TOutput>;
60
- }
61
-
62
- export interface RawRouteConfig {
63
- handle: (req: Request, ctx: ServerContext) => Response | Promise<Response>;
64
- }
65
-
66
- /**
67
- * Create a typed route with full type inference from zod schemas.
68
- *
69
- * @example
70
- * export const pingRoute = createRoute.typed({
71
- * input: z.object({ echo: z.string().optional() }),
72
- * output: z.object({ status: z.literal("ok"), timestamp: z.string() }),
73
- * handle: async (input, ctx) => ({
74
- * status: "ok",
75
- * timestamp: new Date().toISOString(),
76
- * }),
77
- * });
78
- */
79
- export const createRoute = {
80
- typed<TInput, TOutput>(config: TypedRouteConfig<TInput, TOutput>) {
81
- return {
82
- ...config,
83
- _handler: "typed" as const,
84
- };
85
- },
86
-
87
- raw(config: RawRouteConfig) {
88
- return {
89
- ...config,
90
- _handler: "raw" as const,
91
- };
92
- },
93
- };
94
-
95
- // ============================================
96
- // Handler Runtimes
97
- // ============================================
98
-
99
4
  export interface HandlerRuntime<Fn extends Function = Function> {
100
5
  execute(
101
6
  req: Request,
@@ -133,39 +38,37 @@ export function createHandler<Fn extends Function>(
133
38
  };
134
39
  }
135
40
 
41
+ // ==========================================
42
+ // 1. Typed Handler (Default)
43
+ // ==========================================
136
44
  export type TypedFn = (input: any, ctx: ServerContext) => Promise<any> | any;
137
45
  export type TypedHandler = HandlerRuntime<TypedFn>;
138
46
 
139
47
  export const TypedHandler: TypedHandler = {
140
48
  async execute(req, def, handle, ctx) {
141
- if (req.method !== "POST") {
142
- return new Response("Method Not Allowed", { status: 405 });
143
- }
144
-
145
- let body: unknown = {};
146
- try {
147
- body = await req.json();
148
- } catch {
149
- return Response.json({ error: "Invalid JSON" }, { status: 400 });
150
- }
49
+ if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
50
+ let body: any = {};
51
+ try { body = await req.json(); } catch(e) { return Response.json({error: "Invalid JSON"}, {status:400}); }
151
52
 
152
53
  try {
153
54
  const input = def.input ? def.input.parse(body) : body;
154
55
  const result = await handle(input, ctx);
155
56
  const output = def.output ? def.output.parse(result) : result;
156
57
  return Response.json(output);
157
- } catch (e) {
58
+ } catch (e: any) {
158
59
  console.error(e);
159
60
  if (e instanceof z.ZodError) {
160
61
  return Response.json({ error: "Validation Failed", details: e.issues }, { status: 400 });
161
62
  }
162
- const message = e instanceof Error ? e.message : "Internal Error";
163
- return Response.json({ error: message }, { status: 500 });
63
+ return Response.json({ error: e.message || "Internal Error" }, { status: 500 });
164
64
  }
165
65
  },
166
66
  __signature: undefined as unknown as TypedFn
167
- };
67
+ }
168
68
 
69
+ // ==========================================
70
+ // 2. Raw Handler
71
+ // ==========================================
169
72
  export type RawFn = (req: Request, ctx: ServerContext) => Promise<Response> | Response;
170
73
  export type RawHandler = HandlerRuntime<RawFn>;
171
74
 
@@ -174,9 +77,10 @@ export const RawHandler: RawHandler = {
174
77
  return await handle(req, ctx);
175
78
  },
176
79
  __signature: undefined as unknown as RawFn
177
- };
80
+ }
178
81
 
179
82
  export const Handlers = {
180
83
  typed: TypedHandler,
181
84
  raw: RawHandler
182
85
  };
86
+