Open jstorm31 opened 1 year ago
I believe this is the desired behaviour for validateRequest*
calls but not for processRequest*
calls. validateRequest*
does not modify the query, params and body whereas processRequest*
does.
Oh, I overlooked the processRequest*
functions. Thank you! 🙌
I believe the types still don't function correctly though. It appears that the types are using the schema input types rather than the schema output types.
Can confirm this is broken
with member default set
app.get(
"/endpoint",
processRequest({
query: z.object({
sort: z.enum(["NAME_ASC", "NAME_DSC"]).default("NAME_ASC"),
}),
}),
async (request, response) => {
const sort = request.query.sort; // "NAME_ASC" | "NAME_DSC" | undefined
}
);
without member default set
app.get(
"/endpoint",
processRequest({
query: z.object({
sort: z.enum(["NAME_ASC", "NAME_DSC"]),
}),
}),
async (request, response) => {
const sort = request.query.sort; // "NAME_ASC" | "NAME_DSC"
}
);
with a default on the wrapping object. now the entire object could be undefined...
app.get(
"/endpoint",
processRequest({
query: z.object({
sort: z.enum(["NAME_ASC", "NAME_DSC"]).default("NAME_ASC"),
}).default({}),
}),
async (request, response) => {
request.query // {...} | undefined
request.query.sort // error as request.query may be undefined
}
);
even if the default is explicitly stated... still could be undefined.
app.get(
"/endpoint",
processRequest({
query: z.object({
sort: z.enum(["NAME_ASC", "NAME_DSC"]).default("NAME_ASC"),
}).default({
sort: "NAME_ASC"
}),
}),
async (request, response) => {
request.query // {...} | undefined
request.query.sort // error as request.query may be undefined
}
);
I tested this again today, and processRequest
seems to work fine for me with version 1.4.0
// GET /test
router.get('/test', processRequest(
query: z.object({
limit: z.number().min(0).optional().default(10),
})
)),
(req, res) => {
req.query.limit // 10
}
In the library, if safeParse
call is successful, they mutate the req.query
object here
The issue is not the values, it's the inferred types which are incorrect.
Oh I see, you're right, the type is still number | undefined
. ☝️
I ended up writing my own middleware which addresses this issue. The usage is not the same, but regardless you may find it useful. I would submit a PR to fix this issue but it appears as though this project is unmaintained.
import { RequestHandler } from 'express';
import { ZodError, z } from 'zod';
const types = ['query', 'params', 'body'] as const;
/**
* A middleware generator that validates incoming requests against a set of schemas.
* @param schemas The schemas to validate against.
* @returns A middleware function that validates the request.
*/
export default function validate<TParams extends Validation = {}, TQuery extends Validation = {}, TBody extends Validation = {}>(
schemas: ValidationSchemas<TParams, TQuery, TBody>
): RequestHandler<ZodOutput<TParams>, any, ZodOutput<TBody>, ZodOutput<TQuery>> {
// Create validation objects for each type
const validation = {
params: z.object(schemas.params ?? {}).strict() as z.ZodObject<TParams>,
query: z.object(schemas.query ?? {}).strict() as z.ZodObject<TQuery>,
body: z.object(schemas.body ?? {}).strict() as z.ZodObject<TBody>
};
return (req, res, next) => {
const errors: Array<ErrorListItem> = [];
// Validate all types (params, query, body)
for (const type of types) {
const parsed = validation[type].safeParse(req[type]);
// @ts-expect-error This is fine
if (parsed.success) req[type] = parsed.data;
else errors.push({ type, errors: parsed.error });
}
// Return all errors if there are any
if (errors.length > 0) return res.status(400).send(errors.map(error => ({ type: error.type, errors: error.errors })));
return next();
};
}
/**
* The types of validation that can be performed.
*/
type DataType = (typeof types)[number];
/**
* An error item for a specific type.
*/
interface ErrorListItem {
type: DataType;
errors: ZodError<any>;
}
/**
* Generic validation type for a route (either params, query, or body).
*/
type Validation = Record<string, z.ZodTypeAny>;
/**
* The schemas provided to the validate middleware.
*/
interface ValidationSchemas<TParams extends Validation, TQuery extends Validation, TBody extends Validation> {
params?: TParams;
query?: TQuery;
body?: TBody;
}
/**
* The output type of a validation schema.
*/
type ZodOutput<T extends Validation> = z.ZodObject<T>['_output'];
A really basic usage example looks like this:
app.post('/test', validate({ body: { limit: z.number().min(0).optional().default(10) } }), async (req, res) => {
// req.body.limit -> number
});
I may end up making my own package for this tomorrow given I've used this in a couple projects now. Hope it helps!
I have a validation schema for the
limit
parameter, which has a default value:The middleware validates this parameter in the query, but it does not add default values to the parameters. Therefore,
req.query
still contains the original incoming object.This may be intentional, as the library is not intended for sanitization and default values.