diff --git a/package-lock.json b/package-lock.json index ff69bd2..168c824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/http", - "version": "5.54.0", + "version": "5.55.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/http", - "version": "5.54.0", + "version": "5.55.0", "license": "MIT", "devDependencies": { "@athenna/artisan": "^5.11.0", diff --git a/package.json b/package.json index ec44440..fb6dd5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/http", - "version": "5.54.0", + "version": "5.55.0", "description": "The Athenna Http server. Built on top of fastify.", "license": "MIT", "author": "João Lenon ", diff --git a/src/context/Request.ts b/src/context/Request.ts index 811b406..eef91bb 100644 --- a/src/context/Request.ts +++ b/src/context/Request.ts @@ -208,7 +208,7 @@ export class Request extends Macroable { * ``` */ public get body(): any | any[] { - return this.request.body || {} + return this.request.zodParsed?.body || this.request.body || {} } /** @@ -220,7 +220,7 @@ export class Request extends Macroable { * ``` */ public get params(): any { - return this.request.params || {} + return this.request.zodParsed?.params || this.request.params || {} } /** @@ -232,7 +232,7 @@ export class Request extends Macroable { * ``` */ public get queries(): any { - return this.request.query || {} + return this.request.zodParsed?.query || this.request.query || {} } /** @@ -244,7 +244,7 @@ export class Request extends Macroable { * ``` */ public get headers(): any { - return this.request.headers || {} + return this.request.zodParsed?.headers || this.request.headers || {} } /** diff --git a/src/index.ts b/src/index.ts index 5f3e766..88bfc5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,12 @@ declare module 'fastify' { interface FastifyRequest { data: any + zodParsed?: { + body?: any + headers?: any + params?: any + query?: any + } } interface FastifyReply { diff --git a/src/providers/HttpServerProvider.ts b/src/providers/HttpServerProvider.ts index d374a31..565d9a5 100644 --- a/src/providers/HttpServerProvider.ts +++ b/src/providers/HttpServerProvider.ts @@ -15,7 +15,10 @@ export class HttpServerProvider extends ServiceProvider { public register() { const fastifyOptions = Config.get('http.fastify') - this.container.instance('Athenna/Core/HttpServer', new ServerImpl(fastifyOptions)) + this.container.instance( + 'Athenna/Core/HttpServer', + new ServerImpl(fastifyOptions) + ) } public async shutdown() { diff --git a/src/router/RouteSchema.ts b/src/router/RouteSchema.ts index 3d4349d..fa227cd 100644 --- a/src/router/RouteSchema.ts +++ b/src/router/RouteSchema.ts @@ -87,21 +87,38 @@ export async function parseRequestWithZod( schemas: RouteZodSchemas ) { const requestSchemas = schemas.request + const parsed = req.zodParsed || {} if (requestSchemas.body) { - req.body = await parseSchema(requestSchemas.body, req.body) + const body = await parseSchema(requestSchemas.body, req.body) + + req.body = body + parsed.body = body } if (requestSchemas.headers) { - req.headers = await parseSchema(requestSchemas.headers, req.headers) + const headers = await parseSchema(requestSchemas.headers, req.headers) + + req.headers = headers + parsed.headers = headers } if (requestSchemas.params) { - req.params = await parseSchema(requestSchemas.params, req.params) + const params = await parseSchema(requestSchemas.params, req.params) + + req.params = params + parsed.params = params } if (requestSchemas.querystring) { - req.query = await parseSchema(requestSchemas.querystring, req.query) + const query = await parseSchema(requestSchemas.querystring, req.query) + + req.query = query + parsed.query = query + } + + if (Object.keys(parsed).length) { + req.zodParsed = parsed } } diff --git a/src/server/ServerImpl.ts b/src/server/ServerImpl.ts index 8c1da3f..5a16656 100644 --- a/src/server/ServerImpl.ts +++ b/src/server/ServerImpl.ts @@ -59,6 +59,7 @@ export class ServerImpl extends Macroable { this.fastify.decorateReply('body', null) this.fastify.decorateRequest('data', null) + this.fastify.decorateRequest('zodParsed', null) } /** @@ -310,7 +311,10 @@ export class ServerImpl extends Macroable { } if (zodSchemas) { - route.preValidation = [async req => parseRequestWithZod(req, zodSchemas)] + route.preHandler = [ + async req => parseRequestWithZod(req, zodSchemas), + ...this.toRouteHooks(route.preHandler) + ] } if (options.data && Is.Array(route.preHandler)) { @@ -325,9 +329,9 @@ export class ServerImpl extends Macroable { } if (zodSchemas) { - fastifyOptions.preValidation = [ - ...this.toRouteHooks(route.preValidation), - ...this.toRouteHooks(fastifyOptions.preValidation) + fastifyOptions.preHandler = [ + ...this.toRouteHooks(route.preHandler), + ...this.toRouteHooks(fastifyOptions.preHandler) ] } diff --git a/tests/unit/context/RequestTest.ts b/tests/unit/context/RequestTest.ts index 5c65eee..4623da1 100644 --- a/tests/unit/context/RequestTest.ts +++ b/tests/unit/context/RequestTest.ts @@ -125,6 +125,29 @@ export default class RequestTest { }) } + @Test() + public async shouldPreferZodParsedRequestValuesWhenAvailable({ assert }: Context) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.request.zodParsed = { + body: { enabled: true }, + headers: { 'x-enabled': false }, + params: { id: 1 }, + query: { enabled: true } + } + + const ctx = { request: new Request(this.request) } + + assert.deepEqual(ctx.request.body, { enabled: true }) + assert.deepEqual(ctx.request.headers, { 'x-enabled': false }) + assert.deepEqual(ctx.request.params, { id: 1 }) + assert.deepEqual(ctx.request.queries, { enabled: true }) + assert.isTrue(ctx.request.input('enabled')) + assert.isFalse(ctx.request.header('x-enabled')) + assert.equal(ctx.request.param('id'), 1) + assert.isTrue(ctx.request.query('enabled')) + } + @Test() public async shouldBeAbleToGetTheServerPortFromRequest({ assert }: Context) { await this.server.listen({ port: 9999 }) diff --git a/tests/unit/router/RouteTest.ts b/tests/unit/router/RouteTest.ts index 16e1413..914e3df 100644 --- a/tests/unit/router/RouteTest.ts +++ b/tests/unit/router/RouteTest.ts @@ -230,6 +230,60 @@ export default class RouteTest { assert.deepEqual(response.json(), { id: 10, limit: 2 }) } + @Test() + public async shouldExposeParsedZodValuesThroughAllRequestAccessors({ assert }: Context) { + Route.post('users/:published', async ctx => { + await ctx.response.send({ + body: ctx.request.input('syncProfile'), + bodyFromGetter: ctx.request.body.syncProfile, + header: ctx.request.header('x-with-profile'), + headerFromGetter: ctx.request.headers['x-with-profile'], + param: ctx.request.param('published'), + paramFromGetter: ctx.request.params.published, + query: ctx.request.query('withProfile'), + queryFromGetter: ctx.request.queries.withProfile + }) + }).schema({ + body: z.object({ syncProfile: z.stringbool() }), + headers: z.object({ 'x-with-profile': z.stringbool() }), + params: z.object({ published: z.stringbool() }), + querystring: z.object({ withProfile: z.stringbool() }), + response: { + 200: z.object({ + body: z.boolean(), + bodyFromGetter: z.boolean(), + header: z.boolean(), + headerFromGetter: z.boolean(), + param: z.boolean(), + paramFromGetter: z.boolean(), + query: z.boolean(), + queryFromGetter: z.boolean() + }) + } + }) + + Route.register() + + const response = await Server.request({ + path: '/users/true?withProfile=true', + method: 'post', + headers: { 'x-with-profile': 'false' }, + payload: { syncProfile: 'true' } + }) + + assert.equal(response.statusCode, 200) + assert.deepEqual(response.json(), { + body: true, + bodyFromGetter: true, + header: false, + headerFromGetter: false, + param: true, + paramFromGetter: true, + query: true, + queryFromGetter: true + }) + } + @Test() @Cleanup(() => Config.set('openapi.paths', {})) public async shouldAutomaticallyApplySchemasFromOpenApiConfig({ assert }: Context) { @@ -266,6 +320,55 @@ export default class RouteTest { assert.deepEqual(response.json(), { id: 10, limit: 2 }) } + @Test() + @Cleanup(() => Config.set('openapi.paths', {})) + public async shouldAutomaticallyExposeParsedOpenApiZodValuesInRequestAccessors({ assert }: Context) { + Config.set('openapi.paths', { + '/users/{published}': { + post: { + body: z.object({ syncProfile: z.stringbool() }), + headers: z.object({ 'x-with-profile': z.stringbool() }), + params: z.object({ published: z.stringbool() }), + querystring: z.object({ withProfile: z.stringbool() }), + response: { + 200: z.object({ + body: z.boolean(), + header: z.boolean(), + param: z.boolean(), + query: z.boolean() + }) + } + } + } + }) + + Route.post('users/:published', async ctx => { + await ctx.response.send({ + body: ctx.request.input('syncProfile'), + header: ctx.request.header('x-with-profile'), + param: ctx.request.param('published'), + query: ctx.request.query('withProfile') + }) + }) + + Route.register() + + const response = await Server.request({ + path: '/users/true?withProfile=false', + method: 'post', + headers: { 'x-with-profile': 'true' }, + payload: { syncProfile: 'false' } + }) + + assert.equal(response.statusCode, 200) + assert.deepEqual(response.json(), { + body: false, + header: true, + param: true, + query: false + }) + } + @Test() public async shouldBeAbleToHideARouteFromTheSwaggerDocumentation({ assert }: Context) { Route.get('test', new HelloController().index)