@athenna/http 5.46.0 → 5.48.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/package.json +3 -2
- package/src/context/Response.d.ts +1 -0
- package/src/context/Response.js +10 -0
- package/src/exceptions/ResponseValidationException.d.ts +5 -0
- package/src/exceptions/ResponseValidationException.js +11 -0
- package/src/exceptions/ZodValidationException.d.ts +13 -0
- package/src/exceptions/ZodValidationException.js +19 -0
- package/src/handlers/HttpExceptionHandler.js +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/router/Route.d.ts +3 -2
- package/src/router/Route.js +13 -1
- package/src/router/RouteResource.d.ts +13 -0
- package/src/router/RouteResource.js +20 -0
- package/src/router/RouteSchema.d.ts +30 -0
- package/src/router/RouteSchema.js +95 -0
- package/src/server/ServerImpl.d.ts +7 -0
- package/src/server/ServerImpl.js +103 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@athenna/http",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.48.0",
|
|
4
4
|
"description": "The Athenna Http server. Built on top of fastify.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "João Lenon <lenon@athenna.io>",
|
|
@@ -110,7 +110,8 @@
|
|
|
110
110
|
"ora": "^8.2.0",
|
|
111
111
|
"prettier": "^2.8.8",
|
|
112
112
|
"vite": "^6.4.1",
|
|
113
|
-
"vite-plugin-restart": "^0.4.2"
|
|
113
|
+
"vite-plugin-restart": "^0.4.2",
|
|
114
|
+
"zod": "^4.3.6"
|
|
114
115
|
},
|
|
115
116
|
"c8": {
|
|
116
117
|
"all": true,
|
package/src/context/Response.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { View } from '@athenna/view';
|
|
10
10
|
import { Macroable } from '@athenna/common';
|
|
11
|
+
import { parseResponseWithZod } from '#src/router/RouteSchema';
|
|
11
12
|
export class Response extends Macroable {
|
|
12
13
|
constructor(response, request) {
|
|
13
14
|
super();
|
|
@@ -126,10 +127,19 @@ export class Response extends Macroable {
|
|
|
126
127
|
* ```
|
|
127
128
|
*/
|
|
128
129
|
async send(data) {
|
|
130
|
+
const zodSchemas = this.getRouteZodSchemas();
|
|
131
|
+
if (zodSchemas) {
|
|
132
|
+
data = await parseResponseWithZod(this.response, data, zodSchemas);
|
|
133
|
+
}
|
|
129
134
|
await this.response.send(data);
|
|
130
135
|
this.response.body = data;
|
|
131
136
|
return this;
|
|
132
137
|
}
|
|
138
|
+
getRouteZodSchemas() {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
return this.response.request?.routeOptions?.config?.zod || null;
|
|
142
|
+
}
|
|
133
143
|
/**
|
|
134
144
|
* Terminate the request sending a file.
|
|
135
145
|
*
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HttpException } from '#src/exceptions/HttpException';
|
|
2
|
+
export class ResponseValidationException extends HttpException {
|
|
3
|
+
constructor(error) {
|
|
4
|
+
const name = 'ResponseValidationException';
|
|
5
|
+
const code = 'E_RESPONSE_VALIDATION_ERROR';
|
|
6
|
+
const status = 500;
|
|
7
|
+
const message = 'The server failed to generate a valid response.';
|
|
8
|
+
const details = error.issues;
|
|
9
|
+
super({ name, message, status, code, details });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @athenna/http
|
|
3
|
+
*
|
|
4
|
+
* (c) João Lenon <lenon@athenna.io>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
import type { ZodError } from 'zod';
|
|
10
|
+
import { HttpException } from '#src/exceptions/HttpException';
|
|
11
|
+
export declare class ZodValidationException extends HttpException {
|
|
12
|
+
constructor(error: ZodError);
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @athenna/http
|
|
3
|
+
*
|
|
4
|
+
* (c) João Lenon <lenon@athenna.io>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
import { HttpException } from '#src/exceptions/HttpException';
|
|
10
|
+
export class ZodValidationException extends HttpException {
|
|
11
|
+
constructor(error) {
|
|
12
|
+
const name = 'ValidationException';
|
|
13
|
+
const code = 'E_VALIDATION_ERROR';
|
|
14
|
+
const status = 422;
|
|
15
|
+
const message = 'Validation error happened.';
|
|
16
|
+
const details = error.issues;
|
|
17
|
+
super({ name, message, status, code, details });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -51,7 +51,7 @@ export class HttpExceptionHandler extends ExceptionHandler {
|
|
|
51
51
|
if (!isDebugMode) {
|
|
52
52
|
delete body.stack;
|
|
53
53
|
}
|
|
54
|
-
response.status(body.statusCode).send(body);
|
|
54
|
+
await response.status(body.statusCode).send(body);
|
|
55
55
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
56
56
|
// @ts-ignore
|
|
57
57
|
await super.handle({ error, response });
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
package/src/router/Route.d.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* file that was distributed with this source code.
|
|
8
8
|
*/
|
|
9
9
|
import type { RouteJson, RouteHandler, RequestHandler, MiddlewareRecord, MiddlewareRouteType, TerminatorRouteType, InterceptorRouteType } from '#src/types';
|
|
10
|
-
import type { HTTPMethods,
|
|
10
|
+
import type { HTTPMethods, RouteOptions } from 'fastify';
|
|
11
11
|
import { Macroable } from '@athenna/common';
|
|
12
|
+
import { type RouteSchemaOptions } from '#src/router/RouteSchema';
|
|
12
13
|
export declare class Route extends Macroable {
|
|
13
14
|
/**
|
|
14
15
|
* Holds all the route implementations to be registered in the Server.
|
|
@@ -111,7 +112,7 @@ export declare class Route extends Macroable {
|
|
|
111
112
|
* })
|
|
112
113
|
* ```
|
|
113
114
|
*/
|
|
114
|
-
schema(options:
|
|
115
|
+
schema(options: RouteSchemaOptions): Route;
|
|
115
116
|
/**
|
|
116
117
|
* Set up all rate limit options for route.
|
|
117
118
|
*
|
package/src/router/Route.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { Is, Options, Macroable, Route as RouteHelper } from '@athenna/common';
|
|
10
10
|
import { UndefinedMethodException } from '#src/exceptions/UndefinedMethodException';
|
|
11
11
|
import { NotFoundValidatorException } from '#src/exceptions/NotFoundValidatorException';
|
|
12
|
+
import { normalizeRouteSchema } from '#src/router/RouteSchema';
|
|
12
13
|
import { NotFoundMiddlewareException } from '#src/exceptions/NotFoundMiddlewareException';
|
|
13
14
|
export class Route extends Macroable {
|
|
14
15
|
constructor(url, methods, handler) {
|
|
@@ -237,7 +238,18 @@ export class Route extends Macroable {
|
|
|
237
238
|
* ```
|
|
238
239
|
*/
|
|
239
240
|
schema(options) {
|
|
240
|
-
|
|
241
|
+
const { schema, zod } = normalizeRouteSchema(options);
|
|
242
|
+
this.route.fastify.schema = schema;
|
|
243
|
+
if (zod) {
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
245
|
+
// @ts-ignore
|
|
246
|
+
this.route.fastify.config.zod = zod;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
250
|
+
// @ts-ignore
|
|
251
|
+
delete this.route.fastify.config.zod;
|
|
252
|
+
}
|
|
241
253
|
return this;
|
|
242
254
|
}
|
|
243
255
|
/**
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { RouteResourceTypes, TerminatorRouteType, MiddlewareRouteType, InterceptorRouteType } from '#src/types';
|
|
10
10
|
import { Route } from '#src/router/Route';
|
|
11
|
+
import type { RouteSchemaOptions } from '#src/router/RouteSchema';
|
|
11
12
|
import { Macroable } from '@athenna/common';
|
|
12
13
|
export declare class RouteResource extends Macroable {
|
|
13
14
|
/**
|
|
@@ -112,6 +113,18 @@ export declare class RouteResource extends Macroable {
|
|
|
112
113
|
* ```
|
|
113
114
|
*/
|
|
114
115
|
rateLimit(options: import('@fastify/rate-limit').RateLimitOptions): RouteResource;
|
|
116
|
+
/**
|
|
117
|
+
* Set up schema options for specific route resource methods.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* Route.resource('/test', 'TestController').schema({
|
|
122
|
+
* index: { response: { 200: { type: 'object' } } },
|
|
123
|
+
* store: { body: { type: 'object' } }
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
schema(options: Partial<Record<RouteResourceTypes, RouteSchemaOptions>>): RouteResource;
|
|
115
128
|
/**
|
|
116
129
|
* Filter routes by name.
|
|
117
130
|
*/
|
|
@@ -171,6 +171,26 @@ export class RouteResource extends Macroable {
|
|
|
171
171
|
this.routes.forEach(route => route.rateLimit(options));
|
|
172
172
|
return this;
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Set up schema options for specific route resource methods.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* Route.resource('/test', 'TestController').schema({
|
|
180
|
+
* index: { response: { 200: { type: 'object' } } },
|
|
181
|
+
* store: { body: { type: 'object' } }
|
|
182
|
+
* })
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
schema(options) {
|
|
186
|
+
Object.entries(options).forEach(([name, schema]) => {
|
|
187
|
+
if (!schema) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.filter([name]).forEach(route => route.schema(schema));
|
|
191
|
+
});
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
174
194
|
/**
|
|
175
195
|
* Filter routes by name.
|
|
176
196
|
*/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @athenna/http
|
|
3
|
+
*
|
|
4
|
+
* (c) João Lenon <lenon@athenna.io>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
import type { ZodAny } from 'zod';
|
|
10
|
+
import type { FastifyReply, FastifyRequest, FastifySchema } from 'fastify';
|
|
11
|
+
type ZodRequestSchema = Partial<Record<'body' | 'headers' | 'params' | 'querystring', ZodAny>>;
|
|
12
|
+
type ZodResponseSchema = Record<number | string, ZodAny>;
|
|
13
|
+
export type RouteSchemaOptions = FastifySchema & {
|
|
14
|
+
body?: FastifySchema['body'] | ZodAny;
|
|
15
|
+
headers?: FastifySchema['headers'] | ZodAny;
|
|
16
|
+
params?: FastifySchema['params'] | ZodAny;
|
|
17
|
+
querystring?: FastifySchema['querystring'] | ZodAny;
|
|
18
|
+
response?: FastifySchema['response'] | ZodResponseSchema;
|
|
19
|
+
};
|
|
20
|
+
export type RouteZodSchemas = {
|
|
21
|
+
request: ZodRequestSchema;
|
|
22
|
+
response: ZodResponseSchema;
|
|
23
|
+
};
|
|
24
|
+
export declare function normalizeRouteSchema(options: RouteSchemaOptions): {
|
|
25
|
+
schema: FastifySchema;
|
|
26
|
+
zod: RouteZodSchemas | null;
|
|
27
|
+
};
|
|
28
|
+
export declare function parseRequestWithZod(req: FastifyRequest, schemas: RouteZodSchemas): Promise<void>;
|
|
29
|
+
export declare function parseResponseWithZod(reply: FastifyReply, payload: any, schemas: RouteZodSchemas): Promise<any>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @athenna/http
|
|
3
|
+
*
|
|
4
|
+
* (c) João Lenon <lenon@athenna.io>
|
|
5
|
+
*
|
|
6
|
+
* For the full copyright and license information, please view the LICENSE
|
|
7
|
+
* file that was distributed with this source code.
|
|
8
|
+
*/
|
|
9
|
+
import { Is } from '@athenna/common';
|
|
10
|
+
import { ZodValidationException } from '#src/exceptions/ZodValidationException';
|
|
11
|
+
import { ResponseValidationException } from '#src/exceptions/ResponseValidationException';
|
|
12
|
+
export function normalizeRouteSchema(options) {
|
|
13
|
+
const request = {};
|
|
14
|
+
const response = {};
|
|
15
|
+
const schema = { ...options };
|
|
16
|
+
const requestKeys = ['body', 'headers', 'params', 'querystring'];
|
|
17
|
+
requestKeys.forEach(key => {
|
|
18
|
+
if (!isZodSchema(options[key])) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
request[key] = options[key];
|
|
22
|
+
schema[key] = toJsonSchema(options[key], 'input');
|
|
23
|
+
});
|
|
24
|
+
if (options.response && Is.Object(options.response)) {
|
|
25
|
+
schema.response = { ...options.response };
|
|
26
|
+
Object.entries(options.response).forEach(([statusCode, value]) => {
|
|
27
|
+
if (!isZodSchema(value)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
response[statusCode] = value;
|
|
31
|
+
schema.response[statusCode] = toJsonSchema(value, 'output');
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const hasZodSchemas = Object.keys(request).length > 0 || Object.keys(response).length > 0;
|
|
35
|
+
return {
|
|
36
|
+
schema,
|
|
37
|
+
zod: hasZodSchemas ? { request, response } : null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function parseRequestWithZod(req, schemas) {
|
|
41
|
+
const requestSchemas = schemas.request;
|
|
42
|
+
if (requestSchemas.body) {
|
|
43
|
+
req.body = await parseSchema(requestSchemas.body, req.body);
|
|
44
|
+
}
|
|
45
|
+
if (requestSchemas.headers) {
|
|
46
|
+
req.headers = await parseSchema(requestSchemas.headers, req.headers);
|
|
47
|
+
}
|
|
48
|
+
if (requestSchemas.params) {
|
|
49
|
+
req.params = await parseSchema(requestSchemas.params, req.params);
|
|
50
|
+
}
|
|
51
|
+
if (requestSchemas.querystring) {
|
|
52
|
+
req.query = await parseSchema(requestSchemas.querystring, req.query);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function parseResponseWithZod(reply, payload, schemas) {
|
|
56
|
+
const schema = getResponseSchema(reply.statusCode, schemas.response);
|
|
57
|
+
if (!schema) {
|
|
58
|
+
return payload;
|
|
59
|
+
}
|
|
60
|
+
return parseSchema(schema, payload).catch(error => {
|
|
61
|
+
throw new ResponseValidationException(error);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function getResponseSchema(statusCode, schemas) {
|
|
65
|
+
return (schemas[statusCode] ||
|
|
66
|
+
schemas[String(statusCode)] ||
|
|
67
|
+
schemas[`${String(statusCode)[0]}xx`] ||
|
|
68
|
+
schemas.default ||
|
|
69
|
+
null);
|
|
70
|
+
}
|
|
71
|
+
async function parseSchema(schema, data) {
|
|
72
|
+
const result = await schema.safeParseAsync(data);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
throw new ZodValidationException(result.error);
|
|
75
|
+
}
|
|
76
|
+
return result.data;
|
|
77
|
+
}
|
|
78
|
+
function toJsonSchema(schema, io) {
|
|
79
|
+
const jsonSchemaMethod = schema['~standard']?.jsonSchema?.[io] ||
|
|
80
|
+
schema.toJSONSchema;
|
|
81
|
+
if (!jsonSchemaMethod) {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
const jsonSchema = jsonSchemaMethod({
|
|
85
|
+
target: 'draft-07',
|
|
86
|
+
libraryOptions: { unrepresentable: 'any' }
|
|
87
|
+
});
|
|
88
|
+
delete jsonSchema.$schema;
|
|
89
|
+
return jsonSchema;
|
|
90
|
+
}
|
|
91
|
+
function isZodSchema(value) {
|
|
92
|
+
return (Is.Defined(value) &&
|
|
93
|
+
Is.Function(value.parse) &&
|
|
94
|
+
Is.Function(value.safeParseAsync));
|
|
95
|
+
}
|
|
@@ -136,4 +136,11 @@ export declare class ServerImpl extends Macroable {
|
|
|
136
136
|
* Add a new OPTIONS route to the http server.
|
|
137
137
|
*/
|
|
138
138
|
options(options: Omit<RouteJson, 'methods'>): void;
|
|
139
|
+
private toRouteHooks;
|
|
140
|
+
private getFastifyOptionsWithOpenApiSchema;
|
|
141
|
+
private getOpenApiRouteSchema;
|
|
142
|
+
private getOpenApiPathCandidates;
|
|
143
|
+
private normalizePath;
|
|
144
|
+
private mergeFastifySchemas;
|
|
145
|
+
private mergeZodSchemas;
|
|
139
146
|
}
|
package/src/server/ServerImpl.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* file that was distributed with this source code.
|
|
8
8
|
*/
|
|
9
9
|
import fastify from 'fastify';
|
|
10
|
+
import { parseRequestWithZod, normalizeRouteSchema } from '#src/router/RouteSchema';
|
|
10
11
|
import { Options, Macroable, Is } from '@athenna/common';
|
|
11
12
|
import { FastifyHandler } from '#src/handlers/FastifyHandler';
|
|
12
13
|
export class ServerImpl extends Macroable {
|
|
@@ -187,8 +188,13 @@ export class ServerImpl extends Macroable {
|
|
|
187
188
|
return;
|
|
188
189
|
}
|
|
189
190
|
const { middlewares, interceptors, terminators } = options.middlewares;
|
|
191
|
+
const fastifyOptions = this.getFastifyOptionsWithOpenApiSchema(options);
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
193
|
+
// @ts-ignore
|
|
194
|
+
const zodSchemas = fastifyOptions?.config?.zod;
|
|
190
195
|
const route = {
|
|
191
196
|
onSend: [],
|
|
197
|
+
preValidation: [],
|
|
192
198
|
preHandler: [],
|
|
193
199
|
onResponse: [],
|
|
194
200
|
url: options.url,
|
|
@@ -204,6 +210,9 @@ export class ServerImpl extends Macroable {
|
|
|
204
210
|
if (terminators.length) {
|
|
205
211
|
route.onResponse = terminators.map(t => FastifyHandler.terminate(t));
|
|
206
212
|
}
|
|
213
|
+
if (zodSchemas) {
|
|
214
|
+
route.preValidation = [async (req) => parseRequestWithZod(req, zodSchemas)];
|
|
215
|
+
}
|
|
207
216
|
if (options.data && Is.Array(route.preHandler)) {
|
|
208
217
|
route.preHandler?.unshift((req, _, done) => {
|
|
209
218
|
req.data = {
|
|
@@ -213,7 +222,13 @@ export class ServerImpl extends Macroable {
|
|
|
213
222
|
done();
|
|
214
223
|
});
|
|
215
224
|
}
|
|
216
|
-
|
|
225
|
+
if (zodSchemas) {
|
|
226
|
+
fastifyOptions.preValidation = [
|
|
227
|
+
...this.toRouteHooks(route.preValidation),
|
|
228
|
+
...this.toRouteHooks(fastifyOptions.preValidation)
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
this.fastify.route({ ...route, ...fastifyOptions });
|
|
217
232
|
}
|
|
218
233
|
/**
|
|
219
234
|
* Add a new GET route to the http server.
|
|
@@ -257,4 +272,91 @@ export class ServerImpl extends Macroable {
|
|
|
257
272
|
options(options) {
|
|
258
273
|
this.route({ ...options, methods: ['OPTIONS'] });
|
|
259
274
|
}
|
|
275
|
+
toRouteHooks(hooks) {
|
|
276
|
+
if (!hooks) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
return Array.isArray(hooks) ? hooks : [hooks];
|
|
280
|
+
}
|
|
281
|
+
getFastifyOptionsWithOpenApiSchema(options) {
|
|
282
|
+
const automaticSchema = this.getOpenApiRouteSchema(options);
|
|
283
|
+
const fastifyOptions = { ...options.fastify };
|
|
284
|
+
if (!automaticSchema) {
|
|
285
|
+
return fastifyOptions;
|
|
286
|
+
}
|
|
287
|
+
const normalizedSchema = normalizeRouteSchema(automaticSchema);
|
|
288
|
+
const currentConfig = { ...(fastifyOptions.config || {}) };
|
|
289
|
+
fastifyOptions.schema = this.mergeFastifySchemas(normalizedSchema.schema, fastifyOptions.schema);
|
|
290
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
291
|
+
// @ts-ignore
|
|
292
|
+
const currentZod = currentConfig.zod;
|
|
293
|
+
const mergedZod = this.mergeZodSchemas(normalizedSchema.zod, currentZod);
|
|
294
|
+
if (mergedZod) {
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
296
|
+
// @ts-ignore
|
|
297
|
+
currentConfig.zod = mergedZod;
|
|
298
|
+
}
|
|
299
|
+
fastifyOptions.config = currentConfig;
|
|
300
|
+
return fastifyOptions;
|
|
301
|
+
}
|
|
302
|
+
getOpenApiRouteSchema(options) {
|
|
303
|
+
const paths = Config.get('openapi.paths', {});
|
|
304
|
+
const methods = options.methods || [];
|
|
305
|
+
if (!Is.Object(paths) || !options.url || !methods.length) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const candidates = this.getOpenApiPathCandidates(options.url);
|
|
309
|
+
for (const candidate of candidates) {
|
|
310
|
+
const pathConfig = paths[candidate];
|
|
311
|
+
if (!Is.Object(pathConfig)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
for (const method of methods) {
|
|
315
|
+
const methodConfig = pathConfig[method.toLowerCase()];
|
|
316
|
+
if (Is.Object(methodConfig)) {
|
|
317
|
+
return methodConfig;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
getOpenApiPathCandidates(url) {
|
|
324
|
+
const normalized = this.normalizePath(url);
|
|
325
|
+
const openApi = normalized.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
326
|
+
return Array.from(new Set([normalized, openApi]));
|
|
327
|
+
}
|
|
328
|
+
normalizePath(url) {
|
|
329
|
+
if (url === '/') {
|
|
330
|
+
return url;
|
|
331
|
+
}
|
|
332
|
+
return `/${url.replace(/^\//, '').replace(/\/$/, '')}`;
|
|
333
|
+
}
|
|
334
|
+
mergeFastifySchemas(base, override) {
|
|
335
|
+
const merged = {
|
|
336
|
+
...base,
|
|
337
|
+
...override
|
|
338
|
+
};
|
|
339
|
+
if (base?.response || override?.response) {
|
|
340
|
+
merged.response = {
|
|
341
|
+
...(base?.response || {}),
|
|
342
|
+
...(override?.response || {})
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return merged;
|
|
346
|
+
}
|
|
347
|
+
mergeZodSchemas(base, override) {
|
|
348
|
+
if (!base && !override) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
request: {
|
|
353
|
+
...(base?.request || {}),
|
|
354
|
+
...(override?.request || {})
|
|
355
|
+
},
|
|
356
|
+
response: {
|
|
357
|
+
...(base?.response || {}),
|
|
358
|
+
...(override?.response || {})
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
260
362
|
}
|