@rexeus/typeweaver-server 0.5.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.
- package/README.md +393 -0
- package/dist/LICENSE +202 -0
- package/dist/NOTICE +4 -0
- package/dist/index.cjs +110 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +101 -0
- package/dist/lib/Errors.ts +38 -0
- package/dist/lib/FetchApiAdapter.ts +362 -0
- package/dist/lib/Middleware.ts +71 -0
- package/dist/lib/NodeAdapter.ts +121 -0
- package/dist/lib/PathMatcher.ts +56 -0
- package/dist/lib/RequestHandler.ts +35 -0
- package/dist/lib/Router.ts +263 -0
- package/dist/lib/ServerContext.ts +54 -0
- package/dist/lib/StateMap.ts +57 -0
- package/dist/lib/TypedMiddleware.ts +140 -0
- package/dist/lib/TypeweaverApp.ts +376 -0
- package/dist/lib/TypeweaverRouter.ts +138 -0
- package/dist/lib/index.ts +38 -0
- package/dist/metafile-cjs.json +1 -0
- package/dist/metafile-esm.json +1 -0
- package/dist/templates/Router.ejs +43 -0
- package/package.json +71 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
9
|
+
|
|
10
|
+
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
11
|
+
import type { Middleware } from "./Middleware";
|
|
12
|
+
import type { ServerContext } from "./ServerContext";
|
|
13
|
+
import type { TypeweaverApp } from "./TypeweaverApp";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A middleware descriptor carrying compile-time metadata about what state
|
|
17
|
+
* it provides to downstream consumers and what state it requires from
|
|
18
|
+
* upstream middleware.
|
|
19
|
+
*
|
|
20
|
+
* Created via {@link defineMiddleware}. The `_brand` property exists purely
|
|
21
|
+
* at the type level for TypeScript's inference — its runtime value is an empty object.
|
|
22
|
+
*
|
|
23
|
+
* @template TProvides - State keys this middleware adds to the context
|
|
24
|
+
* @template TRequires - State keys this middleware expects to already exist
|
|
25
|
+
*/
|
|
26
|
+
export type TypedMiddleware<
|
|
27
|
+
TProvides extends Record<string, unknown> = {},
|
|
28
|
+
TRequires extends Record<string, unknown> = {},
|
|
29
|
+
> = {
|
|
30
|
+
readonly handler: Middleware;
|
|
31
|
+
readonly _brand: {
|
|
32
|
+
readonly provides: TProvides;
|
|
33
|
+
readonly requires: TRequires;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compile-time error type produced when a typed middleware's requirements
|
|
39
|
+
* are not satisfied by the current accumulated app state.
|
|
40
|
+
*
|
|
41
|
+
* When intersected with `TypedMiddleware`, produces an uninhabitable type —
|
|
42
|
+
* no value can satisfy both, causing a clear compiler error at the call site.
|
|
43
|
+
*
|
|
44
|
+
* The error type's properties (`required`, `available`, `missing`) appear
|
|
45
|
+
* in the compiler diagnostic, helping developers identify what's wrong.
|
|
46
|
+
*/
|
|
47
|
+
export type StateRequirementError<
|
|
48
|
+
TRequired extends Record<string, unknown>,
|
|
49
|
+
TAvailable extends Record<string, unknown>,
|
|
50
|
+
> = {
|
|
51
|
+
readonly __error: "STATE_REQUIREMENT_NOT_MET";
|
|
52
|
+
readonly required: TRequired;
|
|
53
|
+
readonly available: TAvailable;
|
|
54
|
+
readonly missing: Exclude<keyof TRequired, keyof TAvailable>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The type of the `next` function inside a typed middleware handler.
|
|
59
|
+
*
|
|
60
|
+
* When `TProvides` has keys, `next` requires the state object as its argument,
|
|
61
|
+
* enforcing at compile time that middleware provides the state it declares.
|
|
62
|
+
*
|
|
63
|
+
* When `TProvides` is empty (`{}`), `next` takes no arguments — the middleware
|
|
64
|
+
* is pass-through and doesn't need to provide any state.
|
|
65
|
+
*
|
|
66
|
+
* @template TProvides - The state keys this middleware declares it provides
|
|
67
|
+
*/
|
|
68
|
+
export type NextFn<TProvides extends Record<string, unknown>> = [
|
|
69
|
+
keyof TProvides,
|
|
70
|
+
] extends [never]
|
|
71
|
+
? () => Promise<IHttpResponse>
|
|
72
|
+
: (state: TProvides) => Promise<IHttpResponse>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a typed middleware descriptor.
|
|
76
|
+
*
|
|
77
|
+
* The handler receives a `ServerContext` parameterized with the required
|
|
78
|
+
* upstream state, and a `next` function that enforces providing the declared
|
|
79
|
+
* state. When the middleware declares `TProvides`, the state must be passed
|
|
80
|
+
* to `next()` — the pipeline merges it into `ctx.state` before continuing.
|
|
81
|
+
*
|
|
82
|
+
* @template TProvides - State keys this middleware adds
|
|
83
|
+
* @template TRequires - State keys this middleware expects (defaults to none)
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* // Middleware that provides state — MUST pass it to next()
|
|
88
|
+
* const auth = defineMiddleware<{ userId: string }>(async (ctx, next) => {
|
|
89
|
+
* return next({ userId: parseToken(ctx.request.header?.["authorization"]) });
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* // Middleware with requirements — reads upstream state, provides new state
|
|
93
|
+
* const permissions = defineMiddleware<{ permissions: string[] }, { userId: string }>(
|
|
94
|
+
* async (ctx, next) => {
|
|
95
|
+
* const userId = ctx.state.get("userId"); // string (no undefined!)
|
|
96
|
+
* return next({ permissions: await loadPermissions(userId) });
|
|
97
|
+
* }
|
|
98
|
+
* );
|
|
99
|
+
*
|
|
100
|
+
* // Pass-through middleware — next() takes no args
|
|
101
|
+
* const logger = defineMiddleware(async (ctx, next) => {
|
|
102
|
+
* console.log(ctx.request.path);
|
|
103
|
+
* return next();
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function defineMiddleware<
|
|
108
|
+
TProvides extends Record<string, unknown> = {},
|
|
109
|
+
TRequires extends Record<string, unknown> = {},
|
|
110
|
+
>(
|
|
111
|
+
handler: (
|
|
112
|
+
ctx: ServerContext<TRequires>,
|
|
113
|
+
next: NextFn<TProvides>
|
|
114
|
+
) => Promise<IHttpResponse>
|
|
115
|
+
): TypedMiddleware<TProvides, TRequires> {
|
|
116
|
+
return {
|
|
117
|
+
handler: handler as Middleware,
|
|
118
|
+
_brand: {} as TypedMiddleware<TProvides, TRequires>["_brand"],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extracts the accumulated state type from a `TypeweaverApp` instance.
|
|
124
|
+
*
|
|
125
|
+
* Use this to derive the state type for handler implementations
|
|
126
|
+
* without declaring it separately.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const app = new TypeweaverApp()
|
|
131
|
+
* .use(authMiddleware)
|
|
132
|
+
* .use(permissionsMiddleware);
|
|
133
|
+
*
|
|
134
|
+
* type AppState = InferState<typeof app>;
|
|
135
|
+
* // { userId: string } & { permissions: string[] }
|
|
136
|
+
*
|
|
137
|
+
* const handlers: TodoApiHandler<AppState> = { ... };
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export type InferState<T> = T extends TypeweaverApp<infer S> ? S : never;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { HttpResponse, RequestValidationError } from "@rexeus/typeweaver-core";
|
|
9
|
+
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
10
|
+
import { BodyParseError, PayloadTooLargeError } from "./Errors";
|
|
11
|
+
import { FetchApiAdapter } from "./FetchApiAdapter";
|
|
12
|
+
import { executeMiddlewarePipeline } from "./Middleware";
|
|
13
|
+
import { Router } from "./Router";
|
|
14
|
+
import { StateMap } from "./StateMap";
|
|
15
|
+
import type { Middleware } from "./Middleware";
|
|
16
|
+
import type { RequestHandler } from "./RequestHandler";
|
|
17
|
+
import type {
|
|
18
|
+
HttpResponseErrorHandler,
|
|
19
|
+
RouteDefinition,
|
|
20
|
+
UnknownErrorHandler,
|
|
21
|
+
ValidationErrorHandler,
|
|
22
|
+
} from "./Router";
|
|
23
|
+
import type { ServerContext } from "./ServerContext";
|
|
24
|
+
import type { StateRequirementError, TypedMiddleware } from "./TypedMiddleware";
|
|
25
|
+
import type { TypeweaverRouter } from "./TypeweaverRouter";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The main application class that provides routing, middleware, and
|
|
29
|
+
* request handling — all using typeweaver's native `IHttpRequest`/`IHttpResponse` format.
|
|
30
|
+
*
|
|
31
|
+
* Exposes a single `fetch()` method compatible with Bun, Deno, Cloudflare Workers,
|
|
32
|
+
* and adaptable to Node.js `http.createServer`.
|
|
33
|
+
*
|
|
34
|
+
* Internally, the entire pipeline operates on `IHttpRequest`/`IHttpResponse`.
|
|
35
|
+
* Conversion from/to Fetch API `Request`/`Response` happens **only** at the boundary.
|
|
36
|
+
*
|
|
37
|
+
* Middleware is return-based: each middleware returns an `IHttpResponse`
|
|
38
|
+
* instead of mutating shared state.
|
|
39
|
+
*
|
|
40
|
+
* Middleware runs for **all** requests, including 404s and 405s, so global
|
|
41
|
+
* concerns like logging and CORS always execute.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const app = new TypeweaverApp()
|
|
46
|
+
* .use(authMiddleware) // defineMiddleware<{ userId: string }>
|
|
47
|
+
* .use(permissionsMiddleware) // defineMiddleware<{ perms: string[] }, { userId: string }>
|
|
48
|
+
* .route(new TodoRouter({ requestHandlers: { ... } }));
|
|
49
|
+
*
|
|
50
|
+
* Bun.serve({ fetch: app.fetch, port: 3000 });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export type TypeweaverAppOptions = {
|
|
54
|
+
readonly maxBodySize?: number;
|
|
55
|
+
readonly onError?: (error: unknown) => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
59
|
+
export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
60
|
+
private static readonly INTERNAL_SERVER_ERROR_BODY = {
|
|
61
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
62
|
+
message: "An unexpected error occurred",
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
private readonly router = new Router();
|
|
66
|
+
private readonly middlewares: Middleware[] = [];
|
|
67
|
+
private readonly adapter: FetchApiAdapter;
|
|
68
|
+
private readonly onError: (error: unknown) => void;
|
|
69
|
+
|
|
70
|
+
public constructor(options?: TypeweaverAppOptions) {
|
|
71
|
+
this.adapter = new FetchApiAdapter({ maxBodySize: options?.maxBodySize });
|
|
72
|
+
this.onError = options?.onError ?? console.error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private safeOnError(error: unknown): void {
|
|
76
|
+
try {
|
|
77
|
+
this.onError(error);
|
|
78
|
+
} catch (onErrorFailure) {
|
|
79
|
+
console.error(
|
|
80
|
+
"TypeweaverApp: onError callback threw while handling error",
|
|
81
|
+
{ onErrorFailure, originalError: error }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Register a typed middleware that provides state to downstream handlers.
|
|
88
|
+
*
|
|
89
|
+
* Returns a new `TypeweaverApp` type with the accumulated state.
|
|
90
|
+
* Produces a compile-time error if the middleware's requirements are not met
|
|
91
|
+
* by the currently accumulated state.
|
|
92
|
+
*
|
|
93
|
+
* Use {@link defineMiddleware} to create typed middleware.
|
|
94
|
+
*/
|
|
95
|
+
public use<
|
|
96
|
+
TProv extends Record<string, unknown>,
|
|
97
|
+
TReq extends Record<string, unknown>,
|
|
98
|
+
>(
|
|
99
|
+
middleware: TypedMiddleware<TProv, TReq> &
|
|
100
|
+
([TState] extends [TReq] ? unknown : StateRequirementError<TReq, TState>)
|
|
101
|
+
): TypeweaverApp<TState & TProv> {
|
|
102
|
+
this.middlewares.push(middleware.handler);
|
|
103
|
+
return this as unknown as TypeweaverApp<TState & TProv>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Mount a generated `TypeweaverRouter` instance.
|
|
108
|
+
*
|
|
109
|
+
* Registers all routes from the router into the app.
|
|
110
|
+
* Multiple routers can be mounted on the same app.
|
|
111
|
+
*
|
|
112
|
+
* Optionally accepts a prefix to prepend to all routes from the router.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* app.route(new AccountRouter({ requestHandlers: { ... } }));
|
|
117
|
+
* app.route("/api/v1", new TodoRouter({ requestHandlers: { ... } }));
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
public route(router: TypeweaverRouter<Record<string, RequestHandler>>): this;
|
|
121
|
+
public route(
|
|
122
|
+
prefix: string,
|
|
123
|
+
router: TypeweaverRouter<Record<string, RequestHandler>>
|
|
124
|
+
): this;
|
|
125
|
+
public route(
|
|
126
|
+
prefixOrRouter: string | TypeweaverRouter<Record<string, RequestHandler>>,
|
|
127
|
+
router?: TypeweaverRouter<Record<string, RequestHandler>>
|
|
128
|
+
): this {
|
|
129
|
+
if (typeof prefixOrRouter === "string") {
|
|
130
|
+
if (!router) {
|
|
131
|
+
throw new Error("Router is required when mounting with a prefix");
|
|
132
|
+
}
|
|
133
|
+
return this.mountRouter(router, prefixOrRouter);
|
|
134
|
+
}
|
|
135
|
+
return this.mountRouter(prefixOrRouter);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Universal request handler compatible with Fetch API runtimes.
|
|
140
|
+
*
|
|
141
|
+
* The entire pipeline works with `IHttpRequest`/`IHttpResponse`.
|
|
142
|
+
* Conversion from/to Fetch API `Request`/`Response` happens only here, at the boundary.
|
|
143
|
+
*
|
|
144
|
+
* Middleware runs for all requests (including 404s and 405s).
|
|
145
|
+
* HEAD requests automatically fall back to GET handlers with body stripped from the response.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* // Bun
|
|
150
|
+
* Bun.serve({ fetch: app.fetch, port: 3000 });
|
|
151
|
+
*
|
|
152
|
+
* // Deno
|
|
153
|
+
* Deno.serve({ port: 3000 }, app.fetch);
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
public fetch = async (request: Request): Promise<Response> => {
|
|
157
|
+
try {
|
|
158
|
+
const response = await this.processRequest(request);
|
|
159
|
+
return this.adapter.toResponse(response);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof PayloadTooLargeError) {
|
|
162
|
+
this.safeOnError(error);
|
|
163
|
+
return this.adapter.toResponse({
|
|
164
|
+
statusCode: 413,
|
|
165
|
+
body: {
|
|
166
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
167
|
+
message: "Request body exceeds the size limit",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (error instanceof BodyParseError) {
|
|
172
|
+
return this.adapter.toResponse({
|
|
173
|
+
statusCode: 400,
|
|
174
|
+
body: { code: "BAD_REQUEST", message: "Malformed request body" },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
this.safeOnError(error);
|
|
178
|
+
return TypeweaverApp.createErrorResponse();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
private async processRequest(request: Request): Promise<IHttpResponse> {
|
|
183
|
+
const url = new URL(request.url);
|
|
184
|
+
const httpRequest = await this.adapter.toRequest(request, url);
|
|
185
|
+
|
|
186
|
+
const ctx: ServerContext = {
|
|
187
|
+
request: httpRequest,
|
|
188
|
+
state: new StateMap(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const response = await executeMiddlewarePipeline(
|
|
192
|
+
this.middlewares,
|
|
193
|
+
ctx,
|
|
194
|
+
() => this.resolveAndExecute(request.method, url.pathname, ctx)
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return request.method.toUpperCase() === "HEAD"
|
|
198
|
+
? { ...response, body: undefined }
|
|
199
|
+
: response;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Match the route and execute the handler.
|
|
204
|
+
* Called as the final handler in the middleware pipeline.
|
|
205
|
+
*/
|
|
206
|
+
private async resolveAndExecute(
|
|
207
|
+
method: string,
|
|
208
|
+
pathname: string,
|
|
209
|
+
ctx: ServerContext
|
|
210
|
+
): Promise<IHttpResponse> {
|
|
211
|
+
const match = this.router.match(method, pathname);
|
|
212
|
+
|
|
213
|
+
if (match) {
|
|
214
|
+
const routeCtx = this.withPathParams(ctx, match.params);
|
|
215
|
+
try {
|
|
216
|
+
return await this.executeHandler(routeCtx, match.route);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
return this.handleError(error, routeCtx, match.route);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const pathMatch = this.router.matchPath(pathname);
|
|
223
|
+
if (pathMatch) {
|
|
224
|
+
return {
|
|
225
|
+
statusCode: 405,
|
|
226
|
+
header: { Allow: pathMatch.allowedMethods.join(", ") },
|
|
227
|
+
body: {
|
|
228
|
+
code: "METHOD_NOT_ALLOWED",
|
|
229
|
+
message: "Method not supported for this resource",
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
statusCode: 404,
|
|
236
|
+
body: { code: "NOT_FOUND", message: "No matching resource found" },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private withPathParams(
|
|
241
|
+
ctx: ServerContext,
|
|
242
|
+
params: Record<string, string>
|
|
243
|
+
): ServerContext {
|
|
244
|
+
if (Object.keys(params).length === 0) return ctx;
|
|
245
|
+
return { ...ctx, request: { ...ctx.request, param: params } };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async executeHandler(
|
|
249
|
+
ctx: ServerContext,
|
|
250
|
+
route: RouteDefinition
|
|
251
|
+
): Promise<IHttpResponse> {
|
|
252
|
+
const validatedRequest = route.routerConfig.validateRequests
|
|
253
|
+
? route.validator.validate(ctx.request)
|
|
254
|
+
: ctx.request;
|
|
255
|
+
|
|
256
|
+
return route.handler(validatedRequest, ctx);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handle errors using the route's configured error handlers.
|
|
261
|
+
* Handler errors bubble up to the safety net in `fetch()`.
|
|
262
|
+
*/
|
|
263
|
+
private handleError(
|
|
264
|
+
error: unknown,
|
|
265
|
+
ctx: ServerContext,
|
|
266
|
+
route: RouteDefinition
|
|
267
|
+
): IHttpResponse | Promise<IHttpResponse> {
|
|
268
|
+
const config = route.routerConfig;
|
|
269
|
+
|
|
270
|
+
if (error instanceof RequestValidationError) {
|
|
271
|
+
const handler = this.resolveErrorHandler<ValidationErrorHandler>(
|
|
272
|
+
config.handleValidationErrors,
|
|
273
|
+
TypeweaverApp.defaultValidationHandler
|
|
274
|
+
);
|
|
275
|
+
if (handler) return handler(error, ctx);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (error instanceof HttpResponse) {
|
|
279
|
+
const handler = this.resolveErrorHandler<HttpResponseErrorHandler>(
|
|
280
|
+
config.handleHttpResponseErrors,
|
|
281
|
+
TypeweaverApp.defaultHttpResponseHandler
|
|
282
|
+
);
|
|
283
|
+
if (handler) return handler(error, ctx);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const handler = this.resolveErrorHandler<UnknownErrorHandler>(
|
|
287
|
+
config.handleUnknownErrors,
|
|
288
|
+
this.defaultUnknownHandler
|
|
289
|
+
);
|
|
290
|
+
if (handler) return handler(error, ctx);
|
|
291
|
+
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve an error handler option to a concrete handler function.
|
|
297
|
+
*/
|
|
298
|
+
private resolveErrorHandler<T extends (...args: any[]) => any>(
|
|
299
|
+
option: T | boolean | undefined,
|
|
300
|
+
defaultHandler: T
|
|
301
|
+
): T | undefined {
|
|
302
|
+
if (option === false) return undefined;
|
|
303
|
+
if (option === true || option === undefined) return defaultHandler;
|
|
304
|
+
return option;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private mountRouter(
|
|
308
|
+
router: TypeweaverRouter<Record<string, RequestHandler>>,
|
|
309
|
+
prefix?: string
|
|
310
|
+
): this {
|
|
311
|
+
const normalizedPrefix = prefix?.replace(/\/+$/, "");
|
|
312
|
+
for (const route of router.getRoutes()) {
|
|
313
|
+
this.router.add({
|
|
314
|
+
...route,
|
|
315
|
+
path: normalizedPrefix ? normalizedPrefix + route.path : route.path,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return this;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private static sanitizeIssues(
|
|
322
|
+
issues: readonly {
|
|
323
|
+
readonly message: string;
|
|
324
|
+
readonly path: PropertyKey[];
|
|
325
|
+
}[]
|
|
326
|
+
): readonly { message: string; path: PropertyKey[] }[] | undefined {
|
|
327
|
+
if (issues.length === 0) return undefined;
|
|
328
|
+
return issues.map(({ message, path }) => ({ message, path }));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private static defaultValidationHandler: ValidationErrorHandler = (
|
|
332
|
+
err
|
|
333
|
+
): IHttpResponse => {
|
|
334
|
+
const issues: Record<string, unknown> = Object.create(null);
|
|
335
|
+
|
|
336
|
+
const header = TypeweaverApp.sanitizeIssues(err.headerIssues);
|
|
337
|
+
const body = TypeweaverApp.sanitizeIssues(err.bodyIssues);
|
|
338
|
+
const query = TypeweaverApp.sanitizeIssues(err.queryIssues);
|
|
339
|
+
const param = TypeweaverApp.sanitizeIssues(err.pathParamIssues);
|
|
340
|
+
|
|
341
|
+
if (header) issues.header = header;
|
|
342
|
+
if (body) issues.body = body;
|
|
343
|
+
if (query) issues.query = query;
|
|
344
|
+
if (param) issues.param = param;
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
statusCode: 400,
|
|
348
|
+
body: {
|
|
349
|
+
code: "VALIDATION_ERROR",
|
|
350
|
+
message: err.message,
|
|
351
|
+
issues,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
private static defaultHttpResponseHandler: HttpResponseErrorHandler = (
|
|
357
|
+
err
|
|
358
|
+
): IHttpResponse => err;
|
|
359
|
+
|
|
360
|
+
private readonly defaultUnknownHandler: UnknownErrorHandler = (
|
|
361
|
+
error
|
|
362
|
+
): IHttpResponse => {
|
|
363
|
+
this.safeOnError(error);
|
|
364
|
+
return { statusCode: 500, body: TypeweaverApp.INTERNAL_SERVER_ERROR_BODY };
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
private static createErrorResponse(): Response {
|
|
368
|
+
return new Response(
|
|
369
|
+
JSON.stringify(TypeweaverApp.INTERNAL_SERVER_ERROR_BODY),
|
|
370
|
+
{
|
|
371
|
+
status: 500,
|
|
372
|
+
headers: { "content-type": "application/json" },
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// NOTE: The @generated header below is for the copy that ships to user projects.
|
|
2
|
+
// This file IS the source of truth — edit it here.
|
|
3
|
+
/**
|
|
4
|
+
* This file was automatically generated by typeweaver.
|
|
5
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
6
|
+
*
|
|
7
|
+
* @generated by @rexeus/typeweaver
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { HttpMethod, IRequestValidator } from "@rexeus/typeweaver-core";
|
|
11
|
+
import type { RequestHandler } from "./RequestHandler";
|
|
12
|
+
import type {
|
|
13
|
+
HttpResponseErrorHandler,
|
|
14
|
+
RouteDefinition,
|
|
15
|
+
RouterErrorConfig,
|
|
16
|
+
UnknownErrorHandler,
|
|
17
|
+
ValidationErrorHandler,
|
|
18
|
+
} from "./Router";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration options for TypeweaverRouter instances.
|
|
22
|
+
*
|
|
23
|
+
* @template RequestHandlers - Object type containing all handler methods for this router
|
|
24
|
+
*/
|
|
25
|
+
export type TypeweaverRouterOptions<
|
|
26
|
+
RequestHandlers extends Record<string, RequestHandler>,
|
|
27
|
+
> = {
|
|
28
|
+
/**
|
|
29
|
+
* Request handler methods for each operation.
|
|
30
|
+
* Each handler receives a validated request and the server context.
|
|
31
|
+
*/
|
|
32
|
+
readonly requestHandlers: RequestHandlers;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Enable request validation using generated validators.
|
|
36
|
+
* When false, requests are passed through without validation.
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
readonly validateRequests?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configure handling of HttpResponse errors thrown by handlers.
|
|
43
|
+
* - `true`: Use default handler (returns the error as response)
|
|
44
|
+
* - `false`: Disable this handler (errors fall through to the unknown error handler)
|
|
45
|
+
* - `function`: Use custom error handler
|
|
46
|
+
* @default true
|
|
47
|
+
*/
|
|
48
|
+
readonly handleHttpResponseErrors?: HttpResponseErrorHandler | boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Configure handling of request validation errors.
|
|
52
|
+
* - `true`: Use default handler (400 with error details)
|
|
53
|
+
* - `false`: Disable this handler (errors fall through to the unknown error handler)
|
|
54
|
+
* - `function`: Use custom error handler
|
|
55
|
+
* @default true
|
|
56
|
+
*/
|
|
57
|
+
readonly handleValidationErrors?: ValidationErrorHandler | boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Configure handling of unknown errors.
|
|
61
|
+
* - `true`: Use default handler (500 Internal Server Error)
|
|
62
|
+
* - `false`: Disable this handler (errors bubble up to the safety net)
|
|
63
|
+
* - `function`: Use custom error handler
|
|
64
|
+
* @default true
|
|
65
|
+
*/
|
|
66
|
+
readonly handleUnknownErrors?: UnknownErrorHandler | boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Abstract base class for typeweaver-generated routers.
|
|
71
|
+
*
|
|
72
|
+
* Each generated router (e.g., `AccountRouter`, `TodoRouter`) extends this class
|
|
73
|
+
* and registers its routes in the constructor via `this.route(...)`.
|
|
74
|
+
*
|
|
75
|
+
* The router does **not** handle HTTP directly — it collects route definitions
|
|
76
|
+
* that are mounted onto a `TypeweaverApp` via `app.route(...)`.
|
|
77
|
+
*
|
|
78
|
+
* All types are in typeweaver's native `IHttpRequest`/`IHttpResponse` format.
|
|
79
|
+
* No framework-specific types are involved.
|
|
80
|
+
*
|
|
81
|
+
* @template RequestHandlers - Object type containing typed handler methods
|
|
82
|
+
*/
|
|
83
|
+
export abstract class TypeweaverRouter<
|
|
84
|
+
RequestHandlers extends Record<string, RequestHandler>,
|
|
85
|
+
> {
|
|
86
|
+
protected readonly requestHandlers: RequestHandlers;
|
|
87
|
+
private readonly routes: RouteDefinition[] = [];
|
|
88
|
+
private readonly errorConfig: RouterErrorConfig;
|
|
89
|
+
|
|
90
|
+
public constructor(options: TypeweaverRouterOptions<RequestHandlers>) {
|
|
91
|
+
const {
|
|
92
|
+
requestHandlers,
|
|
93
|
+
validateRequests = true,
|
|
94
|
+
handleHttpResponseErrors = true,
|
|
95
|
+
handleValidationErrors = true,
|
|
96
|
+
handleUnknownErrors = true,
|
|
97
|
+
} = options;
|
|
98
|
+
|
|
99
|
+
this.requestHandlers = requestHandlers;
|
|
100
|
+
|
|
101
|
+
this.errorConfig = {
|
|
102
|
+
validateRequests,
|
|
103
|
+
handleHttpResponseErrors,
|
|
104
|
+
handleValidationErrors,
|
|
105
|
+
handleUnknownErrors,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Register a route. Called by generated subclasses in their constructor.
|
|
111
|
+
*
|
|
112
|
+
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
113
|
+
* @param path - Path pattern with `:param` placeholders
|
|
114
|
+
* @param validator - Request validator for this operation
|
|
115
|
+
* @param handler - Type-safe request handler
|
|
116
|
+
*/
|
|
117
|
+
protected route(
|
|
118
|
+
method: HttpMethod,
|
|
119
|
+
path: string,
|
|
120
|
+
validator: IRequestValidator,
|
|
121
|
+
handler: RequestHandler
|
|
122
|
+
): void {
|
|
123
|
+
this.routes.push({
|
|
124
|
+
method,
|
|
125
|
+
path,
|
|
126
|
+
validator,
|
|
127
|
+
handler,
|
|
128
|
+
routerConfig: this.errorConfig,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns all registered routes. Used by `TypeweaverApp` when mounting via `app.route(...)`.
|
|
134
|
+
*/
|
|
135
|
+
public getRoutes(): readonly RouteDefinition[] {
|
|
136
|
+
return this.routes;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file was automatically generated by typeweaver.
|
|
3
|
+
* DO NOT EDIT. Instead, modify the source definition file and generate again.
|
|
4
|
+
*
|
|
5
|
+
* @generated by @rexeus/typeweaver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* eslint-disable import/max-dependencies */
|
|
9
|
+
|
|
10
|
+
export { TypeweaverApp, type TypeweaverAppOptions } from "./TypeweaverApp";
|
|
11
|
+
export {
|
|
12
|
+
TypeweaverRouter,
|
|
13
|
+
type TypeweaverRouterOptions,
|
|
14
|
+
} from "./TypeweaverRouter";
|
|
15
|
+
export { HttpMethod } from "@rexeus/typeweaver-core";
|
|
16
|
+
export type {
|
|
17
|
+
HttpResponseErrorHandler,
|
|
18
|
+
UnknownErrorHandler,
|
|
19
|
+
ValidationErrorHandler,
|
|
20
|
+
} from "./Router";
|
|
21
|
+
export type { ServerContext } from "./ServerContext";
|
|
22
|
+
export type { RequestHandler } from "./RequestHandler";
|
|
23
|
+
export { StateMap } from "./StateMap";
|
|
24
|
+
export {
|
|
25
|
+
defineMiddleware,
|
|
26
|
+
type InferState,
|
|
27
|
+
type NextFn,
|
|
28
|
+
type StateRequirementError,
|
|
29
|
+
type TypedMiddleware,
|
|
30
|
+
} from "./TypedMiddleware";
|
|
31
|
+
export {
|
|
32
|
+
BodyParseError,
|
|
33
|
+
PayloadTooLargeError,
|
|
34
|
+
ResponseSerializationError,
|
|
35
|
+
} from "./Errors";
|
|
36
|
+
export { FetchApiAdapter } from "./FetchApiAdapter";
|
|
37
|
+
export { nodeAdapter, type NodeAdapterOptions } from "./NodeAdapter";
|
|
38
|
+
export { pathMatcher } from "./PathMatcher";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/RouterGenerator.ts":{"bytes":3869,"imports":[{"path":"node:path","kind":"import-statement","external":true},{"path":"node:url","kind":"import-statement","external":true},{"path":"@rexeus/typeweaver-core","kind":"import-statement","external":true},{"path":"@rexeus/typeweaver-gen","kind":"import-statement","external":true},{"path":"case","kind":"import-statement","external":true},{"path":"/Users/denniswentzien/Development/rexeus/typeweaver-5/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":1036,"imports":[{"path":"node:path","kind":"import-statement","external":true},{"path":"node:url","kind":"import-statement","external":true},{"path":"@rexeus/typeweaver-gen","kind":"import-statement","external":true},{"path":"src/RouterGenerator.ts","kind":"import-statement","original":"./RouterGenerator"},{"path":"/Users/denniswentzien/Development/rexeus/typeweaver-5/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/index.cjs":{"imports":[{"path":"node:path","kind":"import-statement","external":true},{"path":"node:url","kind":"import-statement","external":true},{"path":"@rexeus/typeweaver-gen","kind":"import-statement","external":true},{"path":"node:path","kind":"import-statement","external":true},{"path":"node:url","kind":"import-statement","external":true},{"path":"@rexeus/typeweaver-core","kind":"import-statement","external":true},{"path":"@rexeus/typeweaver-gen","kind":"import-statement","external":true},{"path":"case","kind":"import-statement","external":true}],"exports":["default"],"entryPoint":"src/index.ts","inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js":{"bytesInOutput":314},"src/index.ts":{"bytesInOutput":583},"src/RouterGenerator.ts":{"bytesInOutput":2862}},"bytes":3988}}}
|