Skip to content

Commit dcee41c

Browse files
committed
fix(schema): save zod parsed values in request ctx
1 parent a3848bd commit dcee41c

9 files changed

Lines changed: 172 additions & 16 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.54.0",
3+
"version": "5.55.0",
44
"description": "The Athenna Http server. Built on top of fastify.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",

src/context/Request.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export class Request extends Macroable {
208208
* ```
209209
*/
210210
public get body(): any | any[] {
211-
return this.request.body || {}
211+
return this.request.zodParsed?.body || this.request.body || {}
212212
}
213213

214214
/**
@@ -220,7 +220,7 @@ export class Request extends Macroable {
220220
* ```
221221
*/
222222
public get params(): any {
223-
return this.request.params || {}
223+
return this.request.zodParsed?.params || this.request.params || {}
224224
}
225225

226226
/**
@@ -232,7 +232,7 @@ export class Request extends Macroable {
232232
* ```
233233
*/
234234
public get queries(): any {
235-
return this.request.query || {}
235+
return this.request.zodParsed?.query || this.request.query || {}
236236
}
237237

238238
/**
@@ -244,7 +244,7 @@ export class Request extends Macroable {
244244
* ```
245245
*/
246246
public get headers(): any {
247-
return this.request.headers || {}
247+
return this.request.zodParsed?.headers || this.request.headers || {}
248248
}
249249

250250
/**

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
declare module 'fastify' {
1111
interface FastifyRequest {
1212
data: any
13+
zodParsed?: {
14+
body?: any
15+
headers?: any
16+
params?: any
17+
query?: any
18+
}
1319
}
1420

1521
interface FastifyReply {

src/providers/HttpServerProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export class HttpServerProvider extends ServiceProvider {
1515
public register() {
1616
const fastifyOptions = Config.get<FastifyServerOptions>('http.fastify')
1717

18-
this.container.instance('Athenna/Core/HttpServer', new ServerImpl(fastifyOptions))
18+
this.container.instance(
19+
'Athenna/Core/HttpServer',
20+
new ServerImpl(fastifyOptions)
21+
)
1922
}
2023

2124
public async shutdown() {

src/router/RouteSchema.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,38 @@ export async function parseRequestWithZod(
8787
schemas: RouteZodSchemas
8888
) {
8989
const requestSchemas = schemas.request
90+
const parsed = req.zodParsed || {}
9091

9192
if (requestSchemas.body) {
92-
req.body = await parseSchema(requestSchemas.body, req.body)
93+
const body = await parseSchema(requestSchemas.body, req.body)
94+
95+
req.body = body
96+
parsed.body = body
9397
}
9498

9599
if (requestSchemas.headers) {
96-
req.headers = await parseSchema(requestSchemas.headers, req.headers)
100+
const headers = await parseSchema(requestSchemas.headers, req.headers)
101+
102+
req.headers = headers
103+
parsed.headers = headers
97104
}
98105

99106
if (requestSchemas.params) {
100-
req.params = await parseSchema(requestSchemas.params, req.params)
107+
const params = await parseSchema(requestSchemas.params, req.params)
108+
109+
req.params = params
110+
parsed.params = params
101111
}
102112

103113
if (requestSchemas.querystring) {
104-
req.query = await parseSchema(requestSchemas.querystring, req.query)
114+
const query = await parseSchema(requestSchemas.querystring, req.query)
115+
116+
req.query = query
117+
parsed.query = query
118+
}
119+
120+
if (Object.keys(parsed).length) {
121+
req.zodParsed = parsed
105122
}
106123
}
107124

src/server/ServerImpl.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class ServerImpl extends Macroable {
5959

6060
this.fastify.decorateReply('body', null)
6161
this.fastify.decorateRequest('data', null)
62+
this.fastify.decorateRequest('zodParsed', null)
6263
}
6364

6465
/**
@@ -310,7 +311,10 @@ export class ServerImpl extends Macroable {
310311
}
311312

312313
if (zodSchemas) {
313-
route.preValidation = [async req => parseRequestWithZod(req, zodSchemas)]
314+
route.preHandler = [
315+
async req => parseRequestWithZod(req, zodSchemas),
316+
...this.toRouteHooks(route.preHandler)
317+
]
314318
}
315319

316320
if (options.data && Is.Array(route.preHandler)) {
@@ -325,9 +329,9 @@ export class ServerImpl extends Macroable {
325329
}
326330

327331
if (zodSchemas) {
328-
fastifyOptions.preValidation = [
329-
...this.toRouteHooks(route.preValidation),
330-
...this.toRouteHooks(fastifyOptions.preValidation)
332+
fastifyOptions.preHandler = [
333+
...this.toRouteHooks(route.preHandler),
334+
...this.toRouteHooks(fastifyOptions.preHandler)
331335
]
332336
}
333337

tests/unit/context/RequestTest.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,29 @@ export default class RequestTest {
125125
})
126126
}
127127

128+
@Test()
129+
public async shouldPreferZodParsedRequestValuesWhenAvailable({ assert }: Context) {
130+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
131+
// @ts-ignore
132+
this.request.zodParsed = {
133+
body: { enabled: true },
134+
headers: { 'x-enabled': false },
135+
params: { id: 1 },
136+
query: { enabled: true }
137+
}
138+
139+
const ctx = { request: new Request(this.request) }
140+
141+
assert.deepEqual(ctx.request.body, { enabled: true })
142+
assert.deepEqual(ctx.request.headers, { 'x-enabled': false })
143+
assert.deepEqual(ctx.request.params, { id: 1 })
144+
assert.deepEqual(ctx.request.queries, { enabled: true })
145+
assert.isTrue(ctx.request.input('enabled'))
146+
assert.isFalse(ctx.request.header('x-enabled'))
147+
assert.equal(ctx.request.param('id'), 1)
148+
assert.isTrue(ctx.request.query('enabled'))
149+
}
150+
128151
@Test()
129152
public async shouldBeAbleToGetTheServerPortFromRequest({ assert }: Context) {
130153
await this.server.listen({ port: 9999 })

tests/unit/router/RouteTest.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,60 @@ export default class RouteTest {
230230
assert.deepEqual(response.json(), { id: 10, limit: 2 })
231231
}
232232

233+
@Test()
234+
public async shouldExposeParsedZodValuesThroughAllRequestAccessors({ assert }: Context) {
235+
Route.post('users/:published', async ctx => {
236+
await ctx.response.send({
237+
body: ctx.request.input('syncProfile'),
238+
bodyFromGetter: ctx.request.body.syncProfile,
239+
header: ctx.request.header('x-with-profile'),
240+
headerFromGetter: ctx.request.headers['x-with-profile'],
241+
param: ctx.request.param('published'),
242+
paramFromGetter: ctx.request.params.published,
243+
query: ctx.request.query('withProfile'),
244+
queryFromGetter: ctx.request.queries.withProfile
245+
})
246+
}).schema({
247+
body: z.object({ syncProfile: z.stringbool() }),
248+
headers: z.object({ 'x-with-profile': z.stringbool() }),
249+
params: z.object({ published: z.stringbool() }),
250+
querystring: z.object({ withProfile: z.stringbool() }),
251+
response: {
252+
200: z.object({
253+
body: z.boolean(),
254+
bodyFromGetter: z.boolean(),
255+
header: z.boolean(),
256+
headerFromGetter: z.boolean(),
257+
param: z.boolean(),
258+
paramFromGetter: z.boolean(),
259+
query: z.boolean(),
260+
queryFromGetter: z.boolean()
261+
})
262+
}
263+
})
264+
265+
Route.register()
266+
267+
const response = await Server.request({
268+
path: '/users/true?withProfile=true',
269+
method: 'post',
270+
headers: { 'x-with-profile': 'false' },
271+
payload: { syncProfile: 'true' }
272+
})
273+
274+
assert.equal(response.statusCode, 200)
275+
assert.deepEqual(response.json(), {
276+
body: true,
277+
bodyFromGetter: true,
278+
header: false,
279+
headerFromGetter: false,
280+
param: true,
281+
paramFromGetter: true,
282+
query: true,
283+
queryFromGetter: true
284+
})
285+
}
286+
233287
@Test()
234288
@Cleanup(() => Config.set('openapi.paths', {}))
235289
public async shouldAutomaticallyApplySchemasFromOpenApiConfig({ assert }: Context) {
@@ -266,6 +320,55 @@ export default class RouteTest {
266320
assert.deepEqual(response.json(), { id: 10, limit: 2 })
267321
}
268322

323+
@Test()
324+
@Cleanup(() => Config.set('openapi.paths', {}))
325+
public async shouldAutomaticallyExposeParsedOpenApiZodValuesInRequestAccessors({ assert }: Context) {
326+
Config.set('openapi.paths', {
327+
'/users/{published}': {
328+
post: {
329+
body: z.object({ syncProfile: z.stringbool() }),
330+
headers: z.object({ 'x-with-profile': z.stringbool() }),
331+
params: z.object({ published: z.stringbool() }),
332+
querystring: z.object({ withProfile: z.stringbool() }),
333+
response: {
334+
200: z.object({
335+
body: z.boolean(),
336+
header: z.boolean(),
337+
param: z.boolean(),
338+
query: z.boolean()
339+
})
340+
}
341+
}
342+
}
343+
})
344+
345+
Route.post('users/:published', async ctx => {
346+
await ctx.response.send({
347+
body: ctx.request.input('syncProfile'),
348+
header: ctx.request.header('x-with-profile'),
349+
param: ctx.request.param('published'),
350+
query: ctx.request.query('withProfile')
351+
})
352+
})
353+
354+
Route.register()
355+
356+
const response = await Server.request({
357+
path: '/users/true?withProfile=false',
358+
method: 'post',
359+
headers: { 'x-with-profile': 'true' },
360+
payload: { syncProfile: 'false' }
361+
})
362+
363+
assert.equal(response.statusCode, 200)
364+
assert.deepEqual(response.json(), {
365+
body: false,
366+
header: true,
367+
param: true,
368+
query: false
369+
})
370+
}
371+
269372
@Test()
270373
public async shouldBeAbleToHideARouteFromTheSwaggerDocumentation({ assert }: Context) {
271374
Route.get('test', new HelloController().index)

0 commit comments

Comments
 (0)