@rexeus/typeweaver-server 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -44
- package/dist/lib/Router.ts +25 -5
- package/dist/lib/TypeweaverApp.ts +129 -34
- package/dist/lib/TypeweaverRouter.ts +44 -12
- package/dist/lib/index.ts +2 -1
- package/dist/templates/Router.ejs +2 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -113,8 +113,8 @@ export const userHandlers: ServerUserApiHandler = {
|
|
|
113
113
|
};
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
> Generated response
|
|
117
|
-
>
|
|
116
|
+
> Generated response factory functions (e.g. `createGetUserSuccessResponse`) are also available for
|
|
117
|
+
> constructing typed responses with pre-set `type` and `statusCode` discriminators.
|
|
118
118
|
|
|
119
119
|
### Create the app
|
|
120
120
|
|
|
@@ -310,48 +310,67 @@ const app = new TypeweaverApp({
|
|
|
310
310
|
|
|
311
311
|
Each router accepts `TypeweaverRouterOptions`:
|
|
312
312
|
|
|
313
|
-
| Option
|
|
314
|
-
|
|
|
315
|
-
| `requestHandlers`
|
|
316
|
-
| `validateRequests`
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
313
|
+
| Option | Type | Default | Description |
|
|
314
|
+
| -------------------------------- | ---------------------------- | ---------- | ---------------------------------- |
|
|
315
|
+
| `requestHandlers` | `Server<Resource>ApiHandler` | _required_ | Handler methods for each operation |
|
|
316
|
+
| `validateRequests` | `boolean` | `true` | Enable/disable request validation |
|
|
317
|
+
| `validateResponses` | `boolean` | `true` | Enable/disable response validation |
|
|
318
|
+
| `handleRequestValidationErrors` | `boolean \| function` | `true` | Handle request validation errors |
|
|
319
|
+
| `handleResponseValidationErrors` | `boolean \| function` | `true` | Handle response validation errors |
|
|
320
|
+
| `handleHttpResponseErrors` | `boolean \| function` | `true` | Handle thrown typed HTTP responses |
|
|
321
|
+
| `handleUnknownErrors` | `boolean \| function` | `true` | Handle unexpected errors |
|
|
320
322
|
|
|
321
323
|
When set to `true`, error handlers use sensible defaults (400/500 responses). When set to `false`,
|
|
322
|
-
errors fall through to the next handler in the chain
|
|
323
|
-
|
|
324
|
+
errors fall through to the next handler in the chain (except `handleResponseValidationErrors`, where
|
|
325
|
+
`false` means the invalid response is returned as-is — validation still runs for field stripping,
|
|
326
|
+
but invalid responses pass through unchanged). When set to a function, it receives the error and
|
|
327
|
+
`ServerContext` and must return an `IHttpResponse`. If a custom error handler throws, the framework
|
|
328
|
+
catches the exception and falls through gracefully to the next handler.
|
|
324
329
|
|
|
325
330
|
### 🚨 Error Handling
|
|
326
331
|
|
|
327
332
|
#### Throwing errors in handlers
|
|
328
333
|
|
|
329
|
-
|
|
330
|
-
|
|
334
|
+
Throw any object matching `ITypedHttpResponse` (i.e. `{ type: string, statusCode: number, ... }`)
|
|
335
|
+
from your handlers — the framework catches it automatically and returns it as the response:
|
|
331
336
|
|
|
332
337
|
```ts
|
|
333
338
|
import { HttpStatusCode } from "@rexeus/typeweaver-core";
|
|
334
|
-
import { GetUserSuccessResponse, NotFoundErrorResponse } from "./generated";
|
|
335
339
|
|
|
336
340
|
async handleGetUserRequest(request) {
|
|
337
341
|
const user = await db.findUser(request.param.userId);
|
|
338
342
|
if (!user) {
|
|
339
|
-
|
|
343
|
+
// Plain objects work — anything with `type` and `statusCode` is recognized
|
|
344
|
+
throw {
|
|
345
|
+
type: "NotFoundError",
|
|
340
346
|
statusCode: HttpStatusCode.NOT_FOUND,
|
|
341
347
|
header: { "Content-Type": "application/json" },
|
|
342
348
|
body: { message: "Resource not found", code: "NOT_FOUND_ERROR" },
|
|
343
|
-
}
|
|
349
|
+
};
|
|
344
350
|
}
|
|
345
|
-
return
|
|
351
|
+
return {
|
|
352
|
+
type: "GetUserSuccess",
|
|
346
353
|
statusCode: HttpStatusCode.OK,
|
|
347
354
|
header: { "Content-Type": "application/json" },
|
|
348
355
|
body: user,
|
|
349
|
-
}
|
|
356
|
+
};
|
|
350
357
|
}
|
|
351
358
|
```
|
|
352
359
|
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
Generated factory functions (e.g. `createNotFoundErrorResponse`) are a convenient shorthand — they
|
|
361
|
+
set `type` and `statusCode` for you so you only pass `header` and `body`:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { createNotFoundErrorResponse } from "./generated";
|
|
365
|
+
|
|
366
|
+
throw createNotFoundErrorResponse({
|
|
367
|
+
header: { "Content-Type": "application/json" },
|
|
368
|
+
body: { message: "Resource not found", code: "NOT_FOUND_ERROR" },
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
When `handleHttpResponseErrors` is `true` (the default), thrown typed HTTP responses
|
|
373
|
+
(`ITypedHttpResponse`) are returned as-is. No extra configuration needed.
|
|
355
374
|
|
|
356
375
|
#### Custom error mapping
|
|
357
376
|
|
|
@@ -362,21 +381,21 @@ Use custom handler functions to transform errors into your own response shape.
|
|
|
362
381
|
```ts
|
|
363
382
|
new UserRouter({
|
|
364
383
|
requestHandlers: userHandlers,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
},
|
|
384
|
+
handleRequestValidationErrors: (error, ctx) => ({
|
|
385
|
+
type: "ValidationError",
|
|
386
|
+
statusCode: HttpStatusCode.BAD_REQUEST,
|
|
387
|
+
header: { "Content-Type": "application/json" },
|
|
388
|
+
body: {
|
|
389
|
+
code: "VALIDATION_ERROR",
|
|
390
|
+
message: "Request is invalid",
|
|
391
|
+
issues: {
|
|
392
|
+
body: error.bodyIssues,
|
|
393
|
+
query: error.queryIssues,
|
|
394
|
+
param: error.pathParamIssues,
|
|
395
|
+
header: error.headerIssues,
|
|
378
396
|
},
|
|
379
|
-
}
|
|
397
|
+
},
|
|
398
|
+
}),
|
|
380
399
|
});
|
|
381
400
|
```
|
|
382
401
|
|
|
@@ -402,25 +421,27 @@ new UserRouter({
|
|
|
402
421
|
requestHandlers: userHandlers,
|
|
403
422
|
handleUnknownErrors: (error, ctx) => {
|
|
404
423
|
logger.error("Unhandled error", { error, path: ctx.request.path });
|
|
405
|
-
return
|
|
424
|
+
return {
|
|
425
|
+
type: "InternalServerError",
|
|
406
426
|
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
407
427
|
header: { "Content-Type": "application/json" },
|
|
408
428
|
body: { code: "INTERNAL_SERVER_ERROR", message: "Something went wrong" },
|
|
409
|
-
}
|
|
429
|
+
};
|
|
410
430
|
},
|
|
411
431
|
});
|
|
412
432
|
```
|
|
413
433
|
|
|
414
434
|
### 📋 Error Responses
|
|
415
435
|
|
|
416
|
-
| Status | Code | When
|
|
417
|
-
| ------ | ----------------------- |
|
|
418
|
-
| `400` | `BAD_REQUEST` | Malformed request body
|
|
419
|
-
| `400` | Validation issues | `
|
|
420
|
-
| `404` | `NOT_FOUND` | No matching route
|
|
421
|
-
| `405` | `METHOD_NOT_ALLOWED` | Route exists but method not allowed (includes `Allow` header)
|
|
422
|
-
| `413` | `PAYLOAD_TOO_LARGE` | Request body exceeds `maxBodySize`
|
|
423
|
-
| `500` | `INTERNAL_SERVER_ERROR` |
|
|
436
|
+
| Status | Code | When |
|
|
437
|
+
| ------ | ----------------------- | -------------------------------------------------------------------- |
|
|
438
|
+
| `400` | `BAD_REQUEST` | Malformed request body |
|
|
439
|
+
| `400` | Validation issues | `handleRequestValidationErrors: true` and request fails validation |
|
|
440
|
+
| `404` | `NOT_FOUND` | No matching route |
|
|
441
|
+
| `405` | `METHOD_NOT_ALLOWED` | Route exists but method not allowed (includes `Allow` header) |
|
|
442
|
+
| `413` | `PAYLOAD_TOO_LARGE` | Request body exceeds `maxBodySize` |
|
|
443
|
+
| `500` | `INTERNAL_SERVER_ERROR` | `handleResponseValidationErrors: true` and response fails validation |
|
|
444
|
+
| `500` | `INTERNAL_SERVER_ERROR` | Unhandled error in handler |
|
|
424
445
|
|
|
425
446
|
All error responses follow the shape: `{ code: string, message: string }`.
|
|
426
447
|
|
package/dist/lib/Router.ts
CHANGED
|
@@ -9,7 +9,10 @@ import type {
|
|
|
9
9
|
HttpMethod,
|
|
10
10
|
IHttpResponse,
|
|
11
11
|
IRequestValidator,
|
|
12
|
+
IResponseValidator,
|
|
13
|
+
ITypedHttpResponse,
|
|
12
14
|
RequestValidationError,
|
|
15
|
+
ResponseValidationError,
|
|
13
16
|
} from "@rexeus/typeweaver-core";
|
|
14
17
|
import type { RequestHandler } from "./RequestHandler";
|
|
15
18
|
import type { ServerContext } from "./ServerContext";
|
|
@@ -30,7 +33,8 @@ export type RouteDefinition = {
|
|
|
30
33
|
readonly operationId: string;
|
|
31
34
|
readonly method: HttpMethod;
|
|
32
35
|
readonly path: string;
|
|
33
|
-
readonly
|
|
36
|
+
readonly requestValidator: IRequestValidator;
|
|
37
|
+
readonly responseValidator: IResponseValidator;
|
|
34
38
|
readonly handler: RequestHandler<any, any, any>;
|
|
35
39
|
/** Reference to the router config for error handling. */
|
|
36
40
|
readonly routerConfig: RouterErrorConfig;
|
|
@@ -41,28 +45,44 @@ export type RouteDefinition = {
|
|
|
41
45
|
*/
|
|
42
46
|
export type RouterErrorConfig = {
|
|
43
47
|
readonly validateRequests: boolean;
|
|
48
|
+
readonly validateResponses: boolean;
|
|
44
49
|
readonly handleHttpResponseErrors: HttpResponseErrorHandler | boolean;
|
|
45
|
-
readonly
|
|
50
|
+
readonly handleRequestValidationErrors:
|
|
51
|
+
| RequestValidationErrorHandler
|
|
52
|
+
| boolean;
|
|
53
|
+
readonly handleResponseValidationErrors:
|
|
54
|
+
| ResponseValidationErrorHandler
|
|
55
|
+
| boolean;
|
|
46
56
|
readonly handleUnknownErrors: UnknownErrorHandler | boolean;
|
|
47
57
|
};
|
|
48
58
|
|
|
49
59
|
/**
|
|
50
60
|
* Handles HTTP response errors thrown by request handlers.
|
|
51
|
-
* The error parameter is
|
|
61
|
+
* The error parameter is a typed HTTP response object (thrown via `throw { type, statusCode, ... }`).
|
|
52
62
|
*/
|
|
53
63
|
export type HttpResponseErrorHandler = (
|
|
54
|
-
error:
|
|
64
|
+
error: ITypedHttpResponse,
|
|
55
65
|
ctx: ServerContext
|
|
56
66
|
) => Promise<IHttpResponse> | IHttpResponse;
|
|
57
67
|
|
|
58
68
|
/**
|
|
59
69
|
* Handles request validation errors.
|
|
60
70
|
*/
|
|
61
|
-
export type
|
|
71
|
+
export type RequestValidationErrorHandler = (
|
|
62
72
|
error: RequestValidationError,
|
|
63
73
|
ctx: ServerContext
|
|
64
74
|
) => Promise<IHttpResponse> | IHttpResponse;
|
|
65
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Handles response validation errors.
|
|
78
|
+
* Called when a handler returns a response that does not match the expected schema.
|
|
79
|
+
*/
|
|
80
|
+
export type ResponseValidationErrorHandler = (
|
|
81
|
+
error: ResponseValidationError,
|
|
82
|
+
response: IHttpResponse,
|
|
83
|
+
ctx: ServerContext
|
|
84
|
+
) => Promise<IHttpResponse> | IHttpResponse;
|
|
85
|
+
|
|
66
86
|
/**
|
|
67
87
|
* Handles any unknown errors not caught by other handlers.
|
|
68
88
|
*/
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* @generated by @rexeus/typeweaver
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isTypedHttpResponse,
|
|
10
|
+
RequestValidationError,
|
|
11
|
+
} from "@rexeus/typeweaver-core";
|
|
9
12
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
10
13
|
import { BodyParseError, PayloadTooLargeError } from "./Errors";
|
|
11
14
|
import { FetchApiAdapter } from "./FetchApiAdapter";
|
|
@@ -16,10 +19,11 @@ import type { Middleware } from "./Middleware";
|
|
|
16
19
|
import type { RequestHandler } from "./RequestHandler";
|
|
17
20
|
import type {
|
|
18
21
|
HttpResponseErrorHandler,
|
|
22
|
+
ResponseValidationErrorHandler,
|
|
19
23
|
RouteDefinition,
|
|
20
24
|
RouteMatch,
|
|
21
25
|
UnknownErrorHandler,
|
|
22
|
-
|
|
26
|
+
RequestValidationErrorHandler,
|
|
23
27
|
} from "./Router";
|
|
24
28
|
import type { ServerContext } from "./ServerContext";
|
|
25
29
|
import type { StateRequirementError, TypedMiddleware } from "./TypedMiddleware";
|
|
@@ -224,8 +228,15 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
224
228
|
if (match) {
|
|
225
229
|
const routeCtx = this.withPathParams(ctx, match.params);
|
|
226
230
|
try {
|
|
227
|
-
|
|
231
|
+
const response = await this.executeHandler(routeCtx, match.route);
|
|
232
|
+
return await this.validateResponse(match.route, response, routeCtx);
|
|
228
233
|
} catch (error) {
|
|
234
|
+
if (
|
|
235
|
+
isTypedHttpResponse(error) &&
|
|
236
|
+
match.route.routerConfig.validateResponses
|
|
237
|
+
) {
|
|
238
|
+
return await this.validateResponse(match.route, error, routeCtx);
|
|
239
|
+
}
|
|
229
240
|
return this.handleError(error, routeCtx, match.route);
|
|
230
241
|
}
|
|
231
242
|
}
|
|
@@ -261,44 +272,123 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
261
272
|
route: RouteDefinition
|
|
262
273
|
): Promise<IHttpResponse> {
|
|
263
274
|
const validatedRequest = route.routerConfig.validateRequests
|
|
264
|
-
? route.
|
|
275
|
+
? route.requestValidator.validate(ctx.request)
|
|
265
276
|
: ctx.request;
|
|
266
277
|
|
|
267
278
|
return route.handler(validatedRequest, ctx);
|
|
268
279
|
}
|
|
269
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Validates a response against the operation's response validator.
|
|
283
|
+
*
|
|
284
|
+
* Behavior depends on configuration:
|
|
285
|
+
* - `validateResponses: false` → returns the original response unchanged.
|
|
286
|
+
* - `validateResponses: true` (default) → runs validation:
|
|
287
|
+
* - Valid response → returns the stripped response (extra fields removed).
|
|
288
|
+
* - Invalid response + handler configured → calls the handler safely.
|
|
289
|
+
* If the handler throws, falls back to the original response.
|
|
290
|
+
* - Invalid response + `handleResponseValidationErrors: false` → returns
|
|
291
|
+
* the original (invalid) response as-is.
|
|
292
|
+
*
|
|
293
|
+
* @param route - The route definition containing the response validator and config
|
|
294
|
+
* @param response - The response to validate
|
|
295
|
+
* @param ctx - The server context for the current request
|
|
296
|
+
* @returns The validated (and stripped) response, the handler's response, or the original
|
|
297
|
+
*/
|
|
298
|
+
private async validateResponse(
|
|
299
|
+
route: RouteDefinition,
|
|
300
|
+
response: IHttpResponse,
|
|
301
|
+
ctx: ServerContext
|
|
302
|
+
): Promise<IHttpResponse> {
|
|
303
|
+
if (!route.routerConfig.validateResponses) return response;
|
|
304
|
+
|
|
305
|
+
const result = route.responseValidator.safeValidate(response);
|
|
306
|
+
|
|
307
|
+
if (result.isValid) return result.data;
|
|
308
|
+
|
|
309
|
+
const handler = this.resolveErrorHandler<ResponseValidationErrorHandler>(
|
|
310
|
+
route.routerConfig.handleResponseValidationErrors,
|
|
311
|
+
TypeweaverApp.defaultResponseValidationHandler
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (handler) {
|
|
315
|
+
const handlerResponse = await this.safelyExecuteErrorHandler(() =>
|
|
316
|
+
handler(result.error, response, ctx)
|
|
317
|
+
);
|
|
318
|
+
if (handlerResponse) return handlerResponse;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return response;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Safely executes an error handler and returns null if it fails.
|
|
326
|
+
* This allows for graceful fallback to the next handler in the chain
|
|
327
|
+
* without crashing the request pipeline.
|
|
328
|
+
*
|
|
329
|
+
* If the handler throws, the error is reported via `safeOnError`
|
|
330
|
+
* and null is returned so the caller can fall through to the next handler.
|
|
331
|
+
*
|
|
332
|
+
* @param handlerFn - Function that executes the error handler
|
|
333
|
+
* @returns The handler's response if successful, null if the handler throws
|
|
334
|
+
*/
|
|
335
|
+
private async safelyExecuteErrorHandler(
|
|
336
|
+
handlerFn: () => Promise<IHttpResponse> | IHttpResponse
|
|
337
|
+
): Promise<IHttpResponse | null> {
|
|
338
|
+
try {
|
|
339
|
+
return await handlerFn();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
this.safeOnError(error);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
270
346
|
/**
|
|
271
347
|
* Handle errors using the route's configured error handlers.
|
|
272
|
-
* Handler errors bubble up to the safety net in `fetch()`.
|
|
273
348
|
*/
|
|
274
|
-
private handleError(
|
|
349
|
+
private async handleError(
|
|
275
350
|
error: unknown,
|
|
276
351
|
ctx: ServerContext,
|
|
277
352
|
route: RouteDefinition
|
|
278
|
-
):
|
|
353
|
+
): Promise<IHttpResponse> {
|
|
279
354
|
const config = route.routerConfig;
|
|
280
355
|
|
|
281
356
|
if (error instanceof RequestValidationError) {
|
|
282
|
-
const handler = this.resolveErrorHandler<
|
|
283
|
-
config.
|
|
284
|
-
TypeweaverApp.
|
|
357
|
+
const handler = this.resolveErrorHandler<RequestValidationErrorHandler>(
|
|
358
|
+
config.handleRequestValidationErrors,
|
|
359
|
+
TypeweaverApp.defaultRequestValidationHandler
|
|
285
360
|
);
|
|
286
|
-
if (handler)
|
|
361
|
+
if (handler) {
|
|
362
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
363
|
+
handler(error, ctx)
|
|
364
|
+
);
|
|
365
|
+
if (response) return response;
|
|
366
|
+
}
|
|
287
367
|
}
|
|
288
368
|
|
|
289
|
-
if (error
|
|
369
|
+
if (isTypedHttpResponse(error)) {
|
|
290
370
|
const handler = this.resolveErrorHandler<HttpResponseErrorHandler>(
|
|
291
371
|
config.handleHttpResponseErrors,
|
|
292
372
|
TypeweaverApp.defaultHttpResponseHandler
|
|
293
373
|
);
|
|
294
|
-
if (handler)
|
|
374
|
+
if (handler) {
|
|
375
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
376
|
+
handler(error, ctx)
|
|
377
|
+
);
|
|
378
|
+
if (response) return response;
|
|
379
|
+
}
|
|
295
380
|
}
|
|
296
381
|
|
|
297
382
|
const handler = this.resolveErrorHandler<UnknownErrorHandler>(
|
|
298
383
|
config.handleUnknownErrors,
|
|
299
384
|
this.defaultUnknownHandler
|
|
300
385
|
);
|
|
301
|
-
if (handler)
|
|
386
|
+
if (handler) {
|
|
387
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
388
|
+
handler(error, ctx)
|
|
389
|
+
);
|
|
390
|
+
if (response) return response;
|
|
391
|
+
}
|
|
302
392
|
|
|
303
393
|
throw error;
|
|
304
394
|
}
|
|
@@ -339,30 +429,35 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
339
429
|
return issues.map(({ message, path }) => ({ message, path }));
|
|
340
430
|
}
|
|
341
431
|
|
|
342
|
-
private static
|
|
343
|
-
err
|
|
344
|
-
|
|
345
|
-
const issues: Record<string, unknown> = Object.create(null);
|
|
432
|
+
private static defaultRequestValidationHandler: RequestValidationErrorHandler =
|
|
433
|
+
(err): IHttpResponse => {
|
|
434
|
+
const issues: Record<string, unknown> = Object.create(null);
|
|
346
435
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
436
|
+
const header = TypeweaverApp.sanitizeIssues(err.headerIssues);
|
|
437
|
+
const body = TypeweaverApp.sanitizeIssues(err.bodyIssues);
|
|
438
|
+
const query = TypeweaverApp.sanitizeIssues(err.queryIssues);
|
|
439
|
+
const param = TypeweaverApp.sanitizeIssues(err.pathParamIssues);
|
|
351
440
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
441
|
+
if (header) issues.header = header;
|
|
442
|
+
if (body) issues.body = body;
|
|
443
|
+
if (query) issues.query = query;
|
|
444
|
+
if (param) issues.param = param;
|
|
356
445
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
446
|
+
return {
|
|
447
|
+
statusCode: 400,
|
|
448
|
+
body: {
|
|
449
|
+
code: "VALIDATION_ERROR",
|
|
450
|
+
message: err.message,
|
|
451
|
+
issues,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
364
454
|
};
|
|
365
|
-
|
|
455
|
+
|
|
456
|
+
private static defaultResponseValidationHandler: ResponseValidationErrorHandler =
|
|
457
|
+
(): IHttpResponse => ({
|
|
458
|
+
statusCode: 500,
|
|
459
|
+
body: TypeweaverApp.INTERNAL_SERVER_ERROR_BODY,
|
|
460
|
+
});
|
|
366
461
|
|
|
367
462
|
private static defaultHttpResponseHandler: HttpResponseErrorHandler = (
|
|
368
463
|
err
|
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
* @generated by @rexeus/typeweaver
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
HttpMethod,
|
|
12
|
+
IRequestValidator,
|
|
13
|
+
IResponseValidator,
|
|
14
|
+
} from "@rexeus/typeweaver-core";
|
|
11
15
|
import type { RequestHandler } from "./RequestHandler";
|
|
12
16
|
import type {
|
|
13
17
|
HttpResponseErrorHandler,
|
|
18
|
+
ResponseValidationErrorHandler,
|
|
14
19
|
RouteDefinition,
|
|
15
20
|
RouterErrorConfig,
|
|
16
21
|
UnknownErrorHandler,
|
|
17
|
-
|
|
22
|
+
RequestValidationErrorHandler,
|
|
18
23
|
} from "./Router";
|
|
19
24
|
|
|
20
25
|
/**
|
|
@@ -39,22 +44,42 @@ export type TypeweaverRouterOptions<
|
|
|
39
44
|
readonly validateRequests?: boolean;
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* - `false`: Disable this handler (errors fall through to the unknown error handler)
|
|
45
|
-
* - `function`: Use custom error handler
|
|
47
|
+
* Enable response validation using generated validators.
|
|
48
|
+
* When true, responses are validated and stripped of extra fields before sending.
|
|
46
49
|
* @default true
|
|
47
50
|
*/
|
|
48
|
-
readonly
|
|
51
|
+
readonly validateResponses?: boolean;
|
|
49
52
|
|
|
50
53
|
/**
|
|
51
54
|
* Configure handling of request validation errors.
|
|
52
55
|
* - `true`: Use default handler (400 with error details)
|
|
53
56
|
* - `false`: Disable this handler (errors fall through to the unknown error handler)
|
|
57
|
+
* - `function`: Use custom request validation error handler
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
readonly handleRequestValidationErrors?:
|
|
61
|
+
| RequestValidationErrorHandler
|
|
62
|
+
| boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configure handling of response validation errors.
|
|
66
|
+
* - `true`: Use default handler (500 Internal Server Error)
|
|
67
|
+
* - `false`: Disable response validation error handling (return response as-is)
|
|
68
|
+
* - `function`: Use custom response validation error handler
|
|
69
|
+
* @default true
|
|
70
|
+
*/
|
|
71
|
+
readonly handleResponseValidationErrors?:
|
|
72
|
+
| ResponseValidationErrorHandler
|
|
73
|
+
| boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Configure handling of HttpResponse errors thrown by handlers.
|
|
77
|
+
* - `true`: Use default handler (returns the error as response)
|
|
78
|
+
* - `false`: Disable this handler (errors fall through to the unknown error handler)
|
|
54
79
|
* - `function`: Use custom error handler
|
|
55
80
|
* @default true
|
|
56
81
|
*/
|
|
57
|
-
readonly
|
|
82
|
+
readonly handleHttpResponseErrors?: HttpResponseErrorHandler | boolean;
|
|
58
83
|
|
|
59
84
|
/**
|
|
60
85
|
* Configure handling of unknown errors.
|
|
@@ -91,8 +116,10 @@ export abstract class TypeweaverRouter<
|
|
|
91
116
|
const {
|
|
92
117
|
requestHandlers,
|
|
93
118
|
validateRequests = true,
|
|
119
|
+
validateResponses = true,
|
|
94
120
|
handleHttpResponseErrors = true,
|
|
95
|
-
|
|
121
|
+
handleRequestValidationErrors = true,
|
|
122
|
+
handleResponseValidationErrors = true,
|
|
96
123
|
handleUnknownErrors = true,
|
|
97
124
|
} = options;
|
|
98
125
|
|
|
@@ -100,8 +127,10 @@ export abstract class TypeweaverRouter<
|
|
|
100
127
|
|
|
101
128
|
this.errorConfig = {
|
|
102
129
|
validateRequests,
|
|
130
|
+
validateResponses,
|
|
103
131
|
handleHttpResponseErrors,
|
|
104
|
-
|
|
132
|
+
handleRequestValidationErrors,
|
|
133
|
+
handleResponseValidationErrors,
|
|
105
134
|
handleUnknownErrors,
|
|
106
135
|
};
|
|
107
136
|
}
|
|
@@ -113,20 +142,23 @@ export abstract class TypeweaverRouter<
|
|
|
113
142
|
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
114
143
|
* @param path - Path pattern with `:param` placeholders
|
|
115
144
|
* @param validator - Request validator for this operation
|
|
145
|
+
* @param responseValidator - Response validator for this operation
|
|
116
146
|
* @param handler - Type-safe request handler
|
|
117
147
|
*/
|
|
118
148
|
protected route(
|
|
119
149
|
operationId: string,
|
|
120
150
|
method: HttpMethod,
|
|
121
151
|
path: string,
|
|
122
|
-
|
|
152
|
+
requestValidator: IRequestValidator,
|
|
153
|
+
responseValidator: IResponseValidator,
|
|
123
154
|
handler: RequestHandler<any, any, any>
|
|
124
155
|
): void {
|
|
125
156
|
this.routes.push({
|
|
126
157
|
operationId,
|
|
127
158
|
method,
|
|
128
159
|
path,
|
|
129
|
-
|
|
160
|
+
requestValidator,
|
|
161
|
+
responseValidator,
|
|
130
162
|
handler,
|
|
131
163
|
routerConfig: this.errorConfig,
|
|
132
164
|
});
|
package/dist/lib/index.ts
CHANGED
|
@@ -15,9 +15,10 @@ export {
|
|
|
15
15
|
export { HttpMethod } from "@rexeus/typeweaver-core";
|
|
16
16
|
export type {
|
|
17
17
|
HttpResponseErrorHandler,
|
|
18
|
+
RequestValidationErrorHandler,
|
|
19
|
+
ResponseValidationErrorHandler,
|
|
18
20
|
RouteMetadata,
|
|
19
21
|
UnknownErrorHandler,
|
|
20
|
-
ValidationErrorHandler,
|
|
21
22
|
} from "./Router";
|
|
22
23
|
export type { ServerContext } from "./ServerContext";
|
|
23
24
|
export type { RequestHandler } from "./RequestHandler";
|
|
@@ -11,6 +11,7 @@ import { HttpMethod, TypeweaverRouter, type RequestHandler, type TypeweaverRoute
|
|
|
11
11
|
import type { I<%- operation.className %>Request } from "./<%- operation.className %>Request";
|
|
12
12
|
import { <%- operation.className %>RequestValidator } from "./<%- operation.className %>RequestValidator";
|
|
13
13
|
import type { <%- operation.className %>Response } from "./<%- operation.className %>Response";
|
|
14
|
+
import { <%- operation.className %>ResponseValidator } from "./<%- operation.className %>ResponseValidator";
|
|
14
15
|
<% } %>
|
|
15
16
|
|
|
16
17
|
export type Server<%- pascalCaseEntityName %>ApiHandler<
|
|
@@ -36,6 +37,7 @@ export class <%- pascalCaseEntityName %>Router<
|
|
|
36
37
|
HttpMethod.<%- operation.method %>,
|
|
37
38
|
'<%- operation.path %>',
|
|
38
39
|
new <%- operation.className %>RequestValidator(),
|
|
40
|
+
new <%- operation.className %>ResponseValidator(),
|
|
39
41
|
this.requestHandlers.<%- operation.handlerName %>.bind(this.requestHandlers)
|
|
40
42
|
);
|
|
41
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexeus/typeweaver-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Generates a lightweight, dependency-free server with built-in routing and middleware from your API definitions. Powered by Typeweaver.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -47,21 +47,21 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@rexeus/typeweaver-
|
|
51
|
-
"@rexeus/typeweaver-
|
|
50
|
+
"@rexeus/typeweaver-gen": "^0.8.0",
|
|
51
|
+
"@rexeus/typeweaver-core": "^0.8.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"get-port": "^7.1.0",
|
|
55
55
|
"test-utils": "file:../test-utils",
|
|
56
56
|
"tsx": "^4.21.0",
|
|
57
|
-
"@rexeus/typeweaver-core": "^0.
|
|
58
|
-
"@rexeus/typeweaver-gen": "^0.
|
|
57
|
+
"@rexeus/typeweaver-core": "^0.8.0",
|
|
58
|
+
"@rexeus/typeweaver-gen": "^0.8.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"case": "^1.6.3"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
|
-
"typecheck": "tsc --noEmit",
|
|
64
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
65
65
|
"format": "oxfmt",
|
|
66
66
|
"build": "tsdown && mkdir -p ./dist/templates ./dist/lib && cp -r ./src/templates/* ./dist/templates/ && cp -r ./src/lib/* ./dist/lib/ && cp ../../LICENSE ../../NOTICE ./dist/",
|
|
67
67
|
"test": "vitest --run",
|