UNPKG

@jsbytes/boom

Version:

Boom js with Types matching HTTP status codes

483 lines (407 loc) 11.4 kB
/** * http status code/message map */ const http = new Map(); http.set(100, 'Continue'); http.set(101, 'Switching Protocols'); http.set(102, 'Processing'); http.set(103, 'Early Hints'); http.set(200, 'OK'); http.set(201, 'Created'); http.set(202, 'Accepted'); http.set(203, 'Non-Authoritative Information'); http.set(204, 'No Content'); http.set(205, 'Reset Content'); http.set(206, 'Partial Content'); http.set(207, 'Multi-Status'); http.set(208, 'Already Reported'); http.set(226, 'IM Used'); http.set(300, 'Multiple Choices'); http.set(301, 'Moved Permanently'); http.set(302, 'Found'); http.set(303, 'See Other'); http.set(304, 'Not Modified'); http.set(305, 'Use Proxy'); http.set(307, 'Temporary Redirect'); http.set(308, 'Permanent Redirect'); http.set(400, 'Bad Request'); http.set(401, 'Unauthorized'); http.set(402, 'Payment Required'); http.set(403, 'Forbidden'); http.set(404, 'Not Found'); http.set(405, 'Method Not Allowed'); http.set(406, 'Not Acceptable'); http.set(407, 'Proxy Authentication Required'); http.set(408, 'Request Timeout'); http.set(409, 'Conflict'); http.set(410, 'Gone'); http.set(411, 'Length Required'); http.set(412, 'Precondition Failed'); http.set(413, 'Payload Too Large'); http.set(414, 'URI Too Long'); http.set(415, 'Unsupported Media Type'); http.set(416, 'Range Not Satisfiable'); http.set(417, 'Expectation Failed'); http.set(418, "I'm a Teapot"); http.set(421, 'Misdirected Request'); http.set(422, 'Unprocessable Entity'); http.set(423, 'Locked'); http.set(424, 'Failed Dependency'); http.set(425, 'Unordered Collection'); http.set(426, 'Upgrade Required'); http.set(428, 'Precondition Required'); http.set(429, 'Too Many Requests'); http.set(431, 'Request Header Fields Too Large'); http.set(451, 'Unavailable For Legal Reasons'); http.set(500, 'Internal Server Error'); http.set(501, 'Not Implemented'); http.set(502, 'Bad Gateway'); http.set(503, 'Service Unavailable'); http.set(504, 'Gateway Timeout'); http.set(505, 'HTTP Version Not Supported'); http.set(506, 'Variant Also Negotiates'); http.set(507, 'Insufficient Storage'); http.set(508, 'Loop Detected'); http.set(509, 'Bandwidth Limit Exceeded'); http.set(510, 'Not Extended'); http.set(511, 'Network Authentication Required'); /** * @class {Exception} */ export class Exception extends Error { /** * @param {String} message - exception message * @param {*} [data] - any additional data */ constructor(message = '', data = null) { super(message); this.data = data; this.date = new Date(); this.name = new.target.name; Error.captureStackTrace(this, new.target); } /** * Throws this exception */ throw() { throw this; } /** * Throws new Exception */ static throw(...args) { throw new this(...args); } /** * Exception factory */ static create(...args) { return new this(...args); } } /** * @class {HttpException} */ export class HttpException extends Exception { /** * Default http status code * @return {Number} http status code */ static get status() { return 500; } /** * Default http status message * @return {String} http status message */ static get message() { return http.get(this.status); } /** * @param {Number} [status] - http status code * @param {String} [message] - exception message * @param {*} [data] - any additional data */ constructor(...args) { const { status, message, data } = new.target.options(...args); super(message, data); this.status = status; this.server = status >= 500; this.statusCode = status; this.statusMessage = new.target.message; } /** * @private * Parse parameters and construct default constructor options * All parameters are optional * * @param {Number} [status] - http status code * @param {String} [message] - exception message * @param {*} [data] - any additional data * @return {Object} options - { status, message, data } */ static options(...args) { let [ status = this.status, message = this.message, data = null ] = args; /** * Validate message type * * @param {*} any - message * @return {Boolean} result */ const ok = msg => typeof msg === 'string' || typeof msg === 'number' || typeof msg === 'boolean'; /** * case 0 - default - no args * case 1 - status || message * case 2 - (status + message) || (message + data) * case 3 - status & message & data */ switch (args.length) { case 1: { if (status == null) { status = this.status; message = this.message; break; } if (http.has(~~status)) { status = ~~status; message = http.get(status); } else { message = status; status = this.status; } if (!ok(message)) { data = message; message = http.get(status); } break; } case 2: { if (status == null) { status = this.status; } else if (http.has(~~status)) { status = ~~status; } else { data = message; message = status; status = this.status; } if (!ok(message)) { data = message; message = http.get(status); } break; } case 3: { status = http.has(~~status) ? ~~status : this.status; if (!ok(message)) { message = http.get(status); } break; } case 0: default: { status = this.status; message = this.message; data = null; break; } } return { status, message, data }; } } /** * @type {Symbol} - Boom Symbol */ const symbol = Symbol('Boom'); /** * @class {Boom} */ export class Boom { /** * @param {Number} [status] - http status code * @param {String} [message] - exception message * @param {*} [data] - any additional data */ constructor(...args) { let [ status ] = args; status = http.has(~~status) ? ~~status : 500; const type = Boom.getTypeName(status); return new Boom[type](...args); } /** * Check if error is instance of Boom or if error has been boomified * * @param {Error} error - error to check * @return {Boolean} result - true or false */ static isBoom(error) { return !!(error instanceof Error && error.boom && error[symbol]); } /** * Symbol hasInstance * * @param {Error} error * @return {Boolean} result */ static [Symbol.hasInstance](error) { return Boom.isBoom(error); } /** * @private * Determine type name from status code * * @param {Number} status - status code * @return {String} type - type name */ static getTypeName(status) { const code = ~~status; if (!http.has(code)) { throw new Exception('[Boom] valid status code is required', status); } const message = http.get(code); /** * @type {String} - http exception type name */ const type = message .split(' ') .map(s => s.replace(/[-']/gi, '')) .map(s => s[0].toUpperCase() + s.slice(1).toLowerCase()) .join(''); return type; } /** * @private * Determine factory function name from status code * * @param {Number} status - status code * @return {String} factory - factory name */ static getFactoryName(status) { const type = Boom.getTypeName(status); /** * @type {String} - the name of the factory function for any type */ const factory = type[0].toLowerCase() + type.slice(1); return factory; } /** * Decorate Boom with custom exception type and factory method * * @param {Object} options * @param {String} options.type - type name * @param {String} options.factory - factory name * @param {Number} [options.status = 500] - optional status code * @param {String} [option.message = 'Internal Server Error'] - optional status message */ static decorate(options) { if (options == null) { throw new Exception('[Boom] options object is required'); } let { type, factory, status, message } = options; if (typeof type !== 'string') { throw new Exception('[Boom] type name string is required'); } if (typeof factory !== 'string') { throw new Exception('[Boom] factory name string is required'); } if (status == null) { status = 500; } if (!http.has(~~status)) { throw new Exception('[Boom] Invalid status code'); } if (message == null) { message = http.get(status); } if (typeof message !== 'string') { throw new Exception('[Boom] Message should be string') } if (Boom[type]) throw new Exception(`[Boom] Decorator type "${type}" is already registered`); if (Boom[factory]) throw new Exception(`[Boom] Decorator factory "${factory}" is already registered`); /** * @class {BoomHttpException} */ class BoomHttpException extends HttpException { static get status() { return status; } static get message() { return message || http.get(this.status); } constructor(...args) { super(...args); } get boom() { return true; } get [symbol]() { return true; } }; Object.defineProperty(BoomHttpException, 'name', { value: type.concat('Exception') }); // attach type to Boom Boom[type] = BoomHttpException; // attach factory method to Boom Boom[factory] = BoomHttpException.create.bind(BoomHttpException); Object.defineProperty(Boom[factory], 'name', { value: factory }); // allow chaining return Boom; } /** * Patch error object with boom properties * * @param {Error} error - error to boomify * @param {Object} [options] - options object * @param {String} [options.message] - exception message * @param {*} [options.data] - any additional data * @param {Number} [options.status] - status code to use * * @return {Boom} error - boomified exception */ static boomify(error, options = {}) { if (!(error instanceof Error)) { throw new Exception('[Boom] Error instance is required'); } if (error instanceof Boom) { return error; } let status = Number(options.status); if (!http.has(status)) { status = 500; } const message = options.message || error.message || http.get(status); error.status = status; error.statusCode = status; error.message = message; error.statusMessage = http.get(status); error.server = status >= 500; error.date = new Date(); error.data = options.data != null ? options.data : null; error.name = Boom.getTypeName(status).concat('Exception'); error.throw = () => { throw error }; Object.defineProperty(error, 'boom', { value: true }); Object.defineProperty(error, symbol, { value: true }); return error; } } /** * Define Boom Api & Types */ for (const [statusCode, statusMessage] of http) { if (statusCode < 400) continue; const type = Boom.getTypeName(statusCode); const factory = Boom.getFactoryName(statusCode); Boom.decorate({ type, factory, status: statusCode, message: statusMessage }); }