@rexeus/typeweaver-server 0.7.0 → 0.9.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 +66 -45
- package/dist/index.cjs +43 -78
- package/dist/index.mjs +38 -67
- package/dist/index.mjs.map +1 -1
- package/dist/lib/NodeAdapter.ts +13 -10
- package/dist/lib/Router.ts +25 -5
- package/dist/lib/TypeweaverApp.ts +151 -62
- package/dist/lib/TypeweaverRouter.ts +44 -12
- package/dist/lib/index.ts +2 -1
- package/dist/lib/middleware/basicAuth.ts +6 -4
- package/dist/lib/middleware/bearerAuth.ts +6 -4
- package/dist/templates/Router.ejs +2 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ npm install @rexeus/typeweaver-core
|
|
|
46
46
|
## 💡 How to use
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
npx typeweaver generate --input ./api/
|
|
49
|
+
npx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins server
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
More on the CLI in
|
|
@@ -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/index.cjs
CHANGED
|
@@ -6,16 +6,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
19
15
|
}
|
|
20
16
|
return to;
|
|
21
17
|
};
|
|
@@ -23,7 +19,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
19
|
value: mod,
|
|
24
20
|
enumerable: true
|
|
25
21
|
}) : target, mod));
|
|
26
|
-
|
|
27
22
|
//#endregion
|
|
28
23
|
let node_path = require("node:path");
|
|
29
24
|
node_path = __toESM(node_path);
|
|
@@ -32,77 +27,48 @@ let _rexeus_typeweaver_gen = require("@rexeus/typeweaver-gen");
|
|
|
32
27
|
let _rexeus_typeweaver_core = require("@rexeus/typeweaver-core");
|
|
33
28
|
let case$1 = require("case");
|
|
34
29
|
case$1 = __toESM(case$1);
|
|
35
|
-
|
|
36
|
-
//#region src/RouterGenerator.ts
|
|
30
|
+
//#region src/routerGenerator.ts
|
|
37
31
|
/**
|
|
38
32
|
* Generates TypeweaverRouter subclasses from API definitions.
|
|
39
33
|
*
|
|
40
34
|
* For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`
|
|
41
35
|
* file that extends `TypeweaverRouter` and registers all operations as routes.
|
|
42
36
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
path: resource.definition.path
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
static compareRoutes(a, b) {
|
|
80
|
-
const aSegments = a.path.split("/").filter((s) => s);
|
|
81
|
-
const bSegments = b.path.split("/").filter((s) => s);
|
|
82
|
-
if (aSegments.length !== bSegments.length) return aSegments.length - bSegments.length;
|
|
83
|
-
for (let i = 0; i < aSegments.length; i++) {
|
|
84
|
-
const aSegment = aSegments[i];
|
|
85
|
-
const bSegment = bSegments[i];
|
|
86
|
-
const aIsParam = aSegment.startsWith(":");
|
|
87
|
-
if (aIsParam !== bSegment.startsWith(":")) return aIsParam ? 1 : -1;
|
|
88
|
-
if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
|
|
89
|
-
}
|
|
90
|
-
return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);
|
|
91
|
-
}
|
|
92
|
-
static METHOD_PRIORITY = {
|
|
93
|
-
GET: 1,
|
|
94
|
-
POST: 2,
|
|
95
|
-
PUT: 3,
|
|
96
|
-
PATCH: 4,
|
|
97
|
-
DELETE: 5,
|
|
98
|
-
OPTIONS: 6,
|
|
99
|
-
HEAD: 7
|
|
37
|
+
/**
|
|
38
|
+
* Generates router files for all resources in the given context.
|
|
39
|
+
*
|
|
40
|
+
* @param context - The generator context containing resources, templates, and output configuration
|
|
41
|
+
*/
|
|
42
|
+
function generate(context) {
|
|
43
|
+
const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
44
|
+
const templateFile = node_path.default.join(moduleDir, "templates", "Router.ejs");
|
|
45
|
+
for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
|
|
46
|
+
}
|
|
47
|
+
function writeRouter(resource, templateFile, context) {
|
|
48
|
+
const pascalCaseEntityName = case$1.default.pascal(resource.name);
|
|
49
|
+
const outputDir = context.getResourceOutputDir(resource.name);
|
|
50
|
+
const outputPath = node_path.default.join(outputDir, `${pascalCaseEntityName}Router.ts`);
|
|
51
|
+
const operations = resource.operations.filter((operation) => operation.method !== _rexeus_typeweaver_core.HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => (0, _rexeus_typeweaver_gen.compareRoutes)(a, b));
|
|
52
|
+
const content = context.renderTemplate(templateFile, {
|
|
53
|
+
coreDir: (0, _rexeus_typeweaver_gen.relative)(outputDir, context.outputDir),
|
|
54
|
+
entityName: resource.name,
|
|
55
|
+
pascalCaseEntityName,
|
|
56
|
+
operations
|
|
57
|
+
});
|
|
58
|
+
const relativePath = node_path.default.relative(context.outputDir, outputPath);
|
|
59
|
+
context.writeFile(relativePath, content);
|
|
60
|
+
}
|
|
61
|
+
function createOperationData(operation) {
|
|
62
|
+
const operationId = operation.operationId;
|
|
63
|
+
const className = case$1.default.pascal(operationId);
|
|
64
|
+
return {
|
|
65
|
+
operationId,
|
|
66
|
+
className,
|
|
67
|
+
handlerName: `handle${className}Request`,
|
|
68
|
+
method: operation.method,
|
|
69
|
+
path: operation.path
|
|
100
70
|
};
|
|
101
|
-
|
|
102
|
-
return this.METHOD_PRIORITY[method] ?? 999;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
71
|
+
}
|
|
106
72
|
//#endregion
|
|
107
73
|
//#region src/index.ts
|
|
108
74
|
const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
@@ -123,9 +89,8 @@ var ServerPlugin = class extends _rexeus_typeweaver_gen.BasePlugin {
|
|
|
123
89
|
generate(context) {
|
|
124
90
|
const libSourceDir = node_path.default.join(moduleDir, "lib");
|
|
125
91
|
this.copyLibFiles(context, libSourceDir, this.name);
|
|
126
|
-
|
|
92
|
+
generate(context);
|
|
127
93
|
}
|
|
128
94
|
};
|
|
129
|
-
|
|
130
95
|
//#endregion
|
|
131
|
-
module.exports = ServerPlugin;
|
|
96
|
+
module.exports = ServerPlugin;
|
package/dist/index.mjs
CHANGED
|
@@ -1,79 +1,50 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { BasePlugin,
|
|
3
|
+
import { BasePlugin, compareRoutes, relative } from "@rexeus/typeweaver-gen";
|
|
4
4
|
import { HttpMethod } from "@rexeus/typeweaver-core";
|
|
5
5
|
import Case from "case";
|
|
6
|
-
|
|
7
|
-
//#region src/RouterGenerator.ts
|
|
6
|
+
//#region src/routerGenerator.ts
|
|
8
7
|
/**
|
|
9
8
|
* Generates TypeweaverRouter subclasses from API definitions.
|
|
10
9
|
*
|
|
11
10
|
* For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`
|
|
12
11
|
* file that extends `TypeweaverRouter` and registers all operations as routes.
|
|
13
12
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
path: resource.definition.path
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
static compareRoutes(a, b) {
|
|
51
|
-
const aSegments = a.path.split("/").filter((s) => s);
|
|
52
|
-
const bSegments = b.path.split("/").filter((s) => s);
|
|
53
|
-
if (aSegments.length !== bSegments.length) return aSegments.length - bSegments.length;
|
|
54
|
-
for (let i = 0; i < aSegments.length; i++) {
|
|
55
|
-
const aSegment = aSegments[i];
|
|
56
|
-
const bSegment = bSegments[i];
|
|
57
|
-
const aIsParam = aSegment.startsWith(":");
|
|
58
|
-
if (aIsParam !== bSegment.startsWith(":")) return aIsParam ? 1 : -1;
|
|
59
|
-
if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
|
|
60
|
-
}
|
|
61
|
-
return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);
|
|
62
|
-
}
|
|
63
|
-
static METHOD_PRIORITY = {
|
|
64
|
-
GET: 1,
|
|
65
|
-
POST: 2,
|
|
66
|
-
PUT: 3,
|
|
67
|
-
PATCH: 4,
|
|
68
|
-
DELETE: 5,
|
|
69
|
-
OPTIONS: 6,
|
|
70
|
-
HEAD: 7
|
|
13
|
+
/**
|
|
14
|
+
* Generates router files for all resources in the given context.
|
|
15
|
+
*
|
|
16
|
+
* @param context - The generator context containing resources, templates, and output configuration
|
|
17
|
+
*/
|
|
18
|
+
function generate(context) {
|
|
19
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const templateFile = path.join(moduleDir, "templates", "Router.ejs");
|
|
21
|
+
for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
|
|
22
|
+
}
|
|
23
|
+
function writeRouter(resource, templateFile, context) {
|
|
24
|
+
const pascalCaseEntityName = Case.pascal(resource.name);
|
|
25
|
+
const outputDir = context.getResourceOutputDir(resource.name);
|
|
26
|
+
const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);
|
|
27
|
+
const operations = resource.operations.filter((operation) => operation.method !== HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => compareRoutes(a, b));
|
|
28
|
+
const content = context.renderTemplate(templateFile, {
|
|
29
|
+
coreDir: relative(outputDir, context.outputDir),
|
|
30
|
+
entityName: resource.name,
|
|
31
|
+
pascalCaseEntityName,
|
|
32
|
+
operations
|
|
33
|
+
});
|
|
34
|
+
const relativePath = path.relative(context.outputDir, outputPath);
|
|
35
|
+
context.writeFile(relativePath, content);
|
|
36
|
+
}
|
|
37
|
+
function createOperationData(operation) {
|
|
38
|
+
const operationId = operation.operationId;
|
|
39
|
+
const className = Case.pascal(operationId);
|
|
40
|
+
return {
|
|
41
|
+
operationId,
|
|
42
|
+
className,
|
|
43
|
+
handlerName: `handle${className}Request`,
|
|
44
|
+
method: operation.method,
|
|
45
|
+
path: operation.path
|
|
71
46
|
};
|
|
72
|
-
|
|
73
|
-
return this.METHOD_PRIORITY[method] ?? 999;
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
47
|
+
}
|
|
77
48
|
//#endregion
|
|
78
49
|
//#region src/index.ts
|
|
79
50
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -94,10 +65,10 @@ var ServerPlugin = class extends BasePlugin {
|
|
|
94
65
|
generate(context) {
|
|
95
66
|
const libSourceDir = path.join(moduleDir, "lib");
|
|
96
67
|
this.copyLibFiles(context, libSourceDir, this.name);
|
|
97
|
-
|
|
68
|
+
generate(context);
|
|
98
69
|
}
|
|
99
70
|
};
|
|
100
|
-
|
|
101
71
|
//#endregion
|
|
102
72
|
export { ServerPlugin as default };
|
|
73
|
+
|
|
103
74
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes, relative } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\nimport Case from \"case\";\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\n\n/**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\nexport function generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const resource of context.normalizedSpec.resources) {\n writeRouter(resource, templateFile, context);\n }\n}\n\nfunction writeRouter(\n resource: NormalizedResource,\n templateFile: string,\n context: GeneratorContext\n): void {\n const pascalCaseEntityName = Case.pascal(resource.name);\n const outputDir = context.getResourceOutputDir(resource.name);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = resource.operations\n .filter(operation => operation.method !== HttpMethod.HEAD)\n .map(operation => createOperationData(operation))\n .sort((a, b) => compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: relative(outputDir, context.outputDir),\n entityName: resource.name,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n}\n\nfunction createOperationData(operation: NormalizedOperation): OperationData {\n const operationId = operation.operationId;\n const className = Case.pascal(operationId);\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n method: operation.method,\n path: operation.path,\n };\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { generate as generateRouters } from \"./routerGenerator\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport default class ServerPlugin extends BasePlugin {\n public name = \"server\";\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n generateRouters(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,SAAgB,SAAS,SAAiC;CACxD,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,MAAK,MAAM,YAAY,QAAQ,eAAe,UAC5C,aAAY,UAAU,cAAc,QAAQ;;AAIhD,SAAS,YACP,UACA,cACA,SACM;CACN,MAAM,uBAAuB,KAAK,OAAO,SAAS,KAAK;CACvD,MAAM,YAAY,QAAQ,qBAAqB,SAAS,KAAK;CAC7D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;CAE3E,MAAM,aAAa,SAAS,WACzB,QAAO,cAAa,UAAU,WAAW,WAAW,KAAK,CACzD,KAAI,cAAa,oBAAoB,UAAU,CAAC,CAChD,MAAM,GAAG,MAAM,cAAc,GAAG,EAAE,CAAC;CAEtC,MAAM,UAAU,QAAQ,eAAe,cAAc;EACnD,SAAS,SAAS,WAAW,QAAQ,UAAU;EAC/C,YAAY,SAAS;EACrB;EACA;EACD,CAAC;CAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,SAAQ,UAAU,cAAc,QAAQ;;AAG1C,SAAS,oBAAoB,WAA+C;CAC1E,MAAM,cAAc,UAAU;CAC9B,MAAM,YAAY,KAAK,OAAO,YAAY;AAE1C,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACrEH,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAqB,eAArB,cAA0C,WAAW;CACnD,OAAc;;;;;;CAOd,SAAyB,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,WAAgB,QAAQ"}
|
package/dist/lib/NodeAdapter.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* @generated by @rexeus/typeweaver
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
createDefaultErrorBody,
|
|
10
|
+
internalServerErrorDefaultError,
|
|
11
|
+
payloadTooLargeDefaultError,
|
|
12
|
+
} from "@rexeus/typeweaver-core";
|
|
8
13
|
import { PayloadTooLargeError } from "./Errors";
|
|
9
14
|
import type { TypeweaverApp } from "./TypeweaverApp";
|
|
10
15
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -73,26 +78,24 @@ async function handleRequest(
|
|
|
73
78
|
} catch (error) {
|
|
74
79
|
if (error instanceof PayloadTooLargeError) {
|
|
75
80
|
if (!res.headersSent) {
|
|
76
|
-
res.writeHead(
|
|
81
|
+
res.writeHead(payloadTooLargeDefaultError.statusCode, {
|
|
82
|
+
"content-type": "application/json",
|
|
83
|
+
});
|
|
77
84
|
}
|
|
78
85
|
res.end(
|
|
79
|
-
JSON.stringify(
|
|
80
|
-
code: "PAYLOAD_TOO_LARGE",
|
|
81
|
-
message: "Request body exceeds the size limit",
|
|
82
|
-
})
|
|
86
|
+
JSON.stringify(createDefaultErrorBody(payloadTooLargeDefaultError))
|
|
83
87
|
);
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
console.error(error);
|
|
88
92
|
if (!res.headersSent) {
|
|
89
|
-
res.writeHead(
|
|
93
|
+
res.writeHead(internalServerErrorDefaultError.statusCode, {
|
|
94
|
+
"content-type": "application/json",
|
|
95
|
+
});
|
|
90
96
|
}
|
|
91
97
|
res.end(
|
|
92
|
-
JSON.stringify(
|
|
93
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
94
|
-
message: "An unexpected error occurred",
|
|
95
|
-
})
|
|
98
|
+
JSON.stringify(createDefaultErrorBody(internalServerErrorDefaultError))
|
|
96
99
|
);
|
|
97
100
|
}
|
|
98
101
|
}
|
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,18 @@
|
|
|
5
5
|
* @generated by @rexeus/typeweaver
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
badRequestDefaultError,
|
|
10
|
+
createDefaultErrorBody,
|
|
11
|
+
createDefaultErrorResponse,
|
|
12
|
+
internalServerErrorDefaultError,
|
|
13
|
+
isTypedHttpResponse,
|
|
14
|
+
methodNotAllowedDefaultError,
|
|
15
|
+
notFoundDefaultError,
|
|
16
|
+
payloadTooLargeDefaultError,
|
|
17
|
+
RequestValidationError,
|
|
18
|
+
validationDefaultError,
|
|
19
|
+
} from "@rexeus/typeweaver-core";
|
|
9
20
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
10
21
|
import { BodyParseError, PayloadTooLargeError } from "./Errors";
|
|
11
22
|
import { FetchApiAdapter } from "./FetchApiAdapter";
|
|
@@ -16,10 +27,11 @@ import type { Middleware } from "./Middleware";
|
|
|
16
27
|
import type { RequestHandler } from "./RequestHandler";
|
|
17
28
|
import type {
|
|
18
29
|
HttpResponseErrorHandler,
|
|
30
|
+
ResponseValidationErrorHandler,
|
|
19
31
|
RouteDefinition,
|
|
20
32
|
RouteMatch,
|
|
21
33
|
UnknownErrorHandler,
|
|
22
|
-
|
|
34
|
+
RequestValidationErrorHandler,
|
|
23
35
|
} from "./Router";
|
|
24
36
|
import type { ServerContext } from "./ServerContext";
|
|
25
37
|
import type { StateRequirementError, TypedMiddleware } from "./TypedMiddleware";
|
|
@@ -57,10 +69,9 @@ export type TypeweaverAppOptions = {
|
|
|
57
69
|
};
|
|
58
70
|
|
|
59
71
|
export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
60
|
-
private static readonly INTERNAL_SERVER_ERROR_BODY =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
} as const;
|
|
72
|
+
private static readonly INTERNAL_SERVER_ERROR_BODY = createDefaultErrorBody(
|
|
73
|
+
internalServerErrorDefaultError
|
|
74
|
+
);
|
|
64
75
|
|
|
65
76
|
private readonly router = new Router();
|
|
66
77
|
private readonly middlewares: Middleware[] = [];
|
|
@@ -164,19 +175,14 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
164
175
|
} catch (error) {
|
|
165
176
|
if (error instanceof PayloadTooLargeError) {
|
|
166
177
|
this.safeOnError(error);
|
|
167
|
-
return this.adapter.toResponse(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
code: "PAYLOAD_TOO_LARGE",
|
|
171
|
-
message: "Request body exceeds the size limit",
|
|
172
|
-
},
|
|
173
|
-
});
|
|
178
|
+
return this.adapter.toResponse(
|
|
179
|
+
createDefaultErrorResponse(payloadTooLargeDefaultError)
|
|
180
|
+
);
|
|
174
181
|
}
|
|
175
182
|
if (error instanceof BodyParseError) {
|
|
176
|
-
return this.adapter.toResponse(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
183
|
+
return this.adapter.toResponse(
|
|
184
|
+
createDefaultErrorResponse(badRequestDefaultError)
|
|
185
|
+
);
|
|
180
186
|
}
|
|
181
187
|
this.safeOnError(error);
|
|
182
188
|
return TypeweaverApp.createErrorResponse();
|
|
@@ -224,28 +230,27 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
224
230
|
if (match) {
|
|
225
231
|
const routeCtx = this.withPathParams(ctx, match.params);
|
|
226
232
|
try {
|
|
227
|
-
|
|
233
|
+
const response = await this.executeHandler(routeCtx, match.route);
|
|
234
|
+
return await this.validateResponse(match.route, response, routeCtx);
|
|
228
235
|
} catch (error) {
|
|
236
|
+
if (
|
|
237
|
+
isTypedHttpResponse(error) &&
|
|
238
|
+
match.route.routerConfig.validateResponses
|
|
239
|
+
) {
|
|
240
|
+
return await this.validateResponse(match.route, error, routeCtx);
|
|
241
|
+
}
|
|
229
242
|
return this.handleError(error, routeCtx, match.route);
|
|
230
243
|
}
|
|
231
244
|
}
|
|
232
245
|
|
|
233
246
|
const pathMatch = this.router.matchPath(pathname);
|
|
234
247
|
if (pathMatch) {
|
|
235
|
-
return {
|
|
236
|
-
statusCode: 405,
|
|
248
|
+
return createDefaultErrorResponse(methodNotAllowedDefaultError, {
|
|
237
249
|
header: { Allow: pathMatch.allowedMethods.join(", ") },
|
|
238
|
-
|
|
239
|
-
code: "METHOD_NOT_ALLOWED",
|
|
240
|
-
message: "Method not supported for this resource",
|
|
241
|
-
},
|
|
242
|
-
};
|
|
250
|
+
});
|
|
243
251
|
}
|
|
244
252
|
|
|
245
|
-
return
|
|
246
|
-
statusCode: 404,
|
|
247
|
-
body: { code: "NOT_FOUND", message: "No matching resource found" },
|
|
248
|
-
};
|
|
253
|
+
return createDefaultErrorResponse(notFoundDefaultError);
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
private withPathParams(
|
|
@@ -261,44 +266,123 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
261
266
|
route: RouteDefinition
|
|
262
267
|
): Promise<IHttpResponse> {
|
|
263
268
|
const validatedRequest = route.routerConfig.validateRequests
|
|
264
|
-
? route.
|
|
269
|
+
? route.requestValidator.validate(ctx.request)
|
|
265
270
|
: ctx.request;
|
|
266
271
|
|
|
267
272
|
return route.handler(validatedRequest, ctx);
|
|
268
273
|
}
|
|
269
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Validates a response against the operation's response validator.
|
|
277
|
+
*
|
|
278
|
+
* Behavior depends on configuration:
|
|
279
|
+
* - `validateResponses: false` → returns the original response unchanged.
|
|
280
|
+
* - `validateResponses: true` (default) → runs validation:
|
|
281
|
+
* - Valid response → returns the stripped response (extra fields removed).
|
|
282
|
+
* - Invalid response + handler configured → calls the handler safely.
|
|
283
|
+
* If the handler throws, falls back to the original response.
|
|
284
|
+
* - Invalid response + `handleResponseValidationErrors: false` → returns
|
|
285
|
+
* the original (invalid) response as-is.
|
|
286
|
+
*
|
|
287
|
+
* @param route - The route definition containing the response validator and config
|
|
288
|
+
* @param response - The response to validate
|
|
289
|
+
* @param ctx - The server context for the current request
|
|
290
|
+
* @returns The validated (and stripped) response, the handler's response, or the original
|
|
291
|
+
*/
|
|
292
|
+
private async validateResponse(
|
|
293
|
+
route: RouteDefinition,
|
|
294
|
+
response: IHttpResponse,
|
|
295
|
+
ctx: ServerContext
|
|
296
|
+
): Promise<IHttpResponse> {
|
|
297
|
+
if (!route.routerConfig.validateResponses) return response;
|
|
298
|
+
|
|
299
|
+
const result = route.responseValidator.safeValidate(response);
|
|
300
|
+
|
|
301
|
+
if (result.isValid) return result.data;
|
|
302
|
+
|
|
303
|
+
const handler = this.resolveErrorHandler<ResponseValidationErrorHandler>(
|
|
304
|
+
route.routerConfig.handleResponseValidationErrors,
|
|
305
|
+
TypeweaverApp.defaultResponseValidationHandler
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
if (handler) {
|
|
309
|
+
const handlerResponse = await this.safelyExecuteErrorHandler(() =>
|
|
310
|
+
handler(result.error, response, ctx)
|
|
311
|
+
);
|
|
312
|
+
if (handlerResponse) return handlerResponse;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return response;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Safely executes an error handler and returns null if it fails.
|
|
320
|
+
* This allows for graceful fallback to the next handler in the chain
|
|
321
|
+
* without crashing the request pipeline.
|
|
322
|
+
*
|
|
323
|
+
* If the handler throws, the error is reported via `safeOnError`
|
|
324
|
+
* and null is returned so the caller can fall through to the next handler.
|
|
325
|
+
*
|
|
326
|
+
* @param handlerFn - Function that executes the error handler
|
|
327
|
+
* @returns The handler's response if successful, null if the handler throws
|
|
328
|
+
*/
|
|
329
|
+
private async safelyExecuteErrorHandler(
|
|
330
|
+
handlerFn: () => Promise<IHttpResponse> | IHttpResponse
|
|
331
|
+
): Promise<IHttpResponse | null> {
|
|
332
|
+
try {
|
|
333
|
+
return await handlerFn();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.safeOnError(error);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
270
340
|
/**
|
|
271
341
|
* Handle errors using the route's configured error handlers.
|
|
272
|
-
* Handler errors bubble up to the safety net in `fetch()`.
|
|
273
342
|
*/
|
|
274
|
-
private handleError(
|
|
343
|
+
private async handleError(
|
|
275
344
|
error: unknown,
|
|
276
345
|
ctx: ServerContext,
|
|
277
346
|
route: RouteDefinition
|
|
278
|
-
):
|
|
347
|
+
): Promise<IHttpResponse> {
|
|
279
348
|
const config = route.routerConfig;
|
|
280
349
|
|
|
281
350
|
if (error instanceof RequestValidationError) {
|
|
282
|
-
const handler = this.resolveErrorHandler<
|
|
283
|
-
config.
|
|
284
|
-
TypeweaverApp.
|
|
351
|
+
const handler = this.resolveErrorHandler<RequestValidationErrorHandler>(
|
|
352
|
+
config.handleRequestValidationErrors,
|
|
353
|
+
TypeweaverApp.defaultRequestValidationHandler
|
|
285
354
|
);
|
|
286
|
-
if (handler)
|
|
355
|
+
if (handler) {
|
|
356
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
357
|
+
handler(error, ctx)
|
|
358
|
+
);
|
|
359
|
+
if (response) return response;
|
|
360
|
+
}
|
|
287
361
|
}
|
|
288
362
|
|
|
289
|
-
if (error
|
|
363
|
+
if (isTypedHttpResponse(error)) {
|
|
290
364
|
const handler = this.resolveErrorHandler<HttpResponseErrorHandler>(
|
|
291
365
|
config.handleHttpResponseErrors,
|
|
292
366
|
TypeweaverApp.defaultHttpResponseHandler
|
|
293
367
|
);
|
|
294
|
-
if (handler)
|
|
368
|
+
if (handler) {
|
|
369
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
370
|
+
handler(error, ctx)
|
|
371
|
+
);
|
|
372
|
+
if (response) return response;
|
|
373
|
+
}
|
|
295
374
|
}
|
|
296
375
|
|
|
297
376
|
const handler = this.resolveErrorHandler<UnknownErrorHandler>(
|
|
298
377
|
config.handleUnknownErrors,
|
|
299
378
|
this.defaultUnknownHandler
|
|
300
379
|
);
|
|
301
|
-
if (handler)
|
|
380
|
+
if (handler) {
|
|
381
|
+
const response = await this.safelyExecuteErrorHandler(() =>
|
|
382
|
+
handler(error, ctx)
|
|
383
|
+
);
|
|
384
|
+
if (response) return response;
|
|
385
|
+
}
|
|
302
386
|
|
|
303
387
|
throw error;
|
|
304
388
|
}
|
|
@@ -339,30 +423,32 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
339
423
|
return issues.map(({ message, path }) => ({ message, path }));
|
|
340
424
|
}
|
|
341
425
|
|
|
342
|
-
private static
|
|
343
|
-
err
|
|
344
|
-
|
|
345
|
-
const issues: Record<string, unknown> = Object.create(null);
|
|
426
|
+
private static defaultRequestValidationHandler: RequestValidationErrorHandler =
|
|
427
|
+
(err): IHttpResponse => {
|
|
428
|
+
const issues: Record<string, unknown> = Object.create(null);
|
|
346
429
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
430
|
+
const header = TypeweaverApp.sanitizeIssues(err.headerIssues);
|
|
431
|
+
const body = TypeweaverApp.sanitizeIssues(err.bodyIssues);
|
|
432
|
+
const query = TypeweaverApp.sanitizeIssues(err.queryIssues);
|
|
433
|
+
const param = TypeweaverApp.sanitizeIssues(err.pathParamIssues);
|
|
351
434
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
435
|
+
if (header) issues.header = header;
|
|
436
|
+
if (body) issues.body = body;
|
|
437
|
+
if (query) issues.query = query;
|
|
438
|
+
if (param) issues.param = param;
|
|
356
439
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
440
|
+
return {
|
|
441
|
+
statusCode: validationDefaultError.statusCode,
|
|
442
|
+
body: {
|
|
443
|
+
...createDefaultErrorBody(validationDefaultError),
|
|
444
|
+
issues,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
364
447
|
};
|
|
365
|
-
|
|
448
|
+
|
|
449
|
+
private static defaultResponseValidationHandler: ResponseValidationErrorHandler =
|
|
450
|
+
(): IHttpResponse =>
|
|
451
|
+
createDefaultErrorResponse(internalServerErrorDefaultError);
|
|
366
452
|
|
|
367
453
|
private static defaultHttpResponseHandler: HttpResponseErrorHandler = (
|
|
368
454
|
err
|
|
@@ -372,14 +458,17 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
372
458
|
error
|
|
373
459
|
): IHttpResponse => {
|
|
374
460
|
this.safeOnError(error);
|
|
375
|
-
return {
|
|
461
|
+
return {
|
|
462
|
+
statusCode: internalServerErrorDefaultError.statusCode,
|
|
463
|
+
body: TypeweaverApp.INTERNAL_SERVER_ERROR_BODY,
|
|
464
|
+
};
|
|
376
465
|
};
|
|
377
466
|
|
|
378
467
|
private static createErrorResponse(): Response {
|
|
379
468
|
return new Response(
|
|
380
469
|
JSON.stringify(TypeweaverApp.INTERNAL_SERVER_ERROR_BODY),
|
|
381
470
|
{
|
|
382
|
-
status:
|
|
471
|
+
status: internalServerErrorDefaultError.statusCode,
|
|
383
472
|
headers: { "content-type": "application/json" },
|
|
384
473
|
}
|
|
385
474
|
);
|
|
@@ -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";
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDefaultErrorBody,
|
|
3
|
+
unauthorizedDefaultError,
|
|
4
|
+
} from "@rexeus/typeweaver-core";
|
|
1
5
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
6
|
import { defineMiddleware } from "../TypedMiddleware";
|
|
3
7
|
import type { ServerContext } from "../ServerContext";
|
|
@@ -9,7 +13,6 @@ export type BasicAuthOptions = {
|
|
|
9
13
|
ctx: ServerContext
|
|
10
14
|
) => boolean | Promise<boolean>;
|
|
11
15
|
readonly realm?: string;
|
|
12
|
-
readonly unauthorizedMessage?: string;
|
|
13
16
|
readonly onUnauthorized?: (ctx: ServerContext) => IHttpResponse;
|
|
14
17
|
};
|
|
15
18
|
|
|
@@ -17,12 +20,11 @@ const BASIC_PREFIX = "Basic ";
|
|
|
17
20
|
|
|
18
21
|
export function basicAuth(options: BasicAuthOptions) {
|
|
19
22
|
const realm = options.realm ?? "Secure Area";
|
|
20
|
-
const message = options.unauthorizedMessage ?? "Unauthorized";
|
|
21
23
|
|
|
22
24
|
const defaultResponse: IHttpResponse = {
|
|
23
|
-
statusCode:
|
|
25
|
+
statusCode: unauthorizedDefaultError.statusCode,
|
|
24
26
|
header: { "www-authenticate": `Basic realm="${realm}"` },
|
|
25
|
-
body:
|
|
27
|
+
body: createDefaultErrorBody(unauthorizedDefaultError),
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
const deny = (ctx: ServerContext): IHttpResponse =>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDefaultErrorBody,
|
|
3
|
+
unauthorizedDefaultError,
|
|
4
|
+
} from "@rexeus/typeweaver-core";
|
|
1
5
|
import type { IHttpResponse } from "@rexeus/typeweaver-core";
|
|
2
6
|
import { defineMiddleware } from "../TypedMiddleware";
|
|
3
7
|
import type { ServerContext } from "../ServerContext";
|
|
@@ -8,7 +12,6 @@ export type BearerAuthOptions = {
|
|
|
8
12
|
ctx: ServerContext
|
|
9
13
|
) => boolean | Promise<boolean>;
|
|
10
14
|
readonly realm?: string;
|
|
11
|
-
readonly unauthorizedMessage?: string;
|
|
12
15
|
readonly onUnauthorized?: (ctx: ServerContext) => IHttpResponse;
|
|
13
16
|
};
|
|
14
17
|
|
|
@@ -16,12 +19,11 @@ const BEARER_PREFIX = "Bearer ";
|
|
|
16
19
|
|
|
17
20
|
export function bearerAuth(options: BearerAuthOptions) {
|
|
18
21
|
const realm = options.realm ?? "Secure Area";
|
|
19
|
-
const message = options.unauthorizedMessage ?? "Unauthorized";
|
|
20
22
|
|
|
21
23
|
const defaultResponse: IHttpResponse = {
|
|
22
|
-
statusCode:
|
|
24
|
+
statusCode: unauthorizedDefaultError.statusCode,
|
|
23
25
|
header: { "www-authenticate": `Bearer realm="${realm}"` },
|
|
24
|
-
body:
|
|
26
|
+
body: createDefaultErrorBody(unauthorizedDefaultError),
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
const deny = (ctx: ServerContext): IHttpResponse =>
|
|
@@ -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.9.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-core": "^0.
|
|
51
|
-
"@rexeus/typeweaver-gen": "^0.
|
|
50
|
+
"@rexeus/typeweaver-core": "^0.9.0",
|
|
51
|
+
"@rexeus/typeweaver-gen": "^0.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"get-port": "^7.
|
|
54
|
+
"get-port": "^7.2.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.9.0",
|
|
58
|
+
"@rexeus/typeweaver-gen": "^0.9.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",
|