UNPKG

exstack

Version:

A utility library designed to simplify and enhance Express.js applications.

388 lines (383 loc) 12.4 kB
// src/enums.ts var HttpStatus = Object.freeze({ /** Continue with the request. */ CONTINUE: 100, "100_NAME": "CONTINUE", /** Switching protocols. */ SWITCHING_PROTOCOLS: 101, "101_NAME": "SWITCHING_PROTOCOLS", /** Request is being processed. */ PROCESSING: 102, "102_NAME": "PROCESSING", /** Early hints for the client. */ EARLYHINTS: 103, "103_NAME": "EARLY_HINTS", /** Request succeeded. */ OK: 200, "200_NAME": "OK", /** Resource created. */ CREATED: 201, "201_NAME": "CREATED", /** Request accepted for processing. */ ACCEPTED: 202, "202_NAME": "ACCEPTED", /** Non-authoritative information. */ NON_AUTHORITATIVE_INFORMATION: 203, "203_NAME": "NON_AUTHORITATIVE_INFORMATION", /** No content to send. */ NO_CONTENT: 204, "204_NAME": "NO_CONTENT", /** Content reset. */ RESET_CONTENT: 205, "205_NAME": "RESET_CONTENT", /** Partial content delivered. */ PARTIAL_CONTENT: 206, "206_NAME": "PARTIAL_CONTENT", /** Multiple choices available. */ AMBIGUOUS: 300, "300_NAME": "AMBIGUOUS", /** Resource moved permanently. */ MOVED_PERMANENTLY: 301, "301_NAME": "MOVED_PERMANENTLY", /** Resource found at another URI. */ FOUND: 302, "302_NAME": "FOUND", /** See other resource. */ SEE_OTHER: 303, "303_NAME": "SEE_OTHER", /** Resource not modified. */ NOT_MODIFIED: 304, "304_NAME": "NOT_MODIFIED", /** Temporary redirect. */ TEMPORARY_REDIRECT: 307, "307_NAME": "TEMPORARY_REDIRECT", /** Permanent redirect. */ PERMANENT_REDIRECT: 308, "308_NAME": "PERMANENT_REDIRECT", /** Bad request. */ BAD_REQUEST: 400, "400_NAME": "BAD_REQUEST", /** Authentication required. */ UNAUTHORIZED: 401, "401_NAME": "UNAUTHORIZED", /** Payment required. */ PAYMENT_REQUIRED: 402, "402_NAME": "PAYMENT_REQUIRED", /** Access forbidden. */ FORBIDDEN: 403, "403_NAME": "FORBIDDEN", /** Resource not found. */ NOT_FOUND: 404, "404_NAME": "NOT_FOUND", /** Method not allowed. */ METHOD_NOT_ALLOWED: 405, "405_NAME": "METHOD_NOT_ALLOWED", /** Not acceptable content. */ NOT_ACCEPTABLE: 406, "406_NAME": "NOT_ACCEPTABLE", /** Proxy authentication required. */ PROXY_AUTHENTICATION_REQUIRED: 407, "407_NAME": "PROXY_AUTHENTICATION_REQUIRED", /** Request timed out. */ REQUEST_TIMEOUT: 408, "408_NAME": "REQUEST_TIMEOUT", /** Conflict with current state. */ CONFLICT: 409, "409_NAME": "CONFLICT", /** Resource gone. */ GONE: 410, "410_NAME": "GONE", /** Length required. */ LENGTH_REQUIRED: 411, "411_NAME": "LENGTH_REQUIRED", /** Precondition failed. */ PRECONDITION_FAILED: 412, "412_NAME": "PRECONDITION_FAILED", /** Payload too large. */ PAYLOAD_TOO_LARGE: 413, "413_NAME": "PAYLOAD_TOO_LARGE", /** URI too long. */ URI_TOO_LONG: 414, "414_NAME": "URI_TOO_LONG", /** Unsupported media type. */ UNSUPPORTED_MEDIA_TYPE: 415, "415_NAME": "UNSUPPORTED_MEDIA_TYPE", /** Requested range not satisfiable. */ REQUESTED_RANGE_NOT_SATISFIABLE: 416, "416_NAME": "REQUESTED_RANGE_NOT_SATISFIABLE", /** Expectation failed. */ EXPECTATION_FAILED: 417, "417_NAME": "EXPECTATION_FAILED", /** I'm a teapot. */ I_AM_A_TEAPOT: 418, "418_NAME": "I_AM_A_TEAPOT", /** Misdirected request. */ MISDIRECTED: 421, "421_NAME": "MISDIRECTED", /** Unprocessable entity. */ UNPROCESSABLE_ENTITY: 422, "422_NAME": "UNPROCESSABLE_ENTITY", /** Locked. */ LOCKED: 423, "423_NAME": "LOCKED", /** Failed dependency. */ FAILED_DEPENDENCY: 424, "424_NAME": "FAILED_DEPENDENCY", /** Too early. */ TOO_EARLY: 425, "425_NAME": "TOO_EARLY", /** Upgrade required. */ UPGRADE_REQUIRED: 426, "426_NAME": "UPGRADE_REQUIRED", /** Precondition required. */ PRECONDITION_REQUIRED: 428, "428_NAME": "PRECONDITION_REQUIRED", /** Too many requests. */ TOO_MANY_REQUESTS: 429, "429_NAME": "TOO_MANY_REQUESTS", /** Request header fields too large. */ REQUEST_HEADER_FIELDS_TOO_LARGE: 431, "431_NAME": "REQUEST_HEADER_FIELDS_TOO_LARGE", /** Unavailable for legal reasons. */ UNAVAILABLE_FOR_LEGAL_REASONS: 451, "451_NAME": "UNAVAILABLE_FOR_LEGAL_REASONS", /** Internal server error. */ INTERNAL_SERVER_ERROR: 500, "500_NAME": "INTERNAL_SERVER_ERROR", /** Not implemented. */ NOT_IMPLEMENTED: 501, "501_NAME": "NOT_IMPLEMENTED", /** Bad gateway. */ BAD_GATEWAY: 502, "502_NAME": "BAD_GATEWAY", /** Service unavailable. */ SERVICE_UNAVAILABLE: 503, "503_NAME": "SERVICE_UNAVAILABLE", /** Gateway timeout. */ GATEWAY_TIMEOUT: 504, "504_NAME": "GATEWAY_TIMEOUT", /** HTTP version not supported. */ HTTP_VERSION_NOT_SUPPORTED: 505, "505_NAME": "HTTP_VERSION_NOT_SUPPORTED", /** Variant also negotiates. */ VARIANT_ALSO_NEGOTIATES: 506, "506_NAME": "VARIANT_ALSO_NEGOTIATES", /** Insufficient storage. */ INSUFFICIENT_STORAGE: 507, "507_NAME": "INSUFFICIENT_STORAGE", /** Loop detected. */ LOOP_DETECTED: 508, "508_NAME": "LOOP_DETECTED", /** Bandwidth limit exceeded. */ BANDWIDTH_LIMIT_EXCEEDED: 509, "509_NAME": "BANDWIDTH_LIMIT_EXCEEDED", /** Not extended. */ NOT_EXTENDED: 510, "510_NAME": "NOT_EXTENDED", /** Network authentication required. */ NETWORK_AUTHENTICATION_REQUIRED: 511, "511_NAME": "NETWORK_AUTHENTICATION_REQUIRED" }); // src/errors.ts var getErrorName = (status) => { if (status < 400 || status > 511) return "HttpError"; const statusKey = HttpStatus[`${status}_NAME`]; if (!statusKey) return "HttpError"; const name = statusKey.toLowerCase().replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()).replace(/\s+/g, ""); return name.endsWith("Error") ? name : name.concat("Error"); }; var _HttpError = class _HttpError extends Error { /** * Creates an instance of `HTTPException`. * @param status - HTTP status code for the exception. Defaults to 500. * @param options - Additional options for the exception. */ constructor(status = HttpStatus.INTERNAL_SERVER_ERROR, options) { super(typeof options.message === "string" ? options.message : getErrorName(status)); this.status = status; this.options = options; this.name = getErrorName(status); Error.captureStackTrace(this, this.constructor); } /** * Convert the HttpError instance to a Body object. * @example * const errorBody = new HttpError(404, {message: 'Not Found'}).body; */ get body() { const { name: error, status } = this; const { message, data = null } = this.options; return { status, error, message, data }; } /** * Send the json of the error in an HTTP response. * @param {Response} res - The Express response object. * * @example * new HttpError(404, {message: 'Not Found'}).toJson(res); */ toJson(res) { res.status(this.status).json(this.body); } }; /** * Check if the given error is an instance of HttpError. * @param {unknown} value - The error to check. * @returns {boolean} - True if the error is an instance of HttpError, false otherwise. * * @example * if (HttpError.isHttpError(error)) { * // Handle the HttpError * } */ _HttpError.isHttpError = (value) => value instanceof _HttpError; var HttpError = _HttpError; var createHttpErrorClass = (status) => class extends HttpError { constructor(message, data, cause) { super(status, { message, data, cause }); } }; var BadRequestError = createHttpErrorClass(HttpStatus.BAD_REQUEST); var ConflictError = createHttpErrorClass(HttpStatus.CONFLICT); var ForbiddenError = createHttpErrorClass(HttpStatus.FORBIDDEN); var NotFoundError = createHttpErrorClass(HttpStatus.NOT_FOUND); var UnAuthorizedError = createHttpErrorClass(HttpStatus.UNAUTHORIZED); var InternalServerError = createHttpErrorClass(HttpStatus.INTERNAL_SERVER_ERROR); var ContentTooLargeError = createHttpErrorClass(HttpStatus.PAYLOAD_TOO_LARGE); // src/utils.ts import { Router } from "express"; var errorHandler = (isDev = true, logger = console.error) => (err, _req, res, _next) => { if (HttpError.isHttpError(err)) { if (err.options.cause) logger?.(`HttpError Cause: ${err.options.cause}`); return err.toJson(res); } logger?.(`Unknown Error: ${err}`); const unknown = { status: HttpStatus.INTERNAL_SERVER_ERROR, error: "InternalServerError", message: isDev ? err.message || "Unexpected error" : "Something went wrong", stack: isDev ? err.stack : void 0 }; res.status(unknown.status).json(unknown); }; var notFound = (path = "*") => Router().all( path, (req, res) => new NotFoundError(`Cannot ${req.originalUrl} on ${req.method.toUpperCase()}`).toJson(res) ); function makePermission(options) { const { actions, subjects, filter } = options; const map_data = subjects.flatMap( (subject) => (filter?.[subject] ?? actions).map((action) => [ `${subject}_${action}`.toUpperCase(), // Convert key to uppercase `${subject.toLowerCase()}:${action.toLowerCase()}` ]) ); return Object.freeze(Object.fromEntries(map_data)); } // src/api-res.ts var _ApiRes = class _ApiRes { /** * Creates an instance of ApiRes. * @param {any} result - The result of the operation * @param {Status} status - The HTTP status code * @param {string} message - The response message */ constructor(result = {}, status = HttpStatus.OK, message = "Operation successful") { this.result = result; this.status = status; this.message = message; } /** * Returns the Body (JSON) representation of the response. * @returns The Body (JSON) representation of the response * * @example * new ApiRes('Hello World', 200).body; */ get body() { return { status: this.status, message: this.message, result: this.result }; } /** * Send the json of HTTP response. * @param {Response} res - The Express response object. * * @example * new ApiRes('Hello World', 200).toJson(res); */ toJson(res) { res.status(this.status).json(this.body); } }; /** * Creates an OK (200) response. * @param {any} result - The result to be included in the response * @param {string} [message='Request processed successfully'] - The response message * @returns {ApiRes} An ApiRes instance with OK status */ _ApiRes.ok = (result, message = "Request processed successfully") => new _ApiRes(result, HttpStatus.OK, message); /** * Creates a Created (201) response. * @param {any} result - The result to be included in the response * @param {string} [message='Resource created successfully'] - The response message * @returns {ApiRes} An ApiRes instance with Created status */ _ApiRes.created = (result, message = "Resource created successfully") => new _ApiRes(result, HttpStatus.CREATED, message); /** * Creates a paginated OK (200) response. * @param {any} data - The paginated data * @param {object} meta - Metadata for pagination * @param {string} [message='Data retrieved successfully'] - The response message * @returns {ApiRes} An ApiRes instance with OK status and paginated data */ _ApiRes.paginated = (data, meta, message = "Data retrieved successfully") => new _ApiRes({ ...meta, data }, HttpStatus.OK, message); var ApiRes = _ApiRes; // src/handler.ts var handleResult = (result, res) => { if (result instanceof ApiRes) result.toJson(res); else if (result && result !== res) res.send(result); }; var handler = (callback) => async (req, res, next) => { try { const result = callback(req, res, next); if (result instanceof Promise) await result.then((value) => handleResult(value, res)).catch(next); else handleResult(result, res); } catch (error) { next(error); } }; var proxyWrapper = (clsOrInstance, ...args) => { const instance = typeof clsOrInstance === "function" ? new clsOrInstance(...args) : clsOrInstance; return new Proxy(instance, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); return typeof value === "function" ? handler(value.bind(target)) : value; }, set() { throw new Error("Overriding methods and properties is not allowed."); } }); }; export { ApiRes, BadRequestError, ConflictError, ContentTooLargeError, ForbiddenError, HttpError, HttpStatus, InternalServerError, NotFoundError, UnAuthorizedError, createHttpErrorClass, errorHandler, getErrorName, handler, makePermission, notFound, proxyWrapper };