All files / src/middleware validation.middleware.ts

100% Statements 77/77
85.71% Branches 12/14
100% Functions 6/6
100% Lines 61/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 622x 2x 2x 2x 2x 288x 288x 2x 2x 2x 2x 2x 2x 2x 3674x 1217x 119x 119x 1217x 115x 115x 115x 115x 115x 115x 119x 1071x 1071x 7x 7x 4x 2x 1071x 2x 2x 2x 2x 2x 2x 2x 1071x 2x 1071x 2x 146x 2x 2x 2x 2x 174x 176x 176x 345x 2x 2x 145x 2x 2x 2x 2x 2x  
// src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { ParamsDictionary, Query } from 'express-serve-static-core';
import { ZodObject, ZodError, z } from 'zod';
import { ValidationError } from '../services/db/errors.db';
 
/**
 * A middleware factory that generates a validation middleware for a given Zod schema.
 * It validates the request's params, query, and body against the schema.
 *
 * @param schema - The Zod schema to validate against.
 * @returns An Express middleware function.
 */
export const validateRequest =
  (schema: ZodObject<z.ZodRawShape>) => async (req: Request, res: Response, next: NextFunction) => {
    try {
      // Parse and validate the request parts against the schema.
      // This will throw a ZodError if validation fails.
      const { params, query, body } = await schema.parseAsync({
        params: req.params,
        query: req.query,
        body: req.body,
      });
 
      // On success, merge the parsed (and coerced) data back into the request objects.
      // For req.params, we can delete existing keys and assign new ones.
      Object.keys(req.params).forEach((key) => delete (req.params as ParamsDictionary)[key]);
      Object.assign(req.params, params);
 
      // For req.query in Express 5, the query object is lazily evaluated from the URL
      // and cannot be mutated directly. We use Object.defineProperty to replace
      // the getter with our validated/transformed query object.
      Object.defineProperty(req, 'query', {
        value: query as Query,
        writable: true,
        configurable: true,
        enumerable: true,
      });
 
      // For body, direct reassignment works.
      req.body = body;
 
      return next();
    } catch (error) {
      if (error instanceof ZodError) {
        // If it's a Zod validation error, wrap it in our custom ValidationError.
        // The global errorHandler will format this into a 400 response.
        // We must map Zod's issues to our custom ValidationIssue format, as Zod's
        // `path` can include `symbol`, which our type does not allow.
        const validationIssues = error.issues.map((issue) => ({
          ...issue,
          // Convert any symbols in the path to strings to match our ValidationIssue type.
          path: issue.path.map((p) => String(p)),
        }));
 
        return next(new ValidationError(validationIssues));
      }
      // For any other unexpected errors, pass them on.
      return next(error);
    }
  };