@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@athenna/http",
3
- "version": "5.47.0",
3
+ "version": "5.49.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>",
@@ -113,6 +113,7 @@ export declare class Response extends Macroable {
113
113
  * ```
114
114
  */
115
115
  send(data?: any): Promise<Response>;
116
+ private getRouteZodSchemas;
116
117
  /**
117
118
  * @example
118
119
  * ```ts
@@ -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 });
@@ -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
- schema.response[statusCode] = toJsonSchema(value, 'output');
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, coerceDataForValidation(requestSchemas.params, req.params));
56
+ req.params = await parseSchema(requestSchemas.params, req.params);
49
57
  }
50
58
  if (requestSchemas.querystring) {
51
- req.query = await parseSchema(requestSchemas.querystring, coerceDataForValidation(requestSchemas.querystring, req.query));
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
- return parseSchema(schema, payload);
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;
@@ -7,7 +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
+ 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 || [];