@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,6 +1,8 @@
|
|
|
1
1
|
import { Static, Type } from "@sinclair/typebox";
|
|
2
|
-
import { ControllerContext, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
import { ControllerContext, Invocable, KindRef, Ref, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
3
3
|
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
4
|
+
import { type Readable } from "stream";
|
|
5
|
+
import { pipeline } from "stream/promises";
|
|
4
6
|
|
|
5
7
|
const HttpApiRouteManifest = Type.Object({
|
|
6
8
|
request: Type.Object({
|
|
@@ -15,24 +17,23 @@ const HttpApiRouteManifest = Type.Object({
|
|
|
15
17
|
}),
|
|
16
18
|
),
|
|
17
19
|
}),
|
|
18
|
-
handler: Type.Optional(Type.
|
|
19
|
-
response: Type.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Type.String(),
|
|
23
|
-
Type.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}),
|
|
20
|
+
handler: Type.Optional(Type.Unsafe<KindRef<Invocable>>(Ref("kernel#Invocable"))),
|
|
21
|
+
response: Type.Array(
|
|
22
|
+
Type.Object({
|
|
23
|
+
status: Type.Integer({ minimum: 100, maximum: 599 }),
|
|
24
|
+
when: Type.Optional(Type.String()),
|
|
25
|
+
mode: Type.Optional(Type.Union([Type.Literal("buffer"), Type.Literal("stream")])),
|
|
26
|
+
schema: Type.Optional(
|
|
27
|
+
Type.Object({
|
|
28
|
+
query: Type.Optional(Type.Any()),
|
|
29
|
+
body: Type.Optional(Type.Any()),
|
|
30
|
+
headers: Type.Optional(Type.Any()),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
34
|
+
body: Type.Optional(Type.Any()),
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
36
37
|
});
|
|
37
38
|
type HttpApiRouteManifest = Static<typeof HttpApiRouteManifest>;
|
|
38
39
|
|
|
@@ -41,7 +42,74 @@ const HttpApiManifest = Type.Object({
|
|
|
41
42
|
});
|
|
42
43
|
type HttpApiManifest = Static<typeof HttpApiManifest>;
|
|
43
44
|
|
|
44
|
-
export async function register(
|
|
45
|
+
export async function register(_ctx: ControllerContext): Promise<void> {}
|
|
46
|
+
|
|
47
|
+
export type ResponseEntry = Static<
|
|
48
|
+
(typeof HttpApiRouteManifest)["properties"]["response"]["items"]
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
export async function dispatchResponse(
|
|
52
|
+
response: ResponseEntry[],
|
|
53
|
+
result: unknown,
|
|
54
|
+
requestContext: Record<string, unknown>,
|
|
55
|
+
moduleContext: { expandWith: (v: unknown, ctx: Record<string, unknown>) => unknown },
|
|
56
|
+
validateSchema: (value: unknown, schema: unknown) => void,
|
|
57
|
+
reply: FastifyReply,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
let matched: ResponseEntry | undefined;
|
|
60
|
+
let fallback: ResponseEntry | undefined;
|
|
61
|
+
|
|
62
|
+
for (const entry of response) {
|
|
63
|
+
if (!entry.when) {
|
|
64
|
+
fallback ??= entry;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const condition = moduleContext.expandWith(entry.when, { result, ...requestContext });
|
|
68
|
+
if (condition === true) {
|
|
69
|
+
matched = entry;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const statusEntry = matched ?? fallback;
|
|
75
|
+
if (!statusEntry) {
|
|
76
|
+
reply.code(500);
|
|
77
|
+
reply.send({
|
|
78
|
+
error: "InternalServerError",
|
|
79
|
+
message: "No matching response status entry",
|
|
80
|
+
status: 500,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
reply.code(statusEntry.status);
|
|
86
|
+
|
|
87
|
+
if (statusEntry.headers) {
|
|
88
|
+
const mappedHeaders = moduleContext.expandWith(statusEntry.headers, {
|
|
89
|
+
result,
|
|
90
|
+
...requestContext,
|
|
91
|
+
}) as Record<string, unknown>;
|
|
92
|
+
Object.entries(mappedHeaders).forEach(([key, value]) => reply.header(key, value as string));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (statusEntry.mode === "stream") {
|
|
96
|
+
reply.hijack();
|
|
97
|
+
reply.raw.writeHead(statusEntry.status, reply.getHeaders() as Record<string, string>);
|
|
98
|
+
await pipeline(result as Readable, reply.raw);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (statusEntry.body !== undefined) {
|
|
103
|
+
const mappedBody = moduleContext.expandWith(statusEntry.body, { result, ...requestContext });
|
|
104
|
+
if (statusEntry.schema?.body) {
|
|
105
|
+
validateSchema(mappedBody, statusEntry.schema.body);
|
|
106
|
+
}
|
|
107
|
+
reply.send(mappedBody);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reply.send(result);
|
|
112
|
+
}
|
|
45
113
|
|
|
46
114
|
export class HttpServerApi implements ResourceInstance {
|
|
47
115
|
constructor(
|
|
@@ -52,17 +120,6 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
52
120
|
async init() {}
|
|
53
121
|
|
|
54
122
|
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
123
|
if (prefix) {
|
|
67
124
|
app.register(
|
|
68
125
|
async (scoped) => {
|
|
@@ -83,7 +140,8 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
83
140
|
}
|
|
84
141
|
|
|
85
142
|
private registerRoute(app: FastifyInstance, route: HttpApiRouteManifest) {
|
|
86
|
-
|
|
143
|
+
// After Phase 5 injection, KindRef<Invocable> is replaced with the live Invocable instance.
|
|
144
|
+
const handler = route.handler as unknown as Invocable | undefined;
|
|
87
145
|
const translatedPath = translateOpenApiPath(route.request.path);
|
|
88
146
|
|
|
89
147
|
const schema: any = {
|
|
@@ -103,24 +161,16 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
103
161
|
schema.headers = route.request.schema?.headers;
|
|
104
162
|
}
|
|
105
163
|
|
|
106
|
-
schema.response =
|
|
107
|
-
(acc,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
acc[status].querystring = statusConfig.schema.query;
|
|
113
|
-
}
|
|
114
|
-
if (statusConfig.schema.body) {
|
|
115
|
-
acc[status].body = statusConfig.schema.body;
|
|
116
|
-
}
|
|
117
|
-
if (statusConfig.schema.headers) {
|
|
118
|
-
acc[status].headers = statusConfig.schema.headers;
|
|
119
|
-
}
|
|
164
|
+
schema.response = route.response.reduce(
|
|
165
|
+
(acc, entry) => {
|
|
166
|
+
if (entry.schema?.body) {
|
|
167
|
+
acc[entry.status] = entry.schema.body;
|
|
168
|
+
} else if (entry.schema) {
|
|
169
|
+
acc[entry.status] = {};
|
|
120
170
|
}
|
|
121
171
|
return acc;
|
|
122
172
|
},
|
|
123
|
-
{} as Record<
|
|
173
|
+
{} as Record<number, any>,
|
|
124
174
|
);
|
|
125
175
|
|
|
126
176
|
app.route({
|
|
@@ -129,76 +179,26 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
129
179
|
schema,
|
|
130
180
|
handler: async (request: FastifyRequest, reply: FastifyReply) => {
|
|
131
181
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
headers: normalizedHeaders,
|
|
142
|
-
body: request.body,
|
|
182
|
+
const requestContext = {
|
|
183
|
+
request: {
|
|
184
|
+
method: request.method,
|
|
185
|
+
path: request.url,
|
|
186
|
+
params: request.params || {},
|
|
187
|
+
query: request.query || {},
|
|
188
|
+
headers: normalizeHeaders(request.headers),
|
|
189
|
+
body: request.body,
|
|
190
|
+
},
|
|
143
191
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
: undefined;
|
|
155
|
-
|
|
156
|
-
const response = route.response;
|
|
157
|
-
|
|
158
|
-
// Determine final status code
|
|
159
|
-
let statusCode = response.status;
|
|
160
|
-
if (typeof statusCode === "string") {
|
|
161
|
-
statusCode = this.ctx.expandValue(statusCode, { result }) as number;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Convert status to string for lookup
|
|
165
|
-
const statusKey = String(statusCode);
|
|
166
|
-
const statusConfig = response.statuses[statusKey];
|
|
167
|
-
|
|
168
|
-
if (!statusConfig) {
|
|
169
|
-
reply.code(500);
|
|
170
|
-
return reply.send({
|
|
171
|
-
error: "InternalServerError",
|
|
172
|
-
message: "Response status configuration not found",
|
|
173
|
-
status: 500,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Set HTTP status code
|
|
178
|
-
reply.code(statusCode as number);
|
|
179
|
-
|
|
180
|
-
// Map and set response headers if specified
|
|
181
|
-
if (statusConfig.headers) {
|
|
182
|
-
const mappedHeaders = this.ctx.expandValue(statusConfig.headers, { result });
|
|
183
|
-
Object.entries(mappedHeaders).forEach(([key, value]) => {
|
|
184
|
-
reply.header(key, value as string);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Map and send response body if specified
|
|
189
|
-
if (statusConfig.body !== undefined) {
|
|
190
|
-
const mappedBody = this.ctx.expandValue(statusConfig.body, { result });
|
|
191
|
-
|
|
192
|
-
// Validate response body if schema is specified
|
|
193
|
-
if (statusConfig.schema && statusConfig.schema.body) {
|
|
194
|
-
this.ctx.validateSchema(mappedBody, statusConfig.schema.body);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return reply.send(mappedBody);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// No body mapping, send result as-is
|
|
201
|
-
return reply.send(result);
|
|
192
|
+
const result = handler ? await handler.invoke(requestContext) : undefined;
|
|
193
|
+
|
|
194
|
+
return dispatchResponse(
|
|
195
|
+
route.response,
|
|
196
|
+
result,
|
|
197
|
+
requestContext,
|
|
198
|
+
this.ctx.moduleContext,
|
|
199
|
+
this.ctx.validateSchema.bind(this.ctx),
|
|
200
|
+
reply,
|
|
201
|
+
);
|
|
202
202
|
} catch (error) {
|
|
203
203
|
// Let the error handler deal with all errors
|
|
204
204
|
throw error;
|
|
@@ -209,101 +209,8 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export async function create(resource: any, ctx: ResourceContext): Promise<HttpServerApi> {
|
|
212
|
-
// First validate with a permissive schema (handler can be any shape)
|
|
213
212
|
ctx.validateSchema(resource, HttpApiManifest);
|
|
214
|
-
|
|
215
|
-
let handlerCounter = 0;
|
|
216
|
-
const processedRoutes = (resource.routes || []).map((route: any) => {
|
|
217
|
-
if (!route.handler) {
|
|
218
|
-
return route;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Check if handler is unnamed (inline handler)
|
|
222
|
-
if (typeof route.handler === "object" && !route.handler.name) {
|
|
223
|
-
// Use resolveChildren to register the unnamed handler and get its normalized reference
|
|
224
|
-
const resolvedHandler = ctx.resolveChildren(route.handler, `__handler_${handlerCounter++}`);
|
|
225
|
-
|
|
226
|
-
// Return route with the resolved handler reference
|
|
227
|
-
return {
|
|
228
|
-
...route,
|
|
229
|
-
handler: {
|
|
230
|
-
kind: resolvedHandler.kind,
|
|
231
|
-
name: resolvedHandler.name,
|
|
232
|
-
inputs: route.handler.inputs,
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return route;
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// Create the API instance with processed routes
|
|
241
|
-
const processedResource: HttpApiManifest = {
|
|
242
|
-
...resource,
|
|
243
|
-
routes: processedRoutes,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
return new HttpServerApi(ctx, processedResource);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function resolveHandlerName(handler: any): { kind: string; name: string } {
|
|
250
|
-
if (typeof handler === "string") {
|
|
251
|
-
const [kind, name] = handler.split("/");
|
|
252
|
-
return { kind, name };
|
|
253
|
-
}
|
|
254
|
-
if (handler && typeof handler === "object" && typeof handler.kind === "string") {
|
|
255
|
-
// name should always be present after create() processes the routes
|
|
256
|
-
// but fallback gracefully if it's not
|
|
257
|
-
const name = handler.name || `__unnamed_${Math.random().toString(36).slice(2, 9)}`;
|
|
258
|
-
return { name, kind: handler.kind };
|
|
259
|
-
}
|
|
260
|
-
throw new Error("Unable to resolve handler - handler must have a 'kind' property");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function resolveHandlerInputs(handler: any, requestContext: Record<string, any>): any {
|
|
264
|
-
if (typeof handler === "string") {
|
|
265
|
-
return requestContext;
|
|
266
|
-
}
|
|
267
|
-
if (!handler || typeof handler !== "object") {
|
|
268
|
-
return requestContext;
|
|
269
|
-
}
|
|
270
|
-
if (!handler.inputs) {
|
|
271
|
-
return requestContext;
|
|
272
|
-
}
|
|
273
|
-
return resolveTemplateInputs(handler.inputs, requestContext);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function resolveTemplateInputs(value: any, context: Record<string, any>): any {
|
|
277
|
-
if (typeof value === "string") {
|
|
278
|
-
const match = value.match(/^\s*\$\{\{\s*([^}]+)\s*\}\}\s*$/);
|
|
279
|
-
if (match) {
|
|
280
|
-
return resolveTemplatePath(match[1], context);
|
|
281
|
-
}
|
|
282
|
-
return value;
|
|
283
|
-
}
|
|
284
|
-
if (Array.isArray(value)) {
|
|
285
|
-
return value.map((item) => resolveTemplateInputs(item, context));
|
|
286
|
-
}
|
|
287
|
-
if (value && typeof value === "object") {
|
|
288
|
-
const resolved: Record<string, any> = {};
|
|
289
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
290
|
-
resolved[key] = resolveTemplateInputs(entry, context);
|
|
291
|
-
}
|
|
292
|
-
return resolved;
|
|
293
|
-
}
|
|
294
|
-
return value;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function resolveTemplatePath(pathExpression: string, context: Record<string, any>): any {
|
|
298
|
-
const parts = pathExpression.trim().split(".").filter(Boolean);
|
|
299
|
-
let current: any = context;
|
|
300
|
-
for (const part of parts) {
|
|
301
|
-
if (!current || (typeof current !== "object" && typeof current !== "function")) {
|
|
302
|
-
return undefined;
|
|
303
|
-
}
|
|
304
|
-
current = current[part];
|
|
305
|
-
}
|
|
306
|
-
return current;
|
|
213
|
+
return new HttpServerApi(ctx, resource);
|
|
307
214
|
}
|
|
308
215
|
|
|
309
216
|
/**
|
|
@@ -324,125 +231,3 @@ function normalizeHeaders(headers: Record<string, any>): Record<string, any> {
|
|
|
324
231
|
}
|
|
325
232
|
return normalized;
|
|
326
233
|
}
|
|
327
|
-
|
|
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
|
-
/**
|
|
395
|
-
* Legacy function - kept for compatibility but not used
|
|
396
|
-
* Converts framework-specific validation errors to standardized Telo format
|
|
397
|
-
* Returns null if the error is not a validation error
|
|
398
|
-
*/
|
|
399
|
-
function convertValidationError(error: any): Record<string, any> | null {
|
|
400
|
-
// Check if this is a Fastify/AJV validation error
|
|
401
|
-
if (!error || typeof error !== "object") {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Fastify validation errors have a statusCode of 400 and validation array
|
|
406
|
-
if (error.statusCode === 400 && Array.isArray(error.validation)) {
|
|
407
|
-
const details = error.validation.map((err: any) => {
|
|
408
|
-
const path = err.instancePath ? err.instancePath.replace(/^\//, "").replace(/\//g, ".") : "";
|
|
409
|
-
|
|
410
|
-
// Determine location from keyword/message context
|
|
411
|
-
let location = "body"; // default
|
|
412
|
-
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
413
|
-
location = determinLocationFromContext(err);
|
|
414
|
-
} else {
|
|
415
|
-
location = determinLocationFromContext(err);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
location,
|
|
420
|
-
path: path || err.params?.missingProperty || "",
|
|
421
|
-
message: err.message || "Validation failed",
|
|
422
|
-
};
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
error: "ValidationError",
|
|
427
|
-
message: "Request validation failed",
|
|
428
|
-
status: 400,
|
|
429
|
-
details,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Helper to determine the location (body, query, params, headers) from validation error context
|
|
438
|
-
*/
|
|
439
|
-
function determinLocationFromContext(err: any): string {
|
|
440
|
-
// AJV validation errors in Fastify include parent keyword context
|
|
441
|
-
if (err.parentSchema && err.instancePath) {
|
|
442
|
-
const path = err.instancePath;
|
|
443
|
-
// This is a simplified check; in practice, Fastify provides better context
|
|
444
|
-
// For now, default to "body" for general validation errors
|
|
445
|
-
return "body";
|
|
446
|
-
}
|
|
447
|
-
return "body";
|
|
448
|
-
}
|