arrow-express
Version:
Library to bootstrap express applications with zero configuration
314 lines (313 loc) • 8.76 kB
JavaScript
class AppConfigurator {
_prefix = "";
_controllers = [];
logRequests;
/**
* Create AppConfigurator
* @param expressApplication - express application
* @param logRequests - flag if requests should be logged, true by default
*/
constructor(logRequests = true) {
this.logRequests = logRequests;
}
/**
* Register prefix for all paths in application
* @param prefix - prefix string eg: 'api'
*/
prefix(prefix) {
this._prefix = prefix;
return this;
}
getPrefix() {
return this._prefix;
}
/**
* Register controller in application.
* @param controller - registered controller
*/
registerController(controller) {
this._controllers.push(controller);
return this;
}
/**
* Register list of controllers in application.
* @param controllers - controllers to register
*/
registerControllers(...controllers) {
controllers.forEach((controller) => this.registerController(controller));
return this;
}
getControllers() {
return this._controllers;
}
/**
* @description
* Build routes from registered controllers.
* It will create route configuration for each controller and its sub-controllers.
* The route configuration will include the path, method, and handler for each route.
* The handler will call the controller's handler and then the route's request handler.
* If the controller has sub-controllers, it will recursively build routes for them as well.
* @returns Array of route configurations built from registered controllers.
*/
buildRoutes() {
return this._controllers.reduce(
(routes, controller) => routes.concat(this.reduceController(controller, this._prefix)),
[]
);
}
reduceController(controller, path, handler) {
return controller.getRoutes().map((route) => {
const routeConfiguration = {
path: AppConfigurator.getRoutePath(path || "", controller.getPrefix(), route.getPath()),
method: route.getMethod(),
handler: async (res, req, context) => {
const currentContext = await handler?.(res, req, context) || context;
const controllerContext = await controller.getHandler()?.(res, req, currentContext);
return route.getRequestHandler()?.(res, req, controllerContext || currentContext);
}
};
return routeConfiguration;
}).concat(
controller.getControllers().reduce((subRoutes, subController) => {
return subRoutes.concat(
this.reduceController(subController, controller.getPrefix(), async (res, req, context) => {
const currentContext = await handler?.(res, req, context) || context;
return controller.getHandler()?.(res, req, currentContext);
})
);
}, [])
);
}
/**
* Get final route path
* @param paths - array of paths
* @private
*/
static getRoutePath(...paths) {
return paths.filter((path) => !!path).join(`/`);
}
}
function Application() {
return new AppConfigurator();
}
class ControllerConfiguration {
_prefix = "";
_controllers = [];
_routes = [];
_handler;
/**
* Register child controller in controller
* @param controller - controller to register
*/
registerController(controller) {
this._controllers.push(controller);
return this;
}
/**
* Register array of controllers in controller
* @param controllers - routes used in controller
*/
registerControllers(...controllers) {
controllers.forEach(this.registerController.bind(this));
return this;
}
/**
* Register route in controller
* @param route - route used in controller
*/
registerRoute(route) {
this._routes.push(route);
return this;
}
/**
* Register array of routes in controller
* @param routes - routes used in controller
*/
registerRoutes(...routes) {
routes.forEach(this.registerRoute.bind(this));
return this;
}
/**
* Register controller prefix which will be used by all routes
* @param prefix - eg: 'login'
*/
prefix(prefix) {
this._prefix = prefix;
return this;
}
/**
* Register controller handler which will be used by all routes
* @param handler - ControllerHandler function
*/
handler(handler) {
this._handler = handler;
return this;
}
getPrefix() {
return this._prefix;
}
getRoutes() {
return this._routes;
}
getControllers() {
return this._controllers;
}
getHandler() {
return this._handler;
}
}
function Controller() {
return new ControllerConfiguration();
}
class RouteConfigurator {
_method;
_path;
_handler;
/**
* Set method for route
* @param method - Method
*/
method(method) {
this._method = method || "get";
return this;
}
/**
* Register path of route alongside with prefix it is used to create full path
* @param path
*/
path(path) {
this._path = path;
return this;
}
/**
* Set request handler, here you can handle request
* @param handler - RouteHandler
*/
handler(handler) {
this._handler = handler;
return this;
}
getMethod() {
return this._method;
}
getPath() {
return this._path;
}
/**
* Get request handler function
* @return - function which is called by express application on request
*/
getRequestHandler() {
return this._handler;
}
}
function Route() {
return new RouteConfigurator();
}
class ConfigurationError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, ConfigurationError.prototype);
}
}
class RequestError extends Error {
response;
httpCode;
/**
* RequestError constructor
* @param httpCode - HTTP response code used by arrow-express default 500
* @param response - response body send on error
*/
constructor(httpCode, response) {
super("Wrong api response");
this.response = response;
this.httpCode = httpCode || 500;
Object.setPrototypeOf(this, RequestError.prototype);
}
}
class ExpressAdapterConfiguration {
_express;
_appConfigurator;
_configured;
constructor(express, appConfigurator) {
this._configured = false;
this._express = express;
this._appConfigurator = appConfigurator;
}
static expressRouteAsString(r) {
return `${Object.keys(r.route.methods)[0].toUpperCase()}:${r.route?.path}`;
}
registerRouteInExpress(routeConfiguration) {
if (!routeConfiguration.method || !routeConfiguration.handler) {
throw new ConfigurationError(
`${routeConfiguration.path} route is not properly configured, missing path, method or handler`
);
}
this._express[routeConfiguration.method](
`/${routeConfiguration.path}`,
this.createRequestHandler(routeConfiguration)
);
}
getExpressRoutesAsStrings() {
return this._express.router.stack.filter((r) => r.route).map(ExpressAdapterConfiguration.expressRouteAsString);
}
printExpressConfig() {
console.log("Routes registered by Express server:");
this.getExpressRoutesAsStrings().forEach((route) => console.log(route));
}
static canSendResponse(res) {
return !res.writableEnded;
}
createRequestHandler(routeConfiguration) {
return async (req, res) => {
try {
let context;
const response = await routeConfiguration.handler(req, res, context);
if (ExpressAdapterConfiguration.canSendResponse(res)) {
if (!res.statusCode) {
res.status(200);
}
res.send(response);
}
} catch (error) {
if (ExpressAdapterConfiguration.canSendResponse(res)) {
if (error instanceof RequestError) {
res.status(error.httpCode || 500).send(error.response || "Internal error");
} else {
res.status(500).send("Internal error");
}
}
}
};
}
/**
* Register controllers routes in express app
* @param printConfiguration - print express application routes enabled by default.
*/
configure(printConfiguration = true) {
if (this._configured) {
throw new ConfigurationError("Cannot configure application multiple times");
} else {
this._configured = true;
}
const routesConfigurations = this._appConfigurator.buildRoutes();
routesConfigurations.forEach((routeConfiguration) => this.registerRouteInExpress(routeConfiguration));
if (printConfiguration) {
this.printExpressConfig();
}
}
}
const ExpressAdapter = (express, appConfigurator) => {
return new ExpressAdapterConfiguration(express, appConfigurator);
};
export {
AppConfigurator,
Application,
Controller,
ControllerConfiguration,
ExpressAdapter,
RequestError,
Route,
RouteConfigurator
};
//# sourceMappingURL=index.js.map