@telorun/http-server 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/http-api-controller.js +3 -79
- package/dist/http-server-controller.js +76 -3
- package/package.json +5 -4
- package/src/http-api-controller.ts +3 -85
- package/src/http-server-controller.ts +80 -3
- package/dist/openapi.js +0 -152
- package/dist/test-validation.d.ts +0 -1
- package/dist/test-validation.js +0 -36
package/CHANGELOG.md
CHANGED
|
@@ -37,16 +37,6 @@ export class HttpServerApi {
|
|
|
37
37
|
}
|
|
38
38
|
async init() { }
|
|
39
39
|
register(app, prefix = "") {
|
|
40
|
-
// Register custom error handler for validation errors
|
|
41
|
-
app.setErrorHandler((error, request, reply) => {
|
|
42
|
-
const mappedError = convertFastifyValidationError(error);
|
|
43
|
-
if (mappedError) {
|
|
44
|
-
reply.code(400);
|
|
45
|
-
return reply.send(mappedError);
|
|
46
|
-
}
|
|
47
|
-
// Let Fastify handle other errors normally
|
|
48
|
-
throw error;
|
|
49
|
-
});
|
|
50
40
|
if (prefix) {
|
|
51
41
|
app.register(async (scoped) => {
|
|
52
42
|
this.registerRoutes(scoped);
|
|
@@ -83,15 +73,11 @@ export class HttpServerApi {
|
|
|
83
73
|
schema.response = Object.keys(route.response.statuses).reduce((acc, status) => {
|
|
84
74
|
const statusConfig = route.response.statuses[status];
|
|
85
75
|
if (statusConfig.schema) {
|
|
86
|
-
acc[status] = {};
|
|
87
|
-
if (statusConfig.schema.query) {
|
|
88
|
-
acc[status].querystring = statusConfig.schema.query;
|
|
89
|
-
}
|
|
90
76
|
if (statusConfig.schema.body) {
|
|
91
|
-
acc[status]
|
|
77
|
+
acc[status] = statusConfig.schema.body;
|
|
92
78
|
}
|
|
93
|
-
|
|
94
|
-
acc[status]
|
|
79
|
+
else {
|
|
80
|
+
acc[status] = {};
|
|
95
81
|
}
|
|
96
82
|
}
|
|
97
83
|
return acc;
|
|
@@ -269,68 +255,6 @@ function normalizeHeaders(headers) {
|
|
|
269
255
|
}
|
|
270
256
|
return normalized;
|
|
271
257
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Converts Fastify validation errors to standardized Telo format
|
|
274
|
-
* Returns null if the error is not a validation error
|
|
275
|
-
*/
|
|
276
|
-
function convertFastifyValidationError(error) {
|
|
277
|
-
// Check if this is a Fastify validation error
|
|
278
|
-
if (!error || typeof error !== "object" || error.code !== "FST_ERR_VALIDATION") {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
const message = error.message || "";
|
|
282
|
-
const details = [];
|
|
283
|
-
// Parse Fastify validation error message to extract location and field
|
|
284
|
-
// Format examples:
|
|
285
|
-
// "querystring must have required property 'name'"
|
|
286
|
-
// "body must be object"
|
|
287
|
-
// "params.userId must be string"
|
|
288
|
-
let location = "body"; // default
|
|
289
|
-
let fieldPath = "";
|
|
290
|
-
let validationMessage = "Validation failed";
|
|
291
|
-
// Try to extract location from message
|
|
292
|
-
if (message.includes("querystring")) {
|
|
293
|
-
location = "query";
|
|
294
|
-
}
|
|
295
|
-
else if (message.includes("params")) {
|
|
296
|
-
location = "params";
|
|
297
|
-
}
|
|
298
|
-
else if (message.includes("headers")) {
|
|
299
|
-
location = "headers";
|
|
300
|
-
}
|
|
301
|
-
else if (message.includes("body")) {
|
|
302
|
-
location = "body";
|
|
303
|
-
}
|
|
304
|
-
// Extract field name from "must have required property 'fieldName'" pattern
|
|
305
|
-
const requiredMatch = message.match(/must have required property '([^']+)'/);
|
|
306
|
-
if (requiredMatch) {
|
|
307
|
-
fieldPath = requiredMatch[1];
|
|
308
|
-
validationMessage = `is a required property`;
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
// Extract field from "fieldName must be" pattern
|
|
312
|
-
const fieldMatch = message.match(/^(?:querystring|body|params|headers)\.?(\w+)\s/);
|
|
313
|
-
if (fieldMatch) {
|
|
314
|
-
fieldPath = fieldMatch[1];
|
|
315
|
-
}
|
|
316
|
-
validationMessage = message
|
|
317
|
-
.replace(/^(?:querystring|body|params|headers)\.?\w*\s/, "")
|
|
318
|
-
.replace(" must ", " ");
|
|
319
|
-
}
|
|
320
|
-
if (fieldPath || message) {
|
|
321
|
-
details.push({
|
|
322
|
-
location,
|
|
323
|
-
path: fieldPath,
|
|
324
|
-
message: validationMessage,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
error: "ValidationError",
|
|
329
|
-
message: "Request validation failed",
|
|
330
|
-
status: 400,
|
|
331
|
-
details,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
258
|
/**
|
|
335
259
|
* Legacy function - kept for compatibility but not used
|
|
336
260
|
* Converts framework-specific validation errors to standardized Telo format
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import swagger from "@fastify/swagger";
|
|
2
2
|
import apiReference from "@scalar/fastify-api-reference";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
3
4
|
import Fastify from "fastify";
|
|
4
5
|
class HttpServer {
|
|
5
6
|
releaseHold = null;
|
|
@@ -18,13 +19,23 @@ class HttpServer {
|
|
|
18
19
|
if (!this.port) {
|
|
19
20
|
throw new Error("Http.Server port is required");
|
|
20
21
|
}
|
|
21
|
-
this.app = Fastify({ logger: true });
|
|
22
|
+
this.app = Fastify({ logger: true, ajv: { plugins: [addFormats.default] } });
|
|
22
23
|
}
|
|
23
24
|
async init() {
|
|
24
|
-
this.setupPlugins();
|
|
25
|
+
await this.setupPlugins();
|
|
25
26
|
this.setupRoutes();
|
|
26
27
|
}
|
|
27
28
|
async setupPlugins() {
|
|
29
|
+
// Register custom error handler for validation errors
|
|
30
|
+
this.app.setErrorHandler((error, request, reply) => {
|
|
31
|
+
const mappedError = convertFastifyValidationError(error);
|
|
32
|
+
if (mappedError) {
|
|
33
|
+
reply.code(400);
|
|
34
|
+
return reply.send(mappedError);
|
|
35
|
+
}
|
|
36
|
+
// Let Fastify handle other errors normally
|
|
37
|
+
throw error;
|
|
38
|
+
});
|
|
28
39
|
if (this.resource.openapi) {
|
|
29
40
|
const servers = [];
|
|
30
41
|
// const routesByName = new Map<string, HttpRouteResource>();
|
|
@@ -56,7 +67,7 @@ class HttpServer {
|
|
|
56
67
|
const type = mount.type || "";
|
|
57
68
|
const { kind, name } = parseType(type);
|
|
58
69
|
const prefix = mount.path || "";
|
|
59
|
-
const api = this.ctx.
|
|
70
|
+
const api = this.ctx.moduleContext.getInvokable(name);
|
|
60
71
|
if (!api) {
|
|
61
72
|
throw new Error(`Failed to mount Http.Api at "${prefix}": ${type} not found`);
|
|
62
73
|
}
|
|
@@ -102,3 +113,65 @@ function parseType(type) {
|
|
|
102
113
|
}
|
|
103
114
|
return { kind: type.slice(0, separator), name: type.slice(separator + 1) };
|
|
104
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Converts Fastify validation errors to standardized Telo format
|
|
118
|
+
* Returns null if the error is not a validation error
|
|
119
|
+
*/
|
|
120
|
+
function convertFastifyValidationError(error) {
|
|
121
|
+
// Check if this is a Fastify validation error
|
|
122
|
+
if (!error || typeof error !== "object" || error.code !== "FST_ERR_VALIDATION") {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const message = error.message || "";
|
|
126
|
+
const details = [];
|
|
127
|
+
// Parse Fastify validation error message to extract location and field
|
|
128
|
+
// Format examples:
|
|
129
|
+
// "querystring must have required property 'name'"
|
|
130
|
+
// "body must be object"
|
|
131
|
+
// "params.userId must be string"
|
|
132
|
+
let location = "body"; // default
|
|
133
|
+
let fieldPath = "";
|
|
134
|
+
let validationMessage = "Validation failed";
|
|
135
|
+
// Try to extract location from message
|
|
136
|
+
if (message.includes("querystring")) {
|
|
137
|
+
location = "query";
|
|
138
|
+
}
|
|
139
|
+
else if (message.includes("params")) {
|
|
140
|
+
location = "params";
|
|
141
|
+
}
|
|
142
|
+
else if (message.includes("headers")) {
|
|
143
|
+
location = "headers";
|
|
144
|
+
}
|
|
145
|
+
else if (message.includes("body")) {
|
|
146
|
+
location = "body";
|
|
147
|
+
}
|
|
148
|
+
// Extract field name from "must have required property 'fieldName'" pattern
|
|
149
|
+
const requiredMatch = message.match(/must have required property '([^']+)'/);
|
|
150
|
+
if (requiredMatch) {
|
|
151
|
+
fieldPath = requiredMatch[1];
|
|
152
|
+
validationMessage = `is a required property`;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Extract field from "fieldName must be" pattern
|
|
156
|
+
const fieldMatch = message.match(/^(?:querystring|body|params|headers)\.?(\w+)\s/);
|
|
157
|
+
if (fieldMatch) {
|
|
158
|
+
fieldPath = fieldMatch[1];
|
|
159
|
+
}
|
|
160
|
+
validationMessage = message
|
|
161
|
+
.replace(/^(?:querystring|body|params|headers)\.?\w*\s/, "")
|
|
162
|
+
.replace(" must ", " ");
|
|
163
|
+
}
|
|
164
|
+
if (fieldPath || message) {
|
|
165
|
+
details.push({
|
|
166
|
+
location,
|
|
167
|
+
path: fieldPath,
|
|
168
|
+
message: validationMessage,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
error: "ValidationError",
|
|
173
|
+
message: "Request validation failed",
|
|
174
|
+
status: 400,
|
|
175
|
+
details,
|
|
176
|
+
};
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/http-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@fastify/swagger": "^9.6.1",
|
|
21
|
+
"@scalar/fastify-api-reference": "^1.44.6",
|
|
20
22
|
"@sinclair/typebox": "^0.34.48",
|
|
21
23
|
"ajv": "^8.17.1",
|
|
24
|
+
"ajv-formats": "^3.0.1",
|
|
22
25
|
"fastify": "^5.7.2",
|
|
23
|
-
"@
|
|
24
|
-
"@scalar/fastify-api-reference": "^1.44.6",
|
|
25
|
-
"@telorun/sdk": "0.2.4"
|
|
26
|
+
"@telorun/sdk": "0.2.5"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@types/node": "^20.0.0",
|
|
@@ -52,17 +52,6 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
52
52
|
async init() {}
|
|
53
53
|
|
|
54
54
|
register(app: FastifyInstance, prefix = "") {
|
|
55
|
-
// Register custom error handler for validation errors
|
|
56
|
-
app.setErrorHandler((error, request, reply) => {
|
|
57
|
-
const mappedError = convertFastifyValidationError(error);
|
|
58
|
-
if (mappedError) {
|
|
59
|
-
reply.code(400);
|
|
60
|
-
return reply.send(mappedError);
|
|
61
|
-
}
|
|
62
|
-
// Let Fastify handle other errors normally
|
|
63
|
-
throw error;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
55
|
if (prefix) {
|
|
67
56
|
app.register(
|
|
68
57
|
async (scoped) => {
|
|
@@ -107,15 +96,10 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
107
96
|
(acc, status) => {
|
|
108
97
|
const statusConfig = route.response.statuses[status];
|
|
109
98
|
if (statusConfig.schema) {
|
|
110
|
-
acc[status] = {};
|
|
111
|
-
if (statusConfig.schema.query) {
|
|
112
|
-
acc[status].querystring = statusConfig.schema.query;
|
|
113
|
-
}
|
|
114
99
|
if (statusConfig.schema.body) {
|
|
115
|
-
acc[status]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
acc[status].headers = statusConfig.schema.headers;
|
|
100
|
+
acc[status] = statusConfig.schema.body;
|
|
101
|
+
} else {
|
|
102
|
+
acc[status] = {};
|
|
119
103
|
}
|
|
120
104
|
}
|
|
121
105
|
return acc;
|
|
@@ -325,72 +309,6 @@ function normalizeHeaders(headers: Record<string, any>): Record<string, any> {
|
|
|
325
309
|
return normalized;
|
|
326
310
|
}
|
|
327
311
|
|
|
328
|
-
/**
|
|
329
|
-
* Converts Fastify validation errors to standardized Telo format
|
|
330
|
-
* Returns null if the error is not a validation error
|
|
331
|
-
*/
|
|
332
|
-
function convertFastifyValidationError(error: any): Record<string, any> | null {
|
|
333
|
-
// Check if this is a Fastify validation error
|
|
334
|
-
if (!error || typeof error !== "object" || error.code !== "FST_ERR_VALIDATION") {
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const message = error.message || "";
|
|
339
|
-
const details = [];
|
|
340
|
-
|
|
341
|
-
// Parse Fastify validation error message to extract location and field
|
|
342
|
-
// Format examples:
|
|
343
|
-
// "querystring must have required property 'name'"
|
|
344
|
-
// "body must be object"
|
|
345
|
-
// "params.userId must be string"
|
|
346
|
-
|
|
347
|
-
let location = "body"; // default
|
|
348
|
-
let fieldPath = "";
|
|
349
|
-
let validationMessage = "Validation failed";
|
|
350
|
-
|
|
351
|
-
// Try to extract location from message
|
|
352
|
-
if (message.includes("querystring")) {
|
|
353
|
-
location = "query";
|
|
354
|
-
} else if (message.includes("params")) {
|
|
355
|
-
location = "params";
|
|
356
|
-
} else if (message.includes("headers")) {
|
|
357
|
-
location = "headers";
|
|
358
|
-
} else if (message.includes("body")) {
|
|
359
|
-
location = "body";
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Extract field name from "must have required property 'fieldName'" pattern
|
|
363
|
-
const requiredMatch = message.match(/must have required property '([^']+)'/);
|
|
364
|
-
if (requiredMatch) {
|
|
365
|
-
fieldPath = requiredMatch[1];
|
|
366
|
-
validationMessage = `is a required property`;
|
|
367
|
-
} else {
|
|
368
|
-
// Extract field from "fieldName must be" pattern
|
|
369
|
-
const fieldMatch = message.match(/^(?:querystring|body|params|headers)\.?(\w+)\s/);
|
|
370
|
-
if (fieldMatch) {
|
|
371
|
-
fieldPath = fieldMatch[1];
|
|
372
|
-
}
|
|
373
|
-
validationMessage = message
|
|
374
|
-
.replace(/^(?:querystring|body|params|headers)\.?\w*\s/, "")
|
|
375
|
-
.replace(" must ", " ");
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (fieldPath || message) {
|
|
379
|
-
details.push({
|
|
380
|
-
location,
|
|
381
|
-
path: fieldPath,
|
|
382
|
-
message: validationMessage,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
error: "ValidationError",
|
|
388
|
-
message: "Request validation failed",
|
|
389
|
-
status: 400,
|
|
390
|
-
details,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
312
|
/**
|
|
395
313
|
* Legacy function - kept for compatibility but not used
|
|
396
314
|
* Converts framework-specific validation errors to standardized Telo format
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import swagger from "@fastify/swagger";
|
|
2
2
|
import apiReference from "@scalar/fastify-api-reference";
|
|
3
3
|
import type { ResourceContext, ResourceInstance, RuntimeResource } from "@telorun/sdk";
|
|
4
|
+
import addFormats from "ajv-formats";
|
|
4
5
|
import Fastify, { FastifyInstance } from "fastify";
|
|
5
6
|
import { HttpServerApi } from "./http-api-controller.js";
|
|
6
7
|
|
|
@@ -86,15 +87,25 @@ class HttpServer implements ResourceInstance {
|
|
|
86
87
|
if (!this.port) {
|
|
87
88
|
throw new Error("Http.Server port is required");
|
|
88
89
|
}
|
|
89
|
-
this.app = Fastify({ logger: true });
|
|
90
|
+
this.app = Fastify({ logger: true, ajv: { plugins: [addFormats.default as any] } });
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
async init() {
|
|
93
|
-
this.setupPlugins();
|
|
94
|
+
await this.setupPlugins();
|
|
94
95
|
this.setupRoutes();
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
private async setupPlugins() {
|
|
99
|
+
// Register custom error handler for validation errors
|
|
100
|
+
this.app.setErrorHandler((error, request, reply) => {
|
|
101
|
+
const mappedError = convertFastifyValidationError(error);
|
|
102
|
+
if (mappedError) {
|
|
103
|
+
reply.code(400);
|
|
104
|
+
return reply.send(mappedError);
|
|
105
|
+
}
|
|
106
|
+
// Let Fastify handle other errors normally
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
98
109
|
if (this.resource.openapi) {
|
|
99
110
|
const servers = [];
|
|
100
111
|
// const routesByName = new Map<string, HttpRouteResource>();
|
|
@@ -128,7 +139,7 @@ class HttpServer implements ResourceInstance {
|
|
|
128
139
|
const { kind, name } = parseType(type);
|
|
129
140
|
const prefix = mount.path || "";
|
|
130
141
|
|
|
131
|
-
const api
|
|
142
|
+
const api = this.ctx.moduleContext.getInvokable(name) as unknown as HttpServerApi;
|
|
132
143
|
|
|
133
144
|
if (!api) {
|
|
134
145
|
throw new Error(`Failed to mount Http.Api at "${prefix}": ${type} not found`);
|
|
@@ -181,3 +192,69 @@ function parseType(type: string): { kind: string; name: string } {
|
|
|
181
192
|
}
|
|
182
193
|
return { kind: type.slice(0, separator), name: type.slice(separator + 1) };
|
|
183
194
|
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Converts Fastify validation errors to standardized Telo format
|
|
198
|
+
* Returns null if the error is not a validation error
|
|
199
|
+
*/
|
|
200
|
+
function convertFastifyValidationError(error: any): Record<string, any> | null {
|
|
201
|
+
// Check if this is a Fastify validation error
|
|
202
|
+
if (!error || typeof error !== "object" || error.code !== "FST_ERR_VALIDATION") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const message = error.message || "";
|
|
207
|
+
const details = [];
|
|
208
|
+
|
|
209
|
+
// Parse Fastify validation error message to extract location and field
|
|
210
|
+
// Format examples:
|
|
211
|
+
// "querystring must have required property 'name'"
|
|
212
|
+
// "body must be object"
|
|
213
|
+
// "params.userId must be string"
|
|
214
|
+
|
|
215
|
+
let location = "body"; // default
|
|
216
|
+
let fieldPath = "";
|
|
217
|
+
let validationMessage = "Validation failed";
|
|
218
|
+
|
|
219
|
+
// Try to extract location from message
|
|
220
|
+
if (message.includes("querystring")) {
|
|
221
|
+
location = "query";
|
|
222
|
+
} else if (message.includes("params")) {
|
|
223
|
+
location = "params";
|
|
224
|
+
} else if (message.includes("headers")) {
|
|
225
|
+
location = "headers";
|
|
226
|
+
} else if (message.includes("body")) {
|
|
227
|
+
location = "body";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Extract field name from "must have required property 'fieldName'" pattern
|
|
231
|
+
const requiredMatch = message.match(/must have required property '([^']+)'/);
|
|
232
|
+
if (requiredMatch) {
|
|
233
|
+
fieldPath = requiredMatch[1];
|
|
234
|
+
validationMessage = `is a required property`;
|
|
235
|
+
} else {
|
|
236
|
+
// Extract field from "fieldName must be" pattern
|
|
237
|
+
const fieldMatch = message.match(/^(?:querystring|body|params|headers)\.?(\w+)\s/);
|
|
238
|
+
if (fieldMatch) {
|
|
239
|
+
fieldPath = fieldMatch[1];
|
|
240
|
+
}
|
|
241
|
+
validationMessage = message
|
|
242
|
+
.replace(/^(?:querystring|body|params|headers)\.?\w*\s/, "")
|
|
243
|
+
.replace(" must ", " ");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (fieldPath || message) {
|
|
247
|
+
details.push({
|
|
248
|
+
location,
|
|
249
|
+
path: fieldPath,
|
|
250
|
+
message: validationMessage,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
error: "ValidationError",
|
|
256
|
+
message: "Request validation failed",
|
|
257
|
+
status: 400,
|
|
258
|
+
details,
|
|
259
|
+
};
|
|
260
|
+
}
|
package/dist/openapi.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import swagger from '@fastify/swagger';
|
|
2
|
-
import apiReference from '@scalar/fastify-api-reference';
|
|
3
|
-
function getResourceConfig(resource) {
|
|
4
|
-
return resource;
|
|
5
|
-
}
|
|
6
|
-
export function register(ctx) { }
|
|
7
|
-
export function create(resource, ctx) {
|
|
8
|
-
const openApiResource = resource;
|
|
9
|
-
const config = getResourceConfig(openApiResource);
|
|
10
|
-
const apiRefs = config.apis || [];
|
|
11
|
-
if (apiRefs.length === 0) {
|
|
12
|
-
throw new Error(`OpenApi.Spec "${resource.metadata.name}" is missing apis`);
|
|
13
|
-
}
|
|
14
|
-
const handler = (payload) => {
|
|
15
|
-
const serverResource = payload?.resource;
|
|
16
|
-
const app = payload?.app;
|
|
17
|
-
if (!serverResource || !app) {
|
|
18
|
-
throw new Error(`OpenApi.Spec handler missing Http.Server resource or Fastify app`);
|
|
19
|
-
}
|
|
20
|
-
const matchedApis = resolveApis(apiRefs, ctx);
|
|
21
|
-
const mounts = getResourceConfig(serverResource).mounts || [];
|
|
22
|
-
const servers = buildServers(serverResource, mounts, matchedApis);
|
|
23
|
-
if (servers.length === 0) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const paths = buildPaths(matchedApis, mounts);
|
|
27
|
-
const info = config.info && typeof config.info === 'object'
|
|
28
|
-
? config.info
|
|
29
|
-
: {
|
|
30
|
-
title: resource.metadata.name,
|
|
31
|
-
version: resource.version || '1.0.0',
|
|
32
|
-
};
|
|
33
|
-
const routePrefix = config.path || `/openapi/${resource.metadata.name}`;
|
|
34
|
-
app.register(swagger, {
|
|
35
|
-
openapi: {
|
|
36
|
-
openapi: '3.0.0',
|
|
37
|
-
info,
|
|
38
|
-
servers,
|
|
39
|
-
paths,
|
|
40
|
-
},
|
|
41
|
-
routePrefix,
|
|
42
|
-
});
|
|
43
|
-
app.register(apiReference, {
|
|
44
|
-
routePrefix,
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
const httpServers = ctx.getResources('Http.Server');
|
|
48
|
-
return {
|
|
49
|
-
init: async () => {
|
|
50
|
-
// Register listeners after all resources are initialized
|
|
51
|
-
for (const server of httpServers) {
|
|
52
|
-
ctx.on('Http.Server.Ready', server.metadata.name, handler);
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
teardown: () => {
|
|
56
|
-
for (const server of httpServers) {
|
|
57
|
-
ctx.offResourceEvent('Http.Server', server.metadata.name, 'Ready', handler);
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function resolveApis(apiRefs, ctx) {
|
|
63
|
-
const apis = [];
|
|
64
|
-
for (const ref of apiRefs) {
|
|
65
|
-
const { kind, name } = parseRef(ref);
|
|
66
|
-
if (!kind || !name) {
|
|
67
|
-
throw new Error(`Reference not found: ${ref}`);
|
|
68
|
-
}
|
|
69
|
-
if (kind !== 'Http.Api' && kind !== 'Http.Route') {
|
|
70
|
-
throw new Error(`Reference not supported: ${ref}`);
|
|
71
|
-
}
|
|
72
|
-
const resource = ctx.kernel.registry.get(kind)?.get(name);
|
|
73
|
-
if (!resource) {
|
|
74
|
-
throw new Error(`Reference not found: ${ref}`);
|
|
75
|
-
}
|
|
76
|
-
apis.push(resource);
|
|
77
|
-
}
|
|
78
|
-
return apis;
|
|
79
|
-
}
|
|
80
|
-
function buildServers(server, mounts, apis) {
|
|
81
|
-
const config = getResourceConfig(server);
|
|
82
|
-
const host = config.host || '0.0.0.0';
|
|
83
|
-
const port = Number(config.port || 0);
|
|
84
|
-
if (!port) {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
// Server URL should be the base URL without mount paths
|
|
88
|
-
// Paths will include the mount prefix
|
|
89
|
-
return [{ url: `http://${host}:${port}` }];
|
|
90
|
-
}
|
|
91
|
-
function buildPaths(apis, mounts) {
|
|
92
|
-
const paths = {};
|
|
93
|
-
const mountPrefixByType = new Map();
|
|
94
|
-
for (const mount of mounts) {
|
|
95
|
-
if (mount.type) {
|
|
96
|
-
mountPrefixByType.set(mount.type, mount.path || '');
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
for (const api of apis) {
|
|
100
|
-
const prefix = mountPrefixByType.get(`${api.kind}.${api.metadata.name}`) || '';
|
|
101
|
-
if (api.kind === 'Http.Route') {
|
|
102
|
-
const config = getResourceConfig(api);
|
|
103
|
-
const path = joinPath(prefix, api.metadata?.path || config.path || '');
|
|
104
|
-
const method = (api.metadata?.method ||
|
|
105
|
-
config.method ||
|
|
106
|
-
'GET').toLowerCase();
|
|
107
|
-
if (!path) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (!paths[path]) {
|
|
111
|
-
paths[path] = {};
|
|
112
|
-
}
|
|
113
|
-
paths[path][method] = { responses: { '200': { description: 'OK' } } };
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
const routes = getResourceConfig(api).routes || [];
|
|
117
|
-
for (const route of routes) {
|
|
118
|
-
if (typeof route === 'string') {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const request = route.request || {};
|
|
122
|
-
const path = joinPath(prefix, request.path || '');
|
|
123
|
-
const method = (request.method || 'GET').toLowerCase();
|
|
124
|
-
if (!path) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (!paths[path]) {
|
|
128
|
-
paths[path] = {};
|
|
129
|
-
}
|
|
130
|
-
paths[path][method] = { responses: { '200': { description: 'OK' } } };
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return paths;
|
|
134
|
-
}
|
|
135
|
-
function parseRef(ref) {
|
|
136
|
-
const separator = ref.lastIndexOf('.');
|
|
137
|
-
if (separator <= 0 || separator === ref.length - 1) {
|
|
138
|
-
return { kind: '', name: '' };
|
|
139
|
-
}
|
|
140
|
-
return { kind: ref.slice(0, separator), name: ref.slice(separator + 1) };
|
|
141
|
-
}
|
|
142
|
-
function joinPath(prefix, path) {
|
|
143
|
-
if (!prefix) {
|
|
144
|
-
return path;
|
|
145
|
-
}
|
|
146
|
-
if (!path) {
|
|
147
|
-
return prefix;
|
|
148
|
-
}
|
|
149
|
-
const trimmedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
150
|
-
const trimmedPath = path.startsWith('/') ? path : `/${path}`;
|
|
151
|
-
return `${trimmedPrefix}${trimmedPath}`;
|
|
152
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test-validation.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// Test file to validate error conversion logic
|
|
2
|
-
import { convertFastifyValidationError } from "./http-api-controller.js";
|
|
3
|
-
function testValidationError() {
|
|
4
|
-
// Test case 1: Querystring validation error
|
|
5
|
-
const error1 = {
|
|
6
|
-
code: "FST_ERR_VALIDATION",
|
|
7
|
-
message: "querystring must have required property 'name'",
|
|
8
|
-
statusCode: 400,
|
|
9
|
-
error: "Bad Request",
|
|
10
|
-
};
|
|
11
|
-
const result1 = convertFastifyValidationError(error1);
|
|
12
|
-
console.log("Test 1 - Querystring required field:", JSON.stringify(result1, null, 2));
|
|
13
|
-
console.log("✓ Location:", result1?.details[0].location === "query" ? "PASS" : "FAIL");
|
|
14
|
-
console.log("✓ Path:", result1?.details[0].path === "name" ? "PASS" : "FAIL");
|
|
15
|
-
console.log("✓ Message:", result1?.details[0].message.includes("required") ? "PASS" : "FAIL");
|
|
16
|
-
// Test case 2: Body validation error
|
|
17
|
-
const error2 = {
|
|
18
|
-
code: "FST_ERR_VALIDATION",
|
|
19
|
-
message: "body must be object",
|
|
20
|
-
statusCode: 400,
|
|
21
|
-
};
|
|
22
|
-
const result2 = convertFastifyValidationError(error2);
|
|
23
|
-
console.log("\nTest 2 - Body type error:", JSON.stringify(result2, null, 2));
|
|
24
|
-
console.log("✓ Location:", result2?.details[0].location === "body" ? "PASS" : "FAIL");
|
|
25
|
-
// Test case 3: Non-validation error (should return null)
|
|
26
|
-
const error3 = {
|
|
27
|
-
code: "ERR_OTHER",
|
|
28
|
-
message: "Some other error",
|
|
29
|
-
};
|
|
30
|
-
const result3 = convertFastifyValidationError(error3);
|
|
31
|
-
console.log("\nTest 3 - Non-validation error:", result3 === null ? "PASS (null)" : "FAIL (not null)");
|
|
32
|
-
}
|
|
33
|
-
// Only run if this file is executed directly
|
|
34
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
35
|
-
testValidationError();
|
|
36
|
-
}
|