Home Reference Source

src/AutonymError.js

import HTTP from 'http-status-codes'

/**
 * Wrapper for any error that occurred in a policy or store method.
 */
export default class AutonymError {
  /**
   * Code indicating the server could not understand the request due to invalid syntax.
   * @constant
   * @type {string}
   */
  static BAD_REQUEST = 'BAD_REQUEST'

  /**
   * Code indicating the client does not have access rights to the content.
   * @constant
   * @type {string}
   */
  static FORBIDDEN = 'FORBIDDEN'

  /**
   * Code indicating the requested store method is not available for this model.
   * @constant
   * @type {string}
   */
  static METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED'

  /**
   * Code indicating the requested resource does not exist.
   * @constant
   * @type {string}
   */
  static NOT_FOUND = 'NOT_FOUND'

  /**
   * Code indicating the client must be authenticated to perform this action.
   * @constant
   * @type {string}
   */
  static UNAUTHORIZED = 'UNAUTHORIZED'

  /**
   * Code indicating the request was improper, i.e. failed schema validation.
   * @constant
   * @type {string}
   */
  static UNPROCESSABLE_ENTITY = 'UNPROCESSABLE_ENTITY'

  /**
   * Default. Code indicating an unhandled error occurred while the server was processing the request.
   * @constant
   * @type {string}
   */
  static INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'

  /**
   * The error codes that indicate client request errors, rather than internal errors.
   * @constant
   * @type {string[]}
   */
  static CLIENT_ERRORS = [
    AutonymError.BAD_REQUEST,
    AutonymError.FORBIDDEN,
    AutonymError.METHOD_NOT_ALLOWED,
    AutonymError.NOT_FOUND,
    AutonymError.UNAUTHORIZED,
    AutonymError.UNPROCESSABLE_ENTITY,
  ]

  /**
   * Wraps the given error and returns an instance of AutonymError, or returns the given error if it already is an
   * AutonymError. If the error object has the property `code`, it will be used as the AutonymError code. If the
   * error object implements a `toJSON` method, its result will be stored as additional data.
   * @param {Error} error An error object.
   * @returns {AutonymError} The instance of AutonymError.
   * @example
   * const err = new Error('Something bad happened')
   * const autonymError = AutonymError.fromError(err)
   * console.log(autonymError.getPayload()) // { message: 'An internal server error occurred.' }
   * @example
   * const err = new Error('Something bad happened')
   * err.code = AutonymError.BAD_REQUEST
   * const autonymError = AutonymError.fromError(err)
   * // Still internal server error until we call `#toClientError()`
   * console.log(autonymError.getPayload()) // { message: 'An internal server error occurred.' }
   */
  static fromError(error) {
    if (error.isAutonymError) {
      return error
    } else {
      const autonymError = new AutonymError(
        error.code || AutonymError.INTERNAL_SERVER_ERROR,
        error.message || 'An unknown error occurred.',
        typeof error.toJSON === 'function' ? error.toJSON() : error
      )
      autonymError.stack = error.stack
      return autonymError
    }
  }

  /**
   * @param {string} [code] One of the error code static constants, or any other identifiable value for the error
   * type. If falsy, will fall back to `AutonymError.INTERNAL_SERVER_ERROR`.
   * @param {string} message A human-readable description of the error. It will only be passed to the client in a
   * response if the code is one of `Autonym.CLIENT_ERRORS`.
   * @param {object} [data] Additional metadata to store on the error.
   * @example
   * const autonymError = new AutonymError(null, 'Something bad happened')
   * @example
   * const autonymError = new AutonymError(AutonymError.BAD_REQUEST, 'Something bad happened', { invalid: 'xyz' })
   */
  constructor(code, message, data = {}) {
    const normalizedCode = code || AutonymError.INTERNAL_SERVER_ERROR
    this._code = normalizedCode
    this._message = message
    this._data = data
    this.stack = new Error(`[${normalizedCode}] ${message}`).stack
    this._isClientError = false
    this.isAutonymError = true
  }

  /**
   * Gets the error code.
   * @returns {string} The error code.
   */
  getCode() {
    return this._code
  }

  /**
   * Gets the error message.
   * @returns {string} The error message.
   */
  getMessage() {
    return this._message
  }

  /**
   * Gets the HTTP status code for the error based on its code, or falls back to `AutonymError.INTERNAL_SERVER_ERROR`.
   * @returns {number} The HTTP status code.
   */
  getStatus() {
    return HTTP[this.getCode() || AutonymError.INTERNAL_SERVER_ERROR] || HTTP.INTERNAL_SERVER_ERROR
  }

  /**
   * Gets the error metadata.
   * @returns {object} The error metadata.
   */
  getData() {
    return this._data
  }

  /**
   * Gets the data to send in the HTTP response. It will only return the error data and message if the error has
   * been converted to a client error and its code is one of `Autonym.CLIENT_ERRORS`.
   * @returns {object} The error payload. At a minimum, this object will have a `message` property.
   * @example
   * const err = new AutonymError(AutonymError.INTERNAL_SERVER_ERROR, 'Something bad happened.', { x: 2 })
   * console.log(err.getPayload()) // { message: 'An internal server error occurred.' }
   * @example
   * const err = new AutonymError(AutonymError.BAD_REQUEST, 'Something bad happened.', { x: 2 })
   * console.log(err.getPayload()) // { x: 2, message: 'Something bad happened.' }
   */
  getPayload() {
    if (this.isClientError()) {
      return { ...this.getData(), message: this.getMessage() }
    } else {
      return { message: 'An internal server error occurred.' }
    }
  }

  /**
   * Creates a copy of this error with a flag on it indicating it is a client error. Any error thrown will by
   * default be an internal server error, until it is converted to a client error and it has an applicable error code.
   * @returns {AutonymError} The client error.
   * @example
   * const clientError = (new AutonymError(null, 'Something bad happened')).toClientError()
   * console.log(clientError.isClientError()) // false
   * console.log(clientError.getPayload()) // { message: 'An internal server error occurred.' }
   * console.log(clientError.getStatus()) // 500
   * @example
   * const clientError = (new AutonymError(AutonymError.BAD_REQUEST, 'Something bad happened')).toClientError()
   * console.log(clientError.isClientError()) // true
   * console.log(clientError.getPayload()) // { message: 'Something bad happened' }
   * console.log(clientError.getStatus()) // 400
   */
  toClientError() {
    const clientError = new AutonymError(this.getCode(), this.getMessage(), this.getData())
    clientError._isClientError = true
    return clientError
  }

  /**
   * Checks if the error is a client error and its code is one of `Autonym.CLIENT_ERRORS`.
   * @returns {boolean} Whether it is a client error.
   */
  isClientError() {
    return this._isClientError && AutonymError.CLIENT_ERRORS.includes(this.getCode())
  }
}