@telorun/http-server 0.1.3 → 0.1.5
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 +16 -0
- package/dist/http-api-controller.d.ts +44 -14
- package/dist/http-api-controller.js +82 -285
- package/dist/http-server-controller.d.ts +12 -2
- package/dist/http-server-controller.js +142 -6
- package/package.json +5 -4
- package/src/http-api-controller.ts +117 -332
- package/src/http-server-controller.ts +178 -54
- package/dist/openapi.js +0 -152
- package/dist/test-validation.d.ts +0 -1
- package/dist/test-validation.js +0 -36
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import swagger from "@fastify/swagger";
|
|
2
2
|
import apiReference from "@scalar/fastify-api-reference";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Invocable,
|
|
5
|
+
KindRef,
|
|
6
|
+
ResourceContext,
|
|
7
|
+
ResourceInstance,
|
|
8
|
+
RuntimeResource,
|
|
9
|
+
} from "@telorun/sdk";
|
|
10
|
+
import addFormats from "ajv-formats";
|
|
4
11
|
import Fastify, { FastifyInstance } from "fastify";
|
|
5
|
-
import { HttpServerApi } from "./http-api-controller.js";
|
|
6
|
-
|
|
7
|
-
type HttpRouteResource = RuntimeResource & {
|
|
8
|
-
metadata?: { path?: string; method?: string };
|
|
9
|
-
path?: string;
|
|
10
|
-
method?: string;
|
|
11
|
-
handler?: HttpHandlerSpec;
|
|
12
|
-
request?: HttpRequestSchema;
|
|
13
|
-
response?: {
|
|
14
|
-
status?: number;
|
|
15
|
-
headers?: Record<string, string>;
|
|
16
|
-
body?: any;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
12
|
+
import { dispatchResponse, HttpServerApi, ResponseEntry } from "./http-api-controller.js";
|
|
19
13
|
|
|
20
14
|
type HttpServerResource = RuntimeResource & {
|
|
21
15
|
host?: string;
|
|
22
16
|
port?: number;
|
|
23
17
|
baseUrl?: string;
|
|
18
|
+
logger?: boolean;
|
|
19
|
+
contentTypeParsers?: Array<{ contentType: string; parser?: Invocable }>;
|
|
24
20
|
openapi?: {
|
|
25
21
|
info: {
|
|
26
22
|
title: string;
|
|
@@ -31,70 +27,83 @@ type HttpServerResource = RuntimeResource & {
|
|
|
31
27
|
path?: string;
|
|
32
28
|
type?: string;
|
|
33
29
|
}>;
|
|
30
|
+
notFoundHandler?: {
|
|
31
|
+
invoke: KindRef<Invocable>;
|
|
32
|
+
response?: ResponseEntry[];
|
|
33
|
+
};
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
path?: string;
|
|
42
|
-
method?: string;
|
|
43
|
-
query?: Record<string, any>;
|
|
44
|
-
body?: Record<string, any>;
|
|
45
|
-
headers?: Record<string, any>;
|
|
46
|
-
};
|
|
47
|
-
handler?: HttpHandlerSpec;
|
|
48
|
-
response?: {
|
|
49
|
-
status?: number;
|
|
50
|
-
headers?: Record<string, string>;
|
|
51
|
-
body?: any;
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type HttpHandlerSpec =
|
|
58
|
-
| string
|
|
59
|
-
| {
|
|
60
|
-
name?: string;
|
|
61
|
-
inputs?: Record<string, any>;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
type HttpRequestSchema = {
|
|
65
|
-
query?: Record<string, any>;
|
|
66
|
-
body?: Record<string, any>;
|
|
67
|
-
headers?: Record<string, any>;
|
|
36
|
+
type ResolvedHandler = {
|
|
37
|
+
kind: string;
|
|
38
|
+
name: string;
|
|
39
|
+
inputs: Record<string, any>;
|
|
40
|
+
response?: ResponseEntry[];
|
|
68
41
|
};
|
|
69
42
|
|
|
70
43
|
class HttpServer implements ResourceInstance {
|
|
71
44
|
private releaseHold: (() => void) | null = null;
|
|
45
|
+
private pluginsInitialized = false;
|
|
72
46
|
private readonly app: FastifyInstance;
|
|
73
47
|
private readonly host: string;
|
|
74
48
|
private readonly port: number;
|
|
75
49
|
private readonly baseUrl: string;
|
|
76
50
|
private readonly resource: HttpServerResource;
|
|
77
51
|
private readonly ctx: ResourceContext;
|
|
52
|
+
private readonly resolvedNotFoundHandler: ResolvedHandler | null;
|
|
78
53
|
|
|
79
|
-
constructor(
|
|
54
|
+
constructor(
|
|
55
|
+
resource: HttpServerResource,
|
|
56
|
+
ctx: ResourceContext,
|
|
57
|
+
resolvedNotFoundHandler: ResolvedHandler | null = null,
|
|
58
|
+
) {
|
|
80
59
|
this.resource = resource;
|
|
81
60
|
this.ctx = ctx;
|
|
82
61
|
this.host = resource.host || "0.0.0.0";
|
|
83
62
|
this.port = Number(resource.port || 0);
|
|
84
63
|
this.baseUrl = resource.baseUrl ?? `http://${this.host}:${this.port}`;
|
|
64
|
+
this.resolvedNotFoundHandler = resolvedNotFoundHandler;
|
|
85
65
|
|
|
86
66
|
if (!this.port) {
|
|
87
67
|
throw new Error("Http.Server port is required");
|
|
88
68
|
}
|
|
89
|
-
this.app = Fastify({ logger:
|
|
69
|
+
this.app = Fastify({ logger: resource.logger, ajv: { plugins: [addFormats.default as any] } });
|
|
90
70
|
}
|
|
91
71
|
|
|
92
72
|
async init() {
|
|
93
|
-
this.
|
|
73
|
+
if (!this.pluginsInitialized) {
|
|
74
|
+
await this.setupPlugins();
|
|
75
|
+
this.pluginsInitialized = true;
|
|
76
|
+
}
|
|
94
77
|
this.setupRoutes();
|
|
95
78
|
}
|
|
96
79
|
|
|
97
80
|
private async setupPlugins() {
|
|
81
|
+
for (const { contentType, parser } of this.resource.contentTypeParsers ?? []) {
|
|
82
|
+
if (parser) {
|
|
83
|
+
this.app.addContentTypeParser(contentType, { parseAs: "string" }, async (_req, body, done) => {
|
|
84
|
+
try {
|
|
85
|
+
done(null, await parser.invoke({ body }));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
done(err as Error, undefined);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
this.app.addContentTypeParser(contentType, { parseAs: "string" }, (_req, body, done) => {
|
|
92
|
+
done(null, body);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Register custom error handler for validation errors
|
|
98
|
+
this.app.setErrorHandler((error, request, reply) => {
|
|
99
|
+
const mappedError = convertFastifyValidationError(error);
|
|
100
|
+
if (mappedError) {
|
|
101
|
+
reply.code(400);
|
|
102
|
+
return reply.send(mappedError);
|
|
103
|
+
}
|
|
104
|
+
// Let Fastify handle other errors normally
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
98
107
|
if (this.resource.openapi) {
|
|
99
108
|
const servers = [];
|
|
100
109
|
// const routesByName = new Map<string, HttpRouteResource>();
|
|
@@ -128,13 +137,52 @@ class HttpServer implements ResourceInstance {
|
|
|
128
137
|
const { kind, name } = parseType(type);
|
|
129
138
|
const prefix = mount.path || "";
|
|
130
139
|
|
|
131
|
-
const api
|
|
140
|
+
const api = this.ctx.moduleContext.getInvocable(name) as unknown as HttpServerApi;
|
|
132
141
|
|
|
133
142
|
if (!api) {
|
|
134
143
|
throw new Error(`Failed to mount Http.Api at "${prefix}": ${type} not found`);
|
|
135
144
|
}
|
|
136
145
|
api.register(this.app, prefix);
|
|
137
146
|
}
|
|
147
|
+
|
|
148
|
+
if (this.resolvedNotFoundHandler) {
|
|
149
|
+
const handler = this.resolvedNotFoundHandler;
|
|
150
|
+
this.app.setNotFoundHandler(async (request, reply) => {
|
|
151
|
+
const normalizedHeaders: Record<string, any> = {};
|
|
152
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
153
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
154
|
+
}
|
|
155
|
+
const requestContext = {
|
|
156
|
+
request: {
|
|
157
|
+
method: request.method,
|
|
158
|
+
path: request.url,
|
|
159
|
+
params: request.params || {},
|
|
160
|
+
query: request.query || {},
|
|
161
|
+
headers: normalizedHeaders,
|
|
162
|
+
body: request.body,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
const result = await this.ctx.invoke(handler.kind, handler.name, requestContext);
|
|
166
|
+
if (handler.response) {
|
|
167
|
+
return dispatchResponse(
|
|
168
|
+
handler.response,
|
|
169
|
+
result,
|
|
170
|
+
requestContext,
|
|
171
|
+
this.ctx.moduleContext,
|
|
172
|
+
this.ctx.validateSchema.bind(this.ctx),
|
|
173
|
+
reply,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const status = result?.status ?? 200;
|
|
177
|
+
reply.code(status);
|
|
178
|
+
if (result?.headers) {
|
|
179
|
+
Object.entries(result.headers).forEach(([key, value]) =>
|
|
180
|
+
reply.header(key, value as string),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return reply.send(result?.body ?? result);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
async run(): Promise<void> {
|
|
@@ -167,11 +215,21 @@ class HttpServer implements ResourceInstance {
|
|
|
167
215
|
}
|
|
168
216
|
}
|
|
169
217
|
|
|
170
|
-
export function create(
|
|
218
|
+
export async function create(
|
|
171
219
|
resource: HttpServerResource,
|
|
172
220
|
ctx: ResourceContext,
|
|
173
|
-
): ResourceInstance | null {
|
|
174
|
-
|
|
221
|
+
): Promise<ResourceInstance | null> {
|
|
222
|
+
let resolvedNotFoundHandler: ResolvedHandler | null = null;
|
|
223
|
+
if (resource.notFoundHandler) {
|
|
224
|
+
const resolved = ctx.resolveChildren(resource.notFoundHandler.invoke);
|
|
225
|
+
resolvedNotFoundHandler = {
|
|
226
|
+
kind: resolved.kind,
|
|
227
|
+
name: resolved.name,
|
|
228
|
+
inputs: (resource.notFoundHandler.invoke as any).inputs ?? {},
|
|
229
|
+
response: resource.notFoundHandler.response,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return new HttpServer(resource, ctx, resolvedNotFoundHandler);
|
|
175
233
|
}
|
|
176
234
|
|
|
177
235
|
function parseType(type: string): { kind: string; name: string } {
|
|
@@ -181,3 +239,69 @@ function parseType(type: string): { kind: string; name: string } {
|
|
|
181
239
|
}
|
|
182
240
|
return { kind: type.slice(0, separator), name: type.slice(separator + 1) };
|
|
183
241
|
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Converts Fastify validation errors to standardized Telo format
|
|
245
|
+
* Returns null if the error is not a validation error
|
|
246
|
+
*/
|
|
247
|
+
function convertFastifyValidationError(error: any): Record<string, any> | null {
|
|
248
|
+
// Check if this is a Fastify validation error
|
|
249
|
+
if (!error || typeof error !== "object" || error.code !== "FST_ERR_VALIDATION") {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const message = error.message || "";
|
|
254
|
+
const details = [];
|
|
255
|
+
|
|
256
|
+
// Parse Fastify validation error message to extract location and field
|
|
257
|
+
// Format examples:
|
|
258
|
+
// "querystring must have required property 'name'"
|
|
259
|
+
// "body must be object"
|
|
260
|
+
// "params.userId must be string"
|
|
261
|
+
|
|
262
|
+
let location = "body"; // default
|
|
263
|
+
let fieldPath = "";
|
|
264
|
+
let validationMessage = "Validation failed";
|
|
265
|
+
|
|
266
|
+
// Try to extract location from message
|
|
267
|
+
if (message.includes("querystring")) {
|
|
268
|
+
location = "query";
|
|
269
|
+
} else if (message.includes("params")) {
|
|
270
|
+
location = "params";
|
|
271
|
+
} else if (message.includes("headers")) {
|
|
272
|
+
location = "headers";
|
|
273
|
+
} else if (message.includes("body")) {
|
|
274
|
+
location = "body";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Extract field name from "must have required property 'fieldName'" pattern
|
|
278
|
+
const requiredMatch = message.match(/must have required property '([^']+)'/);
|
|
279
|
+
if (requiredMatch) {
|
|
280
|
+
fieldPath = requiredMatch[1];
|
|
281
|
+
validationMessage = `is a required property`;
|
|
282
|
+
} else {
|
|
283
|
+
// Extract field from "fieldName must be" pattern
|
|
284
|
+
const fieldMatch = message.match(/^(?:querystring|body|params|headers)\.?(\w+)\s/);
|
|
285
|
+
if (fieldMatch) {
|
|
286
|
+
fieldPath = fieldMatch[1];
|
|
287
|
+
}
|
|
288
|
+
validationMessage = message
|
|
289
|
+
.replace(/^(?:querystring|body|params|headers)\.?\w*\s/, "")
|
|
290
|
+
.replace(" must ", " ");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (fieldPath || message) {
|
|
294
|
+
details.push({
|
|
295
|
+
location,
|
|
296
|
+
path: fieldPath,
|
|
297
|
+
message: validationMessage,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
error: "ValidationError",
|
|
303
|
+
message: "Request validation failed",
|
|
304
|
+
status: 400,
|
|
305
|
+
details,
|
|
306
|
+
};
|
|
307
|
+
}
|
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
|
-
}
|