Skip to content

Commit 5ed45fe

Browse files
authored
Merge pull request #236 from AthennaIO/develop
fix(openapi): just serialize response schemas
2 parents 8e25299 + 455900e commit 5ed45fe

8 files changed

Lines changed: 85 additions & 45 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@athenna/http",
3-
"version": "5.48.0",
3+
"version": "5.49.0",
44
"description": "The Athenna Http server. Built on top of fastify.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",

src/exceptions/ResponseValidationException.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/router/Route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,18 +346,27 @@ export class Route extends Macroable {
346346
* ```
347347
*/
348348
public schema(options: RouteSchemaOptions): Route {
349-
const { schema, zod } = normalizeRouteSchema(options)
349+
const { schema, swaggerSchema, zod } = normalizeRouteSchema(options)
350350

351351
this.route.fastify.schema = schema
352352

353353
if (zod) {
354354
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
355355
// @ts-ignore
356356
this.route.fastify.config.zod = zod
357+
358+
if (Object.keys(zod.response).length) {
359+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
360+
// @ts-ignore
361+
this.route.fastify.config.swaggerSchema = swaggerSchema
362+
}
357363
} else {
358364
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
359365
// @ts-ignore
360366
delete this.route.fastify.config.zod
367+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
368+
// @ts-ignore
369+
delete this.route.fastify.config.swaggerSchema
361370
}
362371

363372
return this

src/router/RouteSchema.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type { ZodAny } from 'zod'
1111
import { Is } from '@athenna/common'
1212
import type { FastifyReply, FastifyRequest, FastifySchema } from 'fastify'
1313
import { ZodValidationException } from '#src/exceptions/ZodValidationException'
14-
import { ResponseValidationException } from '#src/exceptions/ResponseValidationException'
1514

1615
type ZodRequestSchema = Partial<
1716
Record<'body' | 'headers' | 'params' | 'querystring', ZodAny>
@@ -34,11 +33,13 @@ export type RouteZodSchemas = {
3433

3534
export function normalizeRouteSchema(options: RouteSchemaOptions): {
3635
schema: FastifySchema
36+
swaggerSchema: FastifySchema
3737
zod: RouteZodSchemas | null
3838
} {
3939
const request: ZodRequestSchema = {}
4040
const response: ZodResponseSchema = {}
4141
const schema: FastifySchema = { ...options }
42+
const swaggerSchema: FastifySchema = { ...options }
4243

4344
const requestKeys = ['body', 'headers', 'params', 'querystring'] as const
4445

@@ -49,26 +50,34 @@ export function normalizeRouteSchema(options: RouteSchemaOptions): {
4950

5051
request[key] = options[key]
5152
schema[key] = toJsonSchema(options[key], 'input')
53+
swaggerSchema[key] = toJsonSchema(options[key], 'input')
5254
})
5355

5456
if (options.response && Is.Object(options.response)) {
5557
schema.response = { ...options.response }
58+
swaggerSchema.response = { ...options.response }
5659

5760
Object.entries(options.response).forEach(([statusCode, value]) => {
5861
if (!isZodSchema(value)) {
5962
return
6063
}
6164

6265
response[statusCode] = value
63-
schema.response[statusCode] = toJsonSchema(value, 'output')
66+
swaggerSchema.response[statusCode] = toJsonSchema(value, 'output')
67+
delete schema.response[statusCode]
6468
})
69+
70+
if (!Object.keys(schema.response).length) {
71+
delete schema.response
72+
}
6573
}
6674

6775
const hasZodSchemas =
6876
Object.keys(request).length > 0 || Object.keys(response).length > 0
6977

7078
return {
7179
schema,
80+
swaggerSchema,
7281
zod: hasZodSchemas ? { request, response } : null
7382
}
7483
}
@@ -107,9 +116,9 @@ export async function parseResponseWithZod(
107116
return payload
108117
}
109118

110-
return parseSchema(schema, payload).catch(error => {
111-
throw new ResponseValidationException(error)
112-
})
119+
const result = await schema.safeParseAsync(payload)
120+
121+
return result.success ? result.data : payload
113122
}
114123

115124
function getResponseSchema(

src/server/ServerImpl.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,17 +396,31 @@ export class ServerImpl extends Macroable {
396396
const fastifyOptions = { ...options.fastify }
397397

398398
if (!automaticSchema) {
399+
this.configureSwaggerTransform(fastifyOptions)
400+
399401
return fastifyOptions
400402
}
401403

402404
const normalizedSchema = normalizeRouteSchema(automaticSchema)
403405
const currentConfig = { ...(fastifyOptions.config || {}) }
406+
407+
const currentSwaggerSchema =
408+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
409+
// @ts-ignore
410+
currentConfig.swaggerSchema || fastifyOptions.schema
404411

405412
fastifyOptions.schema = this.mergeFastifySchemas(
406413
normalizedSchema.schema,
407414
fastifyOptions.schema
408415
)
409416

417+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
418+
// @ts-ignore
419+
currentConfig.swaggerSchema = this.mergeFastifySchemas(
420+
normalizedSchema.swaggerSchema,
421+
currentSwaggerSchema
422+
)
423+
410424
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
411425
// @ts-ignore
412426
const currentZod = currentConfig.zod
@@ -419,10 +433,43 @@ export class ServerImpl extends Macroable {
419433
}
420434

421435
fastifyOptions.config = currentConfig
436+
this.configureSwaggerTransform(fastifyOptions)
422437

423438
return fastifyOptions
424439
}
425440

441+
private configureSwaggerTransform(fastifyOptions: any) {
442+
const config = fastifyOptions?.config
443+
444+
if (!config?.swaggerSchema) {
445+
return
446+
}
447+
448+
const customTransform = config.swaggerTransform
449+
450+
if (customTransform === false) {
451+
return
452+
}
453+
454+
config.swaggerTransform = (args: any) => {
455+
const transformed = Is.Function(customTransform)
456+
? customTransform(args)
457+
: args
458+
459+
if (transformed === false) {
460+
return false
461+
}
462+
463+
return {
464+
...transformed,
465+
schema: this.mergeFastifySchemas(
466+
transformed?.schema || args.schema,
467+
config.swaggerSchema
468+
)
469+
}
470+
}
471+
}
472+
426473
private getOpenApiRouteSchema(options: RouteJson): RouteSchemaOptions {
427474
const paths = Config.get('openapi.paths', {})
428475
const methods = options.methods || []

tests/unit/kernels/HttpKernelTest.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,7 @@ export default class HttpKernelTest {
446446

447447
@Test()
448448
@Cleanup(() => Config.set('openapi.paths', {}))
449-
public async shouldNotTriggerUnhandledErrorsWhenZodResponseValidationFailsWithGlobalInterceptors({
450-
assert
451-
}: Context) {
449+
public async shouldIgnoreInvalidZodResponseSchemaWithGlobalInterceptors({ assert }: Context) {
452450
let unhandledRejectionHappened = false
453451
let uncaughtExceptionHappened = false
454452

@@ -492,10 +490,10 @@ export default class HttpKernelTest {
492490
process.removeListener('unhandledRejection', onUnhandledRejection)
493491
process.removeListener('uncaughtException', onUncaughtException)
494492

495-
assert.equal(response.statusCode, 500)
496-
assert.containSubset(response.json(), {
497-
code: 'E_RESPONSE_VALIDATION_ERROR',
498-
statusCode: 500
493+
assert.equal(response.statusCode, 200)
494+
assert.deepEqual(response.json(), {
495+
hello: 'world',
496+
intercepted: true
499497
})
500498
assert.isFalse(unhandledRejectionHappened)
501499
assert.isFalse(uncaughtExceptionHappened)

tests/unit/router/RouteResourceTest.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
* file that was distributed with this source code.
88
*/
99

10+
import { z } from 'zod'
11+
import { Config } from '@athenna/config'
1012
import { MyValidator } from '#tests/fixtures/validators/MyValidator'
1113
import { MyMiddleware } from '#tests/fixtures/middlewares/MyMiddleware'
1214
import { MyTerminator } from '#tests/fixtures/middlewares/MyTerminator'
1315
import { MyInterceptor } from '#tests/fixtures/middlewares/MyInterceptor'
14-
import { Config } from '@athenna/config'
16+
import { Route, Server, HttpRouteProvider, HttpServerProvider } from '#src'
1517
import { Test, AfterEach, BeforeEach, type Context, Cleanup } from '@athenna/test'
1618
import { HelloController } from '#tests/fixtures/controllers/HelloController'
17-
import { Route, Server, HttpKernel, HttpRouteProvider, HttpServerProvider } from '#src'
18-
import z from 'zod'
1919

2020
export default class RouteResourceTest {
2121
@BeforeEach()
@@ -464,11 +464,7 @@ export default class RouteResourceTest {
464464

465465
@Test()
466466
@Cleanup(() => Config.set('openapi.paths', {}))
467-
public async shouldAutomaticallyThrowInternalServerExceptionWhenResponseSchemaIsInvalidInResources({
468-
assert
469-
}: Context) {
470-
await new HttpKernel().registerExceptionHandler()
471-
467+
public async shouldIgnoreInvalidResponseSchemaInResources({ assert }: Context) {
472468
Config.set('openapi.paths', {
473469
'/test': {
474470
get: {
@@ -482,19 +478,14 @@ export default class RouteResourceTest {
482478
})
483479

484480
Route.resource('test', new HelloController()).only(['index'])
485-
486481
Route.register()
487482

488483
const response = await Server.request({
489484
path: '/test',
490485
method: 'get'
491486
})
492487

493-
assert.equal(response.statusCode, 500)
494-
assert.containSubset(response.json(), {
495-
code: 'E_RESPONSE_VALIDATION_ERROR',
496-
statusCode: 500
497-
})
498-
assert.isUndefined(response.json().details)
488+
assert.equal(response.statusCode, 200)
489+
assert.deepEqual(response.json(), { hello: 'world' })
499490
}
500491
}

0 commit comments

Comments
 (0)