Typed request responce

PHOTO EMBED

Thu Mar 30 2023 17:06:41 GMT+0000 (Coordinated Universal Time)

Saved by @dio_dev #typescript

/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response } from 'express'
import createHttpError from 'http-errors'
import { z, ZodError, ZodIssue } from 'zod'

export interface CreateHandler<
  Params extends z.ZodTypeAny = any,
  Body extends z.ZodTypeAny = any,
  Query extends z.ZodTypeAny = any,
  ResJson extends z.ZodTypeAny = any,
  OmittedFields extends 'params' | 'body' | 'query' | '' = '',
> {
  (): {
    resolve(
      f: (
        req: Request<z.input<Params>, any, z.input<Body>>,
        res: Response<ResJson>,
      ) => any,
    ): (req: Request, res: Response) => void
    inputParams<T extends z.ZodTypeAny>(
      paramsSchema: T,
    ): Omit<
      ReturnType<
        CreateHandler<T, Body, Query, ResJson, OmittedFields | 'params'>
      >,
      OmittedFields | 'params'
    >
    inputBody<T extends z.ZodTypeAny>(
      bodySchema: T,
    ): Omit<
      ReturnType<
        CreateHandler<Params, T, Query, ResJson, OmittedFields | 'body'>
      >,
      OmittedFields | 'body'
    >
    inputQuery<T extends z.ZodTypeAny>(
      querySchema: T,
    ): Omit<
      ReturnType<
        CreateHandler<Params, Body, T, ResJson, OmittedFields | 'body'>
      >,
      OmittedFields | 'query'
    >
    responseJson<T extends z.ZodTypeAny>(
      resJsonSchema: T,
    ): Omit<
      ReturnType<
        CreateHandler<Params, Body, Query, z.input<T>, OmittedFields | 'body'>
      >,
      OmittedFields | 'query'
    >
  }
}

type HandlerThis = {
  paramsSchema: z.ZodAny
  bodySchema: z.ZodAny
  querySchema: z.ZodAny
  resJsonSchema: z.ZodAny
}

const formatZodError = {
  all: (prefix: string, zodError: ZodError) =>
    Object.values(
      zodError.flatten(
        (issue: ZodIssue) => `[${prefix}${issue.path}]: ${issue.message}`,
      ).fieldErrors,
    ).join(', '),
  first: (prefix: string, zodError: ZodError) =>
    Object.values(
      zodError.flatten(
        (issue: ZodIssue) => `[${prefix}${issue.path}]: ${issue.message}`,
      ).fieldErrors,
    )[0],
}

export const createHandler: CreateHandler = function () {
  return {
    inputParams(paramsSchema) {
      ;(<any>this).paramsSchema = paramsSchema
      return this
    },
    inputBody(bodySchema) {
      ;(<any>this).bodySchema = bodySchema
      return this
    },
    inputQuery(querySchema) {
      ;(<any>this).querySchema = querySchema
      return this
    },
    responseJson(resJsonSchema) {
      ;(<any>this).resJsonSchema = resJsonSchema
      return this
    },
    resolve(this: HandlerThis, f) {
      return async (req, res) => {
        if (this.paramsSchema) {
          const paramsValidationResult = this.paramsSchema.safeParse(req.params)
          if (!paramsValidationResult.success) {
            return res
              .status(400)
              .json(formatZodError.all('param.', paramsValidationResult.error))
          }

          req.params = paramsValidationResult.data
        }

        if (this.bodySchema) {
          const bodyValidationResult = this.bodySchema.safeParse(req.body)
          if (!bodyValidationResult.success) {
            return res
              .status(400)
              .json(formatZodError.all('body.', bodyValidationResult.error))
          }

          req.body = bodyValidationResult.data
        }

        if (this.querySchema) {
          const queryValidationResult = this.querySchema.safeParse(req.query)
          if (!queryValidationResult.success) {
            return res
              .status(400)
              .json(formatZodError.all('query.', queryValidationResult.error))
          }

          req.query = queryValidationResult.data
        }

        // Because express is very old and can't handle async exceptions, let's do it from this helper
        const resJson: any = res.json
        try {
          if (this.resJsonSchema) {
            const newRes: any = Object.assign(res, { json: undefined })
            newRes.json = (json: any) => {
              const resJsonValidationResult = this.resJsonSchema.safeParse(json)
              if (!resJsonValidationResult.success) {
                res.status(400)
                resJson.call(
                  res,
                  formatZodError.all('resJson.', resJsonValidationResult.error),
                )
                return
              }
              resJson.call(res, json)
            }
            await f(req, newRes as any)
            return
          }

          await f(req, res)
          return
        } catch (error) {
          if (createHttpError.isHttpError(error)) {
            res.status(error.status)
            resJson.call(res, { message: error.message })
          }
          return
        }
      }
    },
  }
}
content_copyCOPY