Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@athenna/http",
"version": "5.48.0",
"version": "5.49.0",
"description": "The Athenna Http server. Built on top of fastify.",
"license": "MIT",
"author": "João Lenon <lenon@athenna.io>",
Expand Down
14 changes: 0 additions & 14 deletions src/exceptions/ResponseValidationException.ts

This file was deleted.

11 changes: 10 additions & 1 deletion src/router/Route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,18 +346,27 @@ export class Route extends Macroable {
* ```
*/
public schema(options: RouteSchemaOptions): Route {
const { schema, zod } = normalizeRouteSchema(options)
const { schema, swaggerSchema, zod } = normalizeRouteSchema(options)

this.route.fastify.schema = schema

if (zod) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.route.fastify.config.zod = zod

if (Object.keys(zod.response).length) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.route.fastify.config.swaggerSchema = swaggerSchema
}
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete this.route.fastify.config.zod
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete this.route.fastify.config.swaggerSchema
}

return this
Expand Down
19 changes: 14 additions & 5 deletions src/router/RouteSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { ZodAny } from 'zod'
import { Is } from '@athenna/common'
import type { FastifyReply, FastifyRequest, FastifySchema } from 'fastify'
import { ZodValidationException } from '#src/exceptions/ZodValidationException'
import { ResponseValidationException } from '#src/exceptions/ResponseValidationException'

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

export function normalizeRouteSchema(options: RouteSchemaOptions): {
schema: FastifySchema
swaggerSchema: FastifySchema
zod: RouteZodSchemas | null
} {
const request: ZodRequestSchema = {}
const response: ZodResponseSchema = {}
const schema: FastifySchema = { ...options }
const swaggerSchema: FastifySchema = { ...options }

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

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

request[key] = options[key]
schema[key] = toJsonSchema(options[key], 'input')
swaggerSchema[key] = toJsonSchema(options[key], 'input')
})

if (options.response && Is.Object(options.response)) {
schema.response = { ...options.response }
swaggerSchema.response = { ...options.response }

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

response[statusCode] = value
schema.response[statusCode] = toJsonSchema(value, 'output')
swaggerSchema.response[statusCode] = toJsonSchema(value, 'output')
delete schema.response[statusCode]
})

if (!Object.keys(schema.response).length) {
delete schema.response
}
}

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

return {
schema,
swaggerSchema,
zod: hasZodSchemas ? { request, response } : null
}
}
Expand Down Expand Up @@ -107,9 +116,9 @@ export async function parseResponseWithZod(
return payload
}

return parseSchema(schema, payload).catch(error => {
throw new ResponseValidationException(error)
})
const result = await schema.safeParseAsync(payload)

return result.success ? result.data : payload
}

function getResponseSchema(
Expand Down
47 changes: 47 additions & 0 deletions src/server/ServerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,17 +396,31 @@ export class ServerImpl extends Macroable {
const fastifyOptions = { ...options.fastify }

if (!automaticSchema) {
this.configureSwaggerTransform(fastifyOptions)

return fastifyOptions
}

const normalizedSchema = normalizeRouteSchema(automaticSchema)
const currentConfig = { ...(fastifyOptions.config || {}) }

const currentSwaggerSchema =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentConfig.swaggerSchema || fastifyOptions.schema

fastifyOptions.schema = this.mergeFastifySchemas(
normalizedSchema.schema,
fastifyOptions.schema
)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentConfig.swaggerSchema = this.mergeFastifySchemas(
normalizedSchema.swaggerSchema,
currentSwaggerSchema
)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const currentZod = currentConfig.zod
Expand All @@ -419,10 +433,43 @@ export class ServerImpl extends Macroable {
}

fastifyOptions.config = currentConfig
this.configureSwaggerTransform(fastifyOptions)

return fastifyOptions
}

private configureSwaggerTransform(fastifyOptions: any) {
const config = fastifyOptions?.config

if (!config?.swaggerSchema) {
return
}

const customTransform = config.swaggerTransform

if (customTransform === false) {
return
}

config.swaggerTransform = (args: any) => {
const transformed = Is.Function(customTransform)
? customTransform(args)
: args

if (transformed === false) {
return false
}

return {
...transformed,
schema: this.mergeFastifySchemas(
transformed?.schema || args.schema,
config.swaggerSchema
)
}
}
}

private getOpenApiRouteSchema(options: RouteJson): RouteSchemaOptions {
const paths = Config.get('openapi.paths', {})
const methods = options.methods || []
Expand Down
12 changes: 5 additions & 7 deletions tests/unit/kernels/HttpKernelTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,7 @@ export default class HttpKernelTest {

@Test()
@Cleanup(() => Config.set('openapi.paths', {}))
public async shouldNotTriggerUnhandledErrorsWhenZodResponseValidationFailsWithGlobalInterceptors({
assert
}: Context) {
public async shouldIgnoreInvalidZodResponseSchemaWithGlobalInterceptors({ assert }: Context) {
let unhandledRejectionHappened = false
let uncaughtExceptionHappened = false

Expand Down Expand Up @@ -492,10 +490,10 @@ export default class HttpKernelTest {
process.removeListener('unhandledRejection', onUnhandledRejection)
process.removeListener('uncaughtException', onUncaughtException)

assert.equal(response.statusCode, 500)
assert.containSubset(response.json(), {
code: 'E_RESPONSE_VALIDATION_ERROR',
statusCode: 500
assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), {
hello: 'world',
intercepted: true
})
assert.isFalse(unhandledRejectionHappened)
assert.isFalse(uncaughtExceptionHappened)
Expand Down
21 changes: 6 additions & 15 deletions tests/unit/router/RouteResourceTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
* file that was distributed with this source code.
*/

import { z } from 'zod'
import { Config } from '@athenna/config'
import { MyValidator } from '#tests/fixtures/validators/MyValidator'
import { MyMiddleware } from '#tests/fixtures/middlewares/MyMiddleware'
import { MyTerminator } from '#tests/fixtures/middlewares/MyTerminator'
import { MyInterceptor } from '#tests/fixtures/middlewares/MyInterceptor'
import { Config } from '@athenna/config'
import { Route, Server, HttpRouteProvider, HttpServerProvider } from '#src'
import { Test, AfterEach, BeforeEach, type Context, Cleanup } from '@athenna/test'
import { HelloController } from '#tests/fixtures/controllers/HelloController'
import { Route, Server, HttpKernel, HttpRouteProvider, HttpServerProvider } from '#src'
import z from 'zod'

export default class RouteResourceTest {
@BeforeEach()
Expand Down Expand Up @@ -464,11 +464,7 @@ export default class RouteResourceTest {

@Test()
@Cleanup(() => Config.set('openapi.paths', {}))
public async shouldAutomaticallyThrowInternalServerExceptionWhenResponseSchemaIsInvalidInResources({
assert
}: Context) {
await new HttpKernel().registerExceptionHandler()

public async shouldIgnoreInvalidResponseSchemaInResources({ assert }: Context) {
Config.set('openapi.paths', {
'/test': {
get: {
Expand All @@ -482,19 +478,14 @@ export default class RouteResourceTest {
})

Route.resource('test', new HelloController()).only(['index'])

Route.register()

const response = await Server.request({
path: '/test',
method: 'get'
})

assert.equal(response.statusCode, 500)
assert.containSubset(response.json(), {
code: 'E_RESPONSE_VALIDATION_ERROR',
statusCode: 500
})
assert.isUndefined(response.json().details)
assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), { hello: 'world' })
}
}
Loading