@jsbytes/boom
Version:
Boom js with Types matching HTTP status codes
483 lines (407 loc) • 11.4 kB
JavaScript
/**
* 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
});
}