Skip to content

Adding Typescript SupportΒ #16

@mastepanoski

Description

@mastepanoski

You could add support to typescript, rewriting withRest or adding compatibility through index.d.ts:

  import type { NextApiRequest, NextApiResponse } from 'next'
  import Boom from '@hapi/boom'
  import Joi from 'joi'
  
  const defaultLogError = (err: Boom.Boom) => {
    // Only log internal server errors
    if (!err.isServer) {
      return
    }
  
    // Log original error if passed
    if (err.data && err.data.originalError) {
      err = err.data.originalError
    }
  
    console.error(err.stack)
  }
  
  const defaultSendError = (res: NextApiResponse, err: Boom.Boom) => {
    const { output } = err
    const { headers, statusCode, payload } = output
  
    Object.entries(headers).forEach(([key, value]) =>
      res.setHeader(key, value || '')
    )
  
    res.status(statusCode).json(payload)
  }
  
  /**
   * Wraps a HTTP request handler with validation against Joi schemas.
   *
   * @param {object} schemas - An object with `query`, `body` or `headers` keys and their associated Joi schemas.
   *                           Each of these schemas will be matched against the incoming request.
   *
   * @returns {Function} The HTTP handler that validates the request.
   *
   * @example
   *
   * const getSchema = {
   *   query: Joi.object({
   *      id: Joi.string().required(),
   *   }),
   * };
   *
   * export default withRest({
   *   GET: withValidation(getSchema)(async req, res) => {
   *     // Do something with `req.query.id`
   *
   *     return { foo: 'bar' };
   *   },
   * });
   */
  export const withValidation =
    <T = any, R = any>(schemas: Joi.PartialSchemaMap<T> | undefined) =>
    (fn: (arg0: NextApiRequest, arg1: NextApiResponse<R>) => Promise<any>) =>
    async (req: NextApiRequest, res: NextApiResponse) => {
      const joiSchema = Joi.object(schemas).unknown(true)
  
      let validated: { [x: string]: any }
  
      try {
        validated = await joiSchema.validateAsync(req)
      } catch (err: any) {
        throw Boom.badRequest(err.message, { originalError: err as Error })
      }
  
      // Joi normalizes values, so we must copy things back to req
      ;['headers', 'body', 'query'].forEach((key: string) => {
        ;(req as any)[key] = validated[key]
      })
  
      return fn(req, res)
    }
  
  /**
   * @typedef {Function} SendError
   *
   * @param {object} res - Node.js response object.
   * @param {Error} err - The Boom error object.
   */
  /**
   * @typedef {Function} LogError
   *
   * @param {Error} err - The Boom error object.
   */
  
  /**
   * Matches handlers defined in `methods` against the HTTP method, like `GET` or `POST`.
   *
   * @param {object.<string, Function>} methods - An object mapping HTTP methods to their handlers.
   * @param {object} options - The options.
   * @param {SendError} options.sendError - A function responsible to send Boom errors back to the client.
   * @param {LogError} options.logError - A function that logs errors.
   *
   * @returns {Function} The composed HTTP handler.
   *
   * @example
   *
   * export default withRest({
   *   GET: async (req, res) => {
   *     // Do something...
   *
   *     return { foo: 'bar' };
   *   },
   * });
   */
  const withRest = (
    methods: {
      [x: string]: any
    },
    opts: {
      logError?: typeof defaultLogError
      sendError?: typeof defaultSendError
    } = {
      logError: defaultLogError,
      sendError: defaultSendError,
    }
  ) => {
    const options = {
      logError: defaultLogError,
      sendError: defaultSendError,
      ...opts,
    }
  
    return async (req: NextApiRequest, res: NextApiResponse) => {
      try {
        const method = methods && methods[req.method || 'unknown']
  
        if (!method) {
          throw Boom.methodNotAllowed(
            `Method ${req.method} is not supported for this endpoint`
          )
        }
  
        const json = await method(req, res)
  
        // Do nothing if the request is already sent (e.g.: a redirect was issued)
        if (res.headersSent) {
          if (json !== undefined) {
            options.logError(
              Boom.internal(
                'You have sent the response inside your handler but still returned something. This error was not sent to the client, however you should probably not return a value in the handler.'
              ) // eslint-disable-line max-len
            )
          }
  
          return
        }
  
        // Next.js doesn't support nulls as `RFC7159` dictates, but we do
        if (json == null) {
          res.setHeader('Content-Type', 'application/json')
          res.setHeader('Content-Length', '4')
          res.end('null')
        } else {
          res.json(json)
        }
      } catch (err: Error | Boom.Boom | any) {
        // Not an ApiError? Then wrap it into an ApiError and log it.
        let apiError = err
        if (!err.isBoom) {
          apiError = Boom.internal(undefined, { originalError: err })
        }
  
        options.logError(apiError)
        options.sendError(res, apiError)
      }
    }
  }
  
  export default withRest
  

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions