exstack
Version:
A utility library designed to simplify and enhance Express.js applications.
496 lines (489 loc) • 16 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const express = __toESM(require("express"));
//#region src/enums.ts
/**
* Enum representing HTTP status codes.
*
* @publicApi http-status code
*/
const HttpStatus = Object.freeze({
CONTINUE: 100,
"100_NAME": "CONTINUE",
SWITCHING_PROTOCOLS: 101,
"101_NAME": "SWITCHING_PROTOCOLS",
PROCESSING: 102,
"102_NAME": "PROCESSING",
EARLYHINTS: 103,
"103_NAME": "EARLY_HINTS",
OK: 200,
"200_NAME": "OK",
CREATED: 201,
"201_NAME": "CREATED",
ACCEPTED: 202,
"202_NAME": "ACCEPTED",
NON_AUTHORITATIVE_INFORMATION: 203,
"203_NAME": "NON_AUTHORITATIVE_INFORMATION",
NO_CONTENT: 204,
"204_NAME": "NO_CONTENT",
RESET_CONTENT: 205,
"205_NAME": "RESET_CONTENT",
PARTIAL_CONTENT: 206,
"206_NAME": "PARTIAL_CONTENT",
AMBIGUOUS: 300,
"300_NAME": "AMBIGUOUS",
MOVED_PERMANENTLY: 301,
"301_NAME": "MOVED_PERMANENTLY",
FOUND: 302,
"302_NAME": "FOUND",
SEE_OTHER: 303,
"303_NAME": "SEE_OTHER",
NOT_MODIFIED: 304,
"304_NAME": "NOT_MODIFIED",
TEMPORARY_REDIRECT: 307,
"307_NAME": "TEMPORARY_REDIRECT",
PERMANENT_REDIRECT: 308,
"308_NAME": "PERMANENT_REDIRECT",
BAD_REQUEST: 400,
"400_NAME": "BAD_REQUEST",
UNAUTHORIZED: 401,
"401_NAME": "UNAUTHORIZED",
PAYMENT_REQUIRED: 402,
"402_NAME": "PAYMENT_REQUIRED",
FORBIDDEN: 403,
"403_NAME": "FORBIDDEN",
NOT_FOUND: 404,
"404_NAME": "NOT_FOUND",
METHOD_NOT_ALLOWED: 405,
"405_NAME": "METHOD_NOT_ALLOWED",
NOT_ACCEPTABLE: 406,
"406_NAME": "NOT_ACCEPTABLE",
PROXY_AUTHENTICATION_REQUIRED: 407,
"407_NAME": "PROXY_AUTHENTICATION_REQUIRED",
REQUEST_TIMEOUT: 408,
"408_NAME": "REQUEST_TIMEOUT",
CONFLICT: 409,
"409_NAME": "CONFLICT",
GONE: 410,
"410_NAME": "GONE",
LENGTH_REQUIRED: 411,
"411_NAME": "LENGTH_REQUIRED",
PRECONDITION_FAILED: 412,
"412_NAME": "PRECONDITION_FAILED",
PAYLOAD_TOO_LARGE: 413,
"413_NAME": "PAYLOAD_TOO_LARGE",
URI_TOO_LONG: 414,
"414_NAME": "URI_TOO_LONG",
UNSUPPORTED_MEDIA_TYPE: 415,
"415_NAME": "UNSUPPORTED_MEDIA_TYPE",
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
"416_NAME": "REQUESTED_RANGE_NOT_SATISFIABLE",
EXPECTATION_FAILED: 417,
"417_NAME": "EXPECTATION_FAILED",
I_AM_A_TEAPOT: 418,
"418_NAME": "I_AM_A_TEAPOT",
MISDIRECTED: 421,
"421_NAME": "MISDIRECTED",
UNPROCESSABLE_ENTITY: 422,
"422_NAME": "UNPROCESSABLE_ENTITY",
LOCKED: 423,
"423_NAME": "LOCKED",
FAILED_DEPENDENCY: 424,
"424_NAME": "FAILED_DEPENDENCY",
TOO_EARLY: 425,
"425_NAME": "TOO_EARLY",
UPGRADE_REQUIRED: 426,
"426_NAME": "UPGRADE_REQUIRED",
PRECONDITION_REQUIRED: 428,
"428_NAME": "PRECONDITION_REQUIRED",
TOO_MANY_REQUESTS: 429,
"429_NAME": "TOO_MANY_REQUESTS",
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
"431_NAME": "REQUEST_HEADER_FIELDS_TOO_LARGE",
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
"451_NAME": "UNAVAILABLE_FOR_LEGAL_REASONS",
INTERNAL_SERVER_ERROR: 500,
"500_NAME": "INTERNAL_SERVER_ERROR",
NOT_IMPLEMENTED: 501,
"501_NAME": "NOT_IMPLEMENTED",
BAD_GATEWAY: 502,
"502_NAME": "BAD_GATEWAY",
SERVICE_UNAVAILABLE: 503,
"503_NAME": "SERVICE_UNAVAILABLE",
GATEWAY_TIMEOUT: 504,
"504_NAME": "GATEWAY_TIMEOUT",
HTTP_VERSION_NOT_SUPPORTED: 505,
"505_NAME": "HTTP_VERSION_NOT_SUPPORTED",
VARIANT_ALSO_NEGOTIATES: 506,
"506_NAME": "VARIANT_ALSO_NEGOTIATES",
INSUFFICIENT_STORAGE: 507,
"507_NAME": "INSUFFICIENT_STORAGE",
LOOP_DETECTED: 508,
"508_NAME": "LOOP_DETECTED",
BANDWIDTH_LIMIT_EXCEEDED: 509,
"509_NAME": "BANDWIDTH_LIMIT_EXCEEDED",
NOT_EXTENDED: 510,
"510_NAME": "NOT_EXTENDED",
NETWORK_AUTHENTICATION_REQUIRED: 511,
"511_NAME": "NETWORK_AUTHENTICATION_REQUIRED"
});
//#endregion
//#region src/errors.ts
/**
* Get a human-readable error name from the HTTP status code.
* @param {number} status - The HTTP status code.
* @returns {string} - The formatted error name.
*/
const 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");
};
/**
* Base class for handling HTTP errors.
* @extends {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);
}
/**
* 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
* }
*/
static isHttpError = (value) => value instanceof HttpError;
/**
* 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, code = null } = this.options;
return {
status,
error,
message,
data,
code
};
}
/**
* 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);
}
};
/**
* Utility function to create custom error classes.
* @param status - HTTP status code.
* @returns - A new error class.
* @example
* const NotFoundError = createHttpErrorClass(HttpStatus.NOT_FOUND);
*/
const createHttpErrorClass = (status) => class extends HttpError {
constructor(message, options = {}) {
super(status, {
message,
...options
});
}
};
/**
* Represents a Bad Request HTTP error (400).
* @extends {HttpError}
*/
const BadRequestError = createHttpErrorClass(HttpStatus.BAD_REQUEST);
/**
* Represents a Conflict HTTP error (409).
* @extends {HttpError}
*/
const ConflictError = createHttpErrorClass(HttpStatus.CONFLICT);
/**
* Represents a Forbidden HTTP error (403).
* @extends {HttpError}
*/
const ForbiddenError = createHttpErrorClass(HttpStatus.FORBIDDEN);
/**
* Represents a Not Found HTTP error (404).
* @extends {HttpError}
*/
const NotFoundError = createHttpErrorClass(HttpStatus.NOT_FOUND);
/**
* Represents an UnAuthorized HTTP error (401).
* @extends {HttpError}
*/
const UnAuthorizedError = createHttpErrorClass(HttpStatus.UNAUTHORIZED);
/**
* Represents an Internal Server Error HTTP error (500).
* @extends {HttpError}
*/
const InternalServerError = createHttpErrorClass(HttpStatus.INTERNAL_SERVER_ERROR);
/**
* Represents an Content Too Larger Error HTTP error (413).
* @extends {HttpError}
*/
const ContentTooLargeError = createHttpErrorClass(HttpStatus.PAYLOAD_TOO_LARGE);
//#endregion
//#region src/utils.ts
/**
* Express middleware to handle `HttpError` and unknown errors.
*
* - Sends JSON response for `HttpError` instances.
* - Logs unknown errors and sends generic error response.
* - Includes detailed error info in development (`isDev`).
*
* @param {Boolean} [isDev = true] - Flag to indicate if the environment is development.
* @param {(error: unknown) => void} [logger = console.error] - Function to log errors.
* @returns {ErrorRequestHandler} - Middleware for handling errors.
*
* @example
* // Basic usage with default options:
* app.use(errorHandler(process.env.NODE_ENV !== 'production'));
* // Custom usage with a logging function in production mode:
* app.use(errorHandler(conf.isDev, logger.error));
*/
const errorHandler = (isDev = true, logger = console.error) => (err, _req, res, _next) => {
if (HttpError.isHttpError(err)) {
if (err.options.cause) logger?.(err.options.cause);
return err.toJson(res);
}
logger?.(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);
};
/**
* Middleware to handle 404 Not Found errors.
*
* This function creates an Express router that catches all requests to
* undefined routes and returns a JSON response with a 404 error.
*
* @param {string} [path='*'] - The route pattern to match (default: '*').
* @returns {Router} Express router instance handling 404 errors.
*
* @example
* app.use(notFound())
*/
const notFound = (path = "*") => (0, express.Router)().all(path, (req, res) => new NotFoundError(`Cannot ${req.originalUrl} on ${req.method.toUpperCase()}`).toJson(res));
/**
* Generates a permission object mapping subjects and actions to permission strings.
*
* @template A - List of actions that can be performed.
* @template S - List of subjects (resources) being acted upon.
* @template F - Optional object specifying which actions are allowed per subject.
*
* @param {Options<A, S, F>} options - The options object containing actions, subjects, and an optional filter.
* @returns {Readonly<PermissionMapping<A, S, F>>} A frozen object mapping subjects and actions to permission strings.
*
* @example
* const permissions = makePermission({
* actions: ['create', 'read', 'update', 'delete'] as const,
* subjects: ['user', 'post', 'comment'] as const,
* filter: {
* user: ['read'],
* post: ['create', 'update'],
* },
* });
* console.log(permissions.USER_CREATE);
*/
function makePermission(options) {
const { actions, subjects, filter } = options;
const map_data = subjects.flatMap((subject) => (filter?.[subject] ?? actions).map((action) => [`${subject}_${action}`.toUpperCase(), `${subject.toLowerCase()}:${action.toLowerCase()}`]));
return Object.freeze(Object.fromEntries(map_data));
}
//#endregion
//#region src/api-res.ts
/**
* ApiRes class for standardizing API responses
*/
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
*/
static 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
*/
static 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
*/
static paginated = (data, meta, message = "Data retrieved successfully") => new ApiRes({
...meta,
data
}, HttpStatus.OK, message);
};
//#endregion
//#region src/handler.ts
/**
* Processes the result of a route handler and sends the appropriate response.
*
* @param {unknown} result - The result of the handler, either an ApiRes instance or any other value.
* @param {Response} res - The Express response object.
*/
const handleResult = (result, res) => {
if (result instanceof ApiRes) result.toJson(res);
else if (result && result !== res) res.send(result);
};
/**
* Wraps an async route handler to manage errors and response handling.
*
* @param {Handler} callback - The route handler, which can return a value or a Promise.
* @returns {Handler} - A wrapped handler with error and result handling.
*
* @example
* // without type
* app.get('/example', handler(async () => await fetchData()));
* // with body type
* app.get('/example', handler<InputType<{name: string}>>(async req => await fetchData(req.body.name)));
* // with param type
* app.get('/example', handler<InputType<any, {name: string}>>(async req => await fetchData(req.param.name)));
* // with query type
* app.get('/example', handler<InputType<any, any, {name: string}>>(async req => await fetchData(req.query.name)));
*/
const 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);
}
};
/**
* @param clsOrInstance - The class constructor or an instance of the class.
* @param args - The arguments for the class constructor.
* @returns A proxied instance where all methods are wrapped with `async-handler`.
*
* @example
* class MyClass {
* async myMethod() {
* return 'Hello, World!';
* }
* }
* const instance = proxyWrapper(MyClass);
*
* app.get("/", instance.myMethod)
*/
const 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.");
}
});
};
//#endregion
exports.ApiRes = ApiRes;
exports.BadRequestError = BadRequestError;
exports.ConflictError = ConflictError;
exports.ContentTooLargeError = ContentTooLargeError;
exports.ForbiddenError = ForbiddenError;
exports.HttpError = HttpError;
exports.HttpStatus = HttpStatus;
exports.InternalServerError = InternalServerError;
exports.NotFoundError = NotFoundError;
exports.UnAuthorizedError = UnAuthorizedError;
exports.createHttpErrorClass = createHttpErrorClass;
exports.errorHandler = errorHandler;
exports.getErrorName = getErrorName;
exports.handler = handler;
exports.makePermission = makePermission;
exports.notFound = notFound;
exports.proxyWrapper = proxyWrapper;