@athenna/http 5.47.0 → 5.49.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 +1 -1
- package/src/context/Response.d.ts +1 -0
- package/src/context/Response.js +10 -0
- package/src/handlers/HttpExceptionHandler.js +1 -1
- package/src/router/Route.js +9 -1
- package/src/router/RouteSchema.d.ts +1 -0
- package/src/router/RouteSchema.js +13 -56
- package/src/server/ServerImpl.d.ts +1 -0
- package/src/server/ServerImpl.js +32 -9
package/package.json
CHANGED
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
|
*
|
|
@@ -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/router/Route.js
CHANGED
|
@@ -238,17 +238,25 @@ export class Route extends Macroable {
|
|
|
238
238
|
* ```
|
|
239
239
|
*/
|
|
240
240
|
schema(options) {
|
|
241
|
-
const { schema, zod } = normalizeRouteSchema(options);
|
|
241
|
+
const { schema, swaggerSchema, zod } = normalizeRouteSchema(options);
|
|
242
242
|
this.route.fastify.schema = schema;
|
|
243
243
|
if (zod) {
|
|
244
244
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
245
245
|
// @ts-ignore
|
|
246
246
|
this.route.fastify.config.zod = zod;
|
|
247
|
+
if (Object.keys(zod.response).length) {
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
this.route.fastify.config.swaggerSchema = swaggerSchema;
|
|
251
|
+
}
|
|
247
252
|
}
|
|
248
253
|
else {
|
|
249
254
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
250
255
|
// @ts-ignore
|
|
251
256
|
delete this.route.fastify.config.zod;
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
delete this.route.fastify.config.swaggerSchema;
|
|
252
260
|
}
|
|
253
261
|
return this;
|
|
254
262
|
}
|
|
@@ -23,6 +23,7 @@ export type RouteZodSchemas = {
|
|
|
23
23
|
};
|
|
24
24
|
export declare function normalizeRouteSchema(options: RouteSchemaOptions): {
|
|
25
25
|
schema: FastifySchema;
|
|
26
|
+
swaggerSchema: FastifySchema;
|
|
26
27
|
zod: RouteZodSchemas | null;
|
|
27
28
|
};
|
|
28
29
|
export declare function parseRequestWithZod(req: FastifyRequest, schemas: RouteZodSchemas): Promise<void>;
|
|
@@ -12,6 +12,7 @@ export function normalizeRouteSchema(options) {
|
|
|
12
12
|
const request = {};
|
|
13
13
|
const response = {};
|
|
14
14
|
const schema = { ...options };
|
|
15
|
+
const swaggerSchema = { ...options };
|
|
15
16
|
const requestKeys = ['body', 'headers', 'params', 'querystring'];
|
|
16
17
|
requestKeys.forEach(key => {
|
|
17
18
|
if (!isZodSchema(options[key])) {
|
|
@@ -19,20 +20,27 @@ export function normalizeRouteSchema(options) {
|
|
|
19
20
|
}
|
|
20
21
|
request[key] = options[key];
|
|
21
22
|
schema[key] = toJsonSchema(options[key], 'input');
|
|
23
|
+
swaggerSchema[key] = toJsonSchema(options[key], 'input');
|
|
22
24
|
});
|
|
23
25
|
if (options.response && Is.Object(options.response)) {
|
|
24
26
|
schema.response = { ...options.response };
|
|
27
|
+
swaggerSchema.response = { ...options.response };
|
|
25
28
|
Object.entries(options.response).forEach(([statusCode, value]) => {
|
|
26
29
|
if (!isZodSchema(value)) {
|
|
27
30
|
return;
|
|
28
31
|
}
|
|
29
32
|
response[statusCode] = value;
|
|
30
|
-
|
|
33
|
+
swaggerSchema.response[statusCode] = toJsonSchema(value, 'output');
|
|
34
|
+
delete schema.response[statusCode];
|
|
31
35
|
});
|
|
36
|
+
if (!Object.keys(schema.response).length) {
|
|
37
|
+
delete schema.response;
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
40
|
const hasZodSchemas = Object.keys(request).length > 0 || Object.keys(response).length > 0;
|
|
34
41
|
return {
|
|
35
42
|
schema,
|
|
43
|
+
swaggerSchema,
|
|
36
44
|
zod: hasZodSchemas ? { request, response } : null
|
|
37
45
|
};
|
|
38
46
|
}
|
|
@@ -45,10 +53,10 @@ export async function parseRequestWithZod(req, schemas) {
|
|
|
45
53
|
req.headers = await parseSchema(requestSchemas.headers, req.headers);
|
|
46
54
|
}
|
|
47
55
|
if (requestSchemas.params) {
|
|
48
|
-
req.params = await parseSchema(requestSchemas.params,
|
|
56
|
+
req.params = await parseSchema(requestSchemas.params, req.params);
|
|
49
57
|
}
|
|
50
58
|
if (requestSchemas.querystring) {
|
|
51
|
-
req.query = await parseSchema(requestSchemas.querystring,
|
|
59
|
+
req.query = await parseSchema(requestSchemas.querystring, req.query);
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
export async function parseResponseWithZod(reply, payload, schemas) {
|
|
@@ -56,7 +64,8 @@ export async function parseResponseWithZod(reply, payload, schemas) {
|
|
|
56
64
|
if (!schema) {
|
|
57
65
|
return payload;
|
|
58
66
|
}
|
|
59
|
-
|
|
67
|
+
const result = await schema.safeParseAsync(payload);
|
|
68
|
+
return result.success ? result.data : payload;
|
|
60
69
|
}
|
|
61
70
|
function getResponseSchema(statusCode, schemas) {
|
|
62
71
|
return (schemas[statusCode] ||
|
|
@@ -85,58 +94,6 @@ function toJsonSchema(schema, io) {
|
|
|
85
94
|
delete jsonSchema.$schema;
|
|
86
95
|
return jsonSchema;
|
|
87
96
|
}
|
|
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
97
|
function isZodSchema(value) {
|
|
141
98
|
return (Is.Defined(value) &&
|
|
142
99
|
Is.Function(value.parse) &&
|
|
@@ -138,6 +138,7 @@ export declare class ServerImpl extends Macroable {
|
|
|
138
138
|
options(options: Omit<RouteJson, 'methods'>): void;
|
|
139
139
|
private toRouteHooks;
|
|
140
140
|
private getFastifyOptionsWithOpenApiSchema;
|
|
141
|
+
private configureSwaggerTransform;
|
|
141
142
|
private getOpenApiRouteSchema;
|
|
142
143
|
private getOpenApiPathCandidates;
|
|
143
144
|
private normalizePath;
|
package/src/server/ServerImpl.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* file that was distributed with this source code.
|
|
8
8
|
*/
|
|
9
9
|
import fastify from 'fastify';
|
|
10
|
-
import {
|
|
10
|
+
import { parseRequestWithZod, normalizeRouteSchema } from '#src/router/RouteSchema';
|
|
11
11
|
import { Options, Macroable, Is } from '@athenna/common';
|
|
12
12
|
import { FastifyHandler } from '#src/handlers/FastifyHandler';
|
|
13
13
|
export class ServerImpl extends Macroable {
|
|
@@ -196,7 +196,6 @@ export class ServerImpl extends Macroable {
|
|
|
196
196
|
onSend: [],
|
|
197
197
|
preValidation: [],
|
|
198
198
|
preHandler: [],
|
|
199
|
-
preSerialization: [],
|
|
200
199
|
onResponse: [],
|
|
201
200
|
url: options.url,
|
|
202
201
|
method: options.methods,
|
|
@@ -213,9 +212,6 @@ export class ServerImpl extends Macroable {
|
|
|
213
212
|
}
|
|
214
213
|
if (zodSchemas) {
|
|
215
214
|
route.preValidation = [async (req) => parseRequestWithZod(req, zodSchemas)];
|
|
216
|
-
route.preSerialization = [
|
|
217
|
-
async (_, reply, payload) => parseResponseWithZod(reply, payload, zodSchemas)
|
|
218
|
-
];
|
|
219
215
|
}
|
|
220
216
|
if (options.data && Is.Array(route.preHandler)) {
|
|
221
217
|
route.preHandler?.unshift((req, _, done) => {
|
|
@@ -231,10 +227,6 @@ export class ServerImpl extends Macroable {
|
|
|
231
227
|
...this.toRouteHooks(route.preValidation),
|
|
232
228
|
...this.toRouteHooks(fastifyOptions.preValidation)
|
|
233
229
|
];
|
|
234
|
-
fastifyOptions.preSerialization = [
|
|
235
|
-
...this.toRouteHooks(route.preSerialization),
|
|
236
|
-
...this.toRouteHooks(fastifyOptions.preSerialization)
|
|
237
|
-
];
|
|
238
230
|
}
|
|
239
231
|
this.fastify.route({ ...route, ...fastifyOptions });
|
|
240
232
|
}
|
|
@@ -290,13 +282,21 @@ export class ServerImpl extends Macroable {
|
|
|
290
282
|
const automaticSchema = this.getOpenApiRouteSchema(options);
|
|
291
283
|
const fastifyOptions = { ...options.fastify };
|
|
292
284
|
if (!automaticSchema) {
|
|
285
|
+
this.configureSwaggerTransform(fastifyOptions);
|
|
293
286
|
return fastifyOptions;
|
|
294
287
|
}
|
|
295
288
|
const normalizedSchema = normalizeRouteSchema(automaticSchema);
|
|
296
289
|
const currentConfig = { ...(fastifyOptions.config || {}) };
|
|
290
|
+
const currentSwaggerSchema =
|
|
291
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
292
|
+
// @ts-ignore
|
|
293
|
+
currentConfig.swaggerSchema || fastifyOptions.schema;
|
|
297
294
|
fastifyOptions.schema = this.mergeFastifySchemas(normalizedSchema.schema, fastifyOptions.schema);
|
|
298
295
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
299
296
|
// @ts-ignore
|
|
297
|
+
currentConfig.swaggerSchema = this.mergeFastifySchemas(normalizedSchema.swaggerSchema, currentSwaggerSchema);
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
299
|
+
// @ts-ignore
|
|
300
300
|
const currentZod = currentConfig.zod;
|
|
301
301
|
const mergedZod = this.mergeZodSchemas(normalizedSchema.zod, currentZod);
|
|
302
302
|
if (mergedZod) {
|
|
@@ -305,8 +305,31 @@ export class ServerImpl extends Macroable {
|
|
|
305
305
|
currentConfig.zod = mergedZod;
|
|
306
306
|
}
|
|
307
307
|
fastifyOptions.config = currentConfig;
|
|
308
|
+
this.configureSwaggerTransform(fastifyOptions);
|
|
308
309
|
return fastifyOptions;
|
|
309
310
|
}
|
|
311
|
+
configureSwaggerTransform(fastifyOptions) {
|
|
312
|
+
const config = fastifyOptions?.config;
|
|
313
|
+
if (!config?.swaggerSchema) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const customTransform = config.swaggerTransform;
|
|
317
|
+
if (customTransform === false) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
config.swaggerTransform = (args) => {
|
|
321
|
+
const transformed = Is.Function(customTransform)
|
|
322
|
+
? customTransform(args)
|
|
323
|
+
: args;
|
|
324
|
+
if (transformed === false) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
...transformed,
|
|
329
|
+
schema: this.mergeFastifySchemas(transformed?.schema || args.schema, config.swaggerSchema)
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
}
|
|
310
333
|
getOpenApiRouteSchema(options) {
|
|
311
334
|
const paths = Config.get('openapi.paths', {});
|
|
312
335
|
const methods = options.methods || [];
|