@athenna/http 5.45.0 → 5.47.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/exceptions/ZodValidationException.d.ts +13 -0
- package/src/exceptions/ZodValidationException.js +19 -0
- 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 +21 -1
- package/src/router/RouteSchema.d.ts +30 -0
- package/src/router/RouteSchema.js +144 -0
- package/src/server/ServerImpl.d.ts +7 -0
- package/src/server/ServerImpl.js +111 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@athenna/http",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.47.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,
|
|
@@ -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
|
+
}
|
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
|
*/
|
|
@@ -15,7 +15,7 @@ export class RouteResource extends Macroable {
|
|
|
15
15
|
* All routes registered in the resource.
|
|
16
16
|
*/
|
|
17
17
|
this.routes = [];
|
|
18
|
-
this.resource = resource;
|
|
18
|
+
this.resource = resource.replace(/^\/|\/$/g, '');
|
|
19
19
|
this.controller = controller;
|
|
20
20
|
this.buildRoutes();
|
|
21
21
|
}
|
|
@@ -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,144 @@
|
|
|
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
|
+
export function normalizeRouteSchema(options) {
|
|
12
|
+
const request = {};
|
|
13
|
+
const response = {};
|
|
14
|
+
const schema = { ...options };
|
|
15
|
+
const requestKeys = ['body', 'headers', 'params', 'querystring'];
|
|
16
|
+
requestKeys.forEach(key => {
|
|
17
|
+
if (!isZodSchema(options[key])) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
request[key] = options[key];
|
|
21
|
+
schema[key] = toJsonSchema(options[key], 'input');
|
|
22
|
+
});
|
|
23
|
+
if (options.response && Is.Object(options.response)) {
|
|
24
|
+
schema.response = { ...options.response };
|
|
25
|
+
Object.entries(options.response).forEach(([statusCode, value]) => {
|
|
26
|
+
if (!isZodSchema(value)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
response[statusCode] = value;
|
|
30
|
+
schema.response[statusCode] = toJsonSchema(value, 'output');
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const hasZodSchemas = Object.keys(request).length > 0 || Object.keys(response).length > 0;
|
|
34
|
+
return {
|
|
35
|
+
schema,
|
|
36
|
+
zod: hasZodSchemas ? { request, response } : null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function parseRequestWithZod(req, schemas) {
|
|
40
|
+
const requestSchemas = schemas.request;
|
|
41
|
+
if (requestSchemas.body) {
|
|
42
|
+
req.body = await parseSchema(requestSchemas.body, req.body);
|
|
43
|
+
}
|
|
44
|
+
if (requestSchemas.headers) {
|
|
45
|
+
req.headers = await parseSchema(requestSchemas.headers, req.headers);
|
|
46
|
+
}
|
|
47
|
+
if (requestSchemas.params) {
|
|
48
|
+
req.params = await parseSchema(requestSchemas.params, coerceDataForValidation(requestSchemas.params, req.params));
|
|
49
|
+
}
|
|
50
|
+
if (requestSchemas.querystring) {
|
|
51
|
+
req.query = await parseSchema(requestSchemas.querystring, coerceDataForValidation(requestSchemas.querystring, req.query));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function parseResponseWithZod(reply, payload, schemas) {
|
|
55
|
+
const schema = getResponseSchema(reply.statusCode, schemas.response);
|
|
56
|
+
if (!schema) {
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
return parseSchema(schema, payload);
|
|
60
|
+
}
|
|
61
|
+
function getResponseSchema(statusCode, schemas) {
|
|
62
|
+
return (schemas[statusCode] ||
|
|
63
|
+
schemas[String(statusCode)] ||
|
|
64
|
+
schemas[`${String(statusCode)[0]}xx`] ||
|
|
65
|
+
schemas.default ||
|
|
66
|
+
null);
|
|
67
|
+
}
|
|
68
|
+
async function parseSchema(schema, data) {
|
|
69
|
+
const result = await schema.safeParseAsync(data);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
throw new ZodValidationException(result.error);
|
|
72
|
+
}
|
|
73
|
+
return result.data;
|
|
74
|
+
}
|
|
75
|
+
function toJsonSchema(schema, io) {
|
|
76
|
+
const jsonSchemaMethod = schema['~standard']?.jsonSchema?.[io] ||
|
|
77
|
+
schema.toJSONSchema;
|
|
78
|
+
if (!jsonSchemaMethod) {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
const jsonSchema = jsonSchemaMethod({
|
|
82
|
+
target: 'draft-07',
|
|
83
|
+
libraryOptions: { unrepresentable: 'any' }
|
|
84
|
+
});
|
|
85
|
+
delete jsonSchema.$schema;
|
|
86
|
+
return jsonSchema;
|
|
87
|
+
}
|
|
88
|
+
function coerceDataForValidation(schema, data) {
|
|
89
|
+
return coerceDataByJsonSchema(toJsonSchema(schema, 'input'), data);
|
|
90
|
+
}
|
|
91
|
+
function coerceDataByJsonSchema(schema, data) {
|
|
92
|
+
if (Is.Undefined(data) || Is.Null(data) || !schema) {
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
if (schema.anyOf) {
|
|
96
|
+
return coerceWithAlternatives(schema.anyOf, data);
|
|
97
|
+
}
|
|
98
|
+
if (schema.oneOf) {
|
|
99
|
+
return coerceWithAlternatives(schema.oneOf, data);
|
|
100
|
+
}
|
|
101
|
+
if (schema.type === 'object' && Is.Object(data)) {
|
|
102
|
+
const coerced = { ...data };
|
|
103
|
+
const properties = schema.properties || {};
|
|
104
|
+
Object.entries(properties).forEach(([key, childSchema]) => {
|
|
105
|
+
if (!Object.hasOwn(coerced, key)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
coerced[key] = coerceDataByJsonSchema(childSchema, coerced[key]);
|
|
109
|
+
});
|
|
110
|
+
return coerced;
|
|
111
|
+
}
|
|
112
|
+
if (schema.type === 'array' && Is.Array(data) && schema.items) {
|
|
113
|
+
return data.map(item => coerceDataByJsonSchema(schema.items, item));
|
|
114
|
+
}
|
|
115
|
+
if (schema.type === 'number' || schema.type === 'integer') {
|
|
116
|
+
return coerceNumber(data, schema.type === 'integer');
|
|
117
|
+
}
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
function coerceWithAlternatives(schemas, data) {
|
|
121
|
+
let coerced = data;
|
|
122
|
+
schemas.forEach(schema => {
|
|
123
|
+
coerced = coerceDataByJsonSchema(schema, coerced);
|
|
124
|
+
});
|
|
125
|
+
return coerced;
|
|
126
|
+
}
|
|
127
|
+
function coerceNumber(value, integerOnly) {
|
|
128
|
+
if (!Is.String(value) || value.trim() === '') {
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
const parsed = integerOnly ? Number.parseInt(value, 10) : Number(value);
|
|
132
|
+
if (Number.isNaN(parsed)) {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
if (integerOnly && !Number.isInteger(parsed)) {
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
return parsed;
|
|
139
|
+
}
|
|
140
|
+
function isZodSchema(value) {
|
|
141
|
+
return (Is.Defined(value) &&
|
|
142
|
+
Is.Function(value.parse) &&
|
|
143
|
+
Is.Function(value.safeParseAsync));
|
|
144
|
+
}
|
|
@@ -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 { normalizeRouteSchema, parseRequestWithZod, parseResponseWithZod } 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,9 +188,15 @@ 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: [],
|
|
199
|
+
preSerialization: [],
|
|
193
200
|
onResponse: [],
|
|
194
201
|
url: options.url,
|
|
195
202
|
method: options.methods,
|
|
@@ -204,6 +211,12 @@ export class ServerImpl extends Macroable {
|
|
|
204
211
|
if (terminators.length) {
|
|
205
212
|
route.onResponse = terminators.map(t => FastifyHandler.terminate(t));
|
|
206
213
|
}
|
|
214
|
+
if (zodSchemas) {
|
|
215
|
+
route.preValidation = [async (req) => parseRequestWithZod(req, zodSchemas)];
|
|
216
|
+
route.preSerialization = [
|
|
217
|
+
async (_, reply, payload) => parseResponseWithZod(reply, payload, zodSchemas)
|
|
218
|
+
];
|
|
219
|
+
}
|
|
207
220
|
if (options.data && Is.Array(route.preHandler)) {
|
|
208
221
|
route.preHandler?.unshift((req, _, done) => {
|
|
209
222
|
req.data = {
|
|
@@ -213,7 +226,17 @@ export class ServerImpl extends Macroable {
|
|
|
213
226
|
done();
|
|
214
227
|
});
|
|
215
228
|
}
|
|
216
|
-
|
|
229
|
+
if (zodSchemas) {
|
|
230
|
+
fastifyOptions.preValidation = [
|
|
231
|
+
...this.toRouteHooks(route.preValidation),
|
|
232
|
+
...this.toRouteHooks(fastifyOptions.preValidation)
|
|
233
|
+
];
|
|
234
|
+
fastifyOptions.preSerialization = [
|
|
235
|
+
...this.toRouteHooks(route.preSerialization),
|
|
236
|
+
...this.toRouteHooks(fastifyOptions.preSerialization)
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
this.fastify.route({ ...route, ...fastifyOptions });
|
|
217
240
|
}
|
|
218
241
|
/**
|
|
219
242
|
* Add a new GET route to the http server.
|
|
@@ -257,4 +280,91 @@ export class ServerImpl extends Macroable {
|
|
|
257
280
|
options(options) {
|
|
258
281
|
this.route({ ...options, methods: ['OPTIONS'] });
|
|
259
282
|
}
|
|
283
|
+
toRouteHooks(hooks) {
|
|
284
|
+
if (!hooks) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return Array.isArray(hooks) ? hooks : [hooks];
|
|
288
|
+
}
|
|
289
|
+
getFastifyOptionsWithOpenApiSchema(options) {
|
|
290
|
+
const automaticSchema = this.getOpenApiRouteSchema(options);
|
|
291
|
+
const fastifyOptions = { ...options.fastify };
|
|
292
|
+
if (!automaticSchema) {
|
|
293
|
+
return fastifyOptions;
|
|
294
|
+
}
|
|
295
|
+
const normalizedSchema = normalizeRouteSchema(automaticSchema);
|
|
296
|
+
const currentConfig = { ...(fastifyOptions.config || {}) };
|
|
297
|
+
fastifyOptions.schema = this.mergeFastifySchemas(normalizedSchema.schema, fastifyOptions.schema);
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
299
|
+
// @ts-ignore
|
|
300
|
+
const currentZod = currentConfig.zod;
|
|
301
|
+
const mergedZod = this.mergeZodSchemas(normalizedSchema.zod, currentZod);
|
|
302
|
+
if (mergedZod) {
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
304
|
+
// @ts-ignore
|
|
305
|
+
currentConfig.zod = mergedZod;
|
|
306
|
+
}
|
|
307
|
+
fastifyOptions.config = currentConfig;
|
|
308
|
+
return fastifyOptions;
|
|
309
|
+
}
|
|
310
|
+
getOpenApiRouteSchema(options) {
|
|
311
|
+
const paths = Config.get('openapi.paths', {});
|
|
312
|
+
const methods = options.methods || [];
|
|
313
|
+
if (!Is.Object(paths) || !options.url || !methods.length) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const candidates = this.getOpenApiPathCandidates(options.url);
|
|
317
|
+
for (const candidate of candidates) {
|
|
318
|
+
const pathConfig = paths[candidate];
|
|
319
|
+
if (!Is.Object(pathConfig)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
for (const method of methods) {
|
|
323
|
+
const methodConfig = pathConfig[method.toLowerCase()];
|
|
324
|
+
if (Is.Object(methodConfig)) {
|
|
325
|
+
return methodConfig;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
getOpenApiPathCandidates(url) {
|
|
332
|
+
const normalized = this.normalizePath(url);
|
|
333
|
+
const openApi = normalized.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
334
|
+
return Array.from(new Set([normalized, openApi]));
|
|
335
|
+
}
|
|
336
|
+
normalizePath(url) {
|
|
337
|
+
if (url === '/') {
|
|
338
|
+
return url;
|
|
339
|
+
}
|
|
340
|
+
return `/${url.replace(/^\//, '').replace(/\/$/, '')}`;
|
|
341
|
+
}
|
|
342
|
+
mergeFastifySchemas(base, override) {
|
|
343
|
+
const merged = {
|
|
344
|
+
...base,
|
|
345
|
+
...override
|
|
346
|
+
};
|
|
347
|
+
if (base?.response || override?.response) {
|
|
348
|
+
merged.response = {
|
|
349
|
+
...(base?.response || {}),
|
|
350
|
+
...(override?.response || {})
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return merged;
|
|
354
|
+
}
|
|
355
|
+
mergeZodSchemas(base, override) {
|
|
356
|
+
if (!base && !override) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
request: {
|
|
361
|
+
...(base?.request || {}),
|
|
362
|
+
...(override?.request || {})
|
|
363
|
+
},
|
|
364
|
+
response: {
|
|
365
|
+
...(base?.response || {}),
|
|
366
|
+
...(override?.response || {})
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
260
370
|
}
|