restifyx.js
Version:
Advanced API endpoint handler system with automatic documentation
423 lines • 18.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RestifyXApp = void 0;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const helmet_1 = __importDefault(require("helmet"));
const morgan_1 = __importDefault(require("morgan"));
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
const compression_1 = __importDefault(require("compression"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const logger_1 = require("../utils/logger");
const EndpointRegistry_1 = require("./EndpointRegistry");
const SwaggerGenerator_1 = require("./SwaggerGenerator");
const ApiError_1 = require("./errors/ApiError");
const config_1 = require("../utils/config");
const security_1 = require("../utils/security");
const performance_1 = require("../utils/performance");
const https_1 = __importDefault(require("https"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const hotReloader_1 = require("../utils/hotReloader");
class RestifyXApp {
constructor(options = {}) {
this.server = null;
this.isSetup = false;
this.shutdownHandlers = [];
this.hotReloader = null;
this.config = (0, config_1.resolveConfig)(options);
this.logger = (0, logger_1.setupLogger)(this.config.logging);
this.app = (0, express_1.default)();
this.endpointRegistry = new EndpointRegistry_1.EndpointRegistry(this.config);
this.swaggerGenerator = new SwaggerGenerator_1.SwaggerGenerator(this.config.swagger);
if (this.config.gracefulShutdown.enabled) {
this.setupGracefulShutdown();
}
}
/**
* Sets up the application by applying middleware and registering endpoints.
* This method is called automatically when the application is started.
* It can also be called manually if you want to set up the application without starting it.
* @returns {Promise<RestifyXApp>} The RestifyXApp instance.
* @throws {Error} Throws an error if the setup fails.
*/
async setup() {
try {
this.logger.info("Setting up Restifyx.js application...");
this.applyPreMiddleware();
this.applyBuiltInMiddleware();
this.applyPostMiddleware();
if (this.config.swagger.enabled) {
this.setupSwagger();
}
if (this.config.endpoints.autoRegister) {
await this.registerEndpoints();
}
if (this.config.hotReload.enabled) {
this.setupHotReloader();
}
this.applyErrorHandlingMiddleware();
this.isSetup = true;
this.logger.info("Restifyx.js application setup complete");
return this;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to setup Restifyx.js application: ${errorMessage}`, { error });
if (this.config.strictMode) {
throw error;
}
return this;
}
}
/**
Starts the application server.
This method is called automatically when the application is started.
It can also be called manually if you want to start the server without setting up the application.
@returns {Promise<Server | HttpsServer>} The server instance.
@throws {Error} Throws an error if the server fails to start.
*/
async start() {
return new Promise((resolve, reject) => {
try {
if (!this.isSetup) {
this.logger.warn("Application has not been set up. Running setup automatically.");
this.setup().catch(reject);
}
if (this.config.https.enabled) {
const httpsOptions = {
key: fs_1.default.readFileSync(path_1.default.resolve(process.cwd(), this.config.https.keyPath)),
cert: fs_1.default.readFileSync(path_1.default.resolve(process.cwd(), this.config.https.certPath)),
passphrase: this.config.https.passphrase,
};
this.server = https_1.default.createServer(httpsOptions, this.app);
this.server.listen(this.config.port, this.config.host, () => {
this.logger.info(`HTTPS API is now running on ${this.config.host}:${this.config.port}`);
this.logger.info(`Server URL: https://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`);
this.logServerInfo();
resolve(this.server);
});
}
else {
this.server = this.app.listen(this.config.port, this.config.host, () => {
this.logger.info(`API is now running on ${this.config.host}:${this.config.port}`);
this.logger.info(`Server URL: http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`);
this.logServerInfo();
resolve(this.server);
});
}
this.server.on("error", (err) => {
this.logger.error(`Server error: ${err.message}`, { error: err });
reject(err);
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to start the server: ${errorMessage}`, { error });
reject(error);
}
});
}
/**
* Stops the application server.
* This method is called automatically when the application is stopped.
* It can also be called manually if you want to stop the server without shutting down the application.
* @returns {Promise<void>} A promise that resolves when the server has stopped.
* @throws {Error} Throws an error if the server fails to stop.
*/
async stop() {
return new Promise(async (resolve, reject) => {
if (!this.server) {
this.logger.warn("Server is not running");
return resolve();
}
try {
if (this.hotReloader) {
this.hotReloader.stop();
this.hotReloader = null;
this.logger.info("Hot reloader stopped");
}
if (this.shutdownHandlers.length > 0) {
this.logger.info(`Executing ${this.shutdownHandlers.length} shutdown handlers...`);
await Promise.all(this.shutdownHandlers.map((handler) => handler()));
}
this.server.close((err) => {
if (err) {
this.logger.error(`Error stopping server: ${err.message}`, { error: err });
return reject(err);
}
this.logger.info("Server stopped successfully");
this.server = null;
resolve();
});
}
catch (error) {
this.logger.error(`Error during shutdown: ${error instanceof Error ? error.message : String(error)}`, { error });
reject(error);
}
});
}
async setupHotReloader() {
try {
this.logger.info("Setting up hot reloader...");
this.hotReloader = new hotReloader_1.HotReloader(this, this.config);
await this.hotReloader.start();
this.logger.info("Hot reloader started successfully");
}
catch (error) {
this.logger.error(`Failed to set up hot reloader: ${error instanceof Error ? error.message : String(error)}`, {
error,
});
if (this.config.strictMode) {
throw error;
}
}
}
/**
* Returns the Express application instance.
* @returns {Express}
*/
getApp() {
return this.app;
}
/**
* Returns the logger instance.
* @returns {Logger}
*/
getLogger() {
return this.logger;
}
/**
* Returns the server instance.
Returns the server instance.
@returns {Server | HttpsServer | null}
*/
getConfig() {
return this.config;
}
/**
* Returns the endpoint registry.
* @returns {EndpointRegistry}
*/
getEndpointRegistry() {
return this.endpointRegistry;
}
/**
* Registers a single endpoint.
* This method is used to register a single endpoint manually.
* @returns {void}
* @throws {Error} Throws an error if the endpoint registration fails.
*/
registerEndpoint(endpoint) {
this.endpointRegistry.registerSingleEndpoint(this.app, endpoint);
}
/**
* Registers all endpoints.
* This method is used to register all endpoints automatically.
* @returns {Promise<EndpointRegistrationResult>} The result of the endpoint registration.
* @throws {Error} Throws an error if the endpoint registration fails.
*/
async registerEndpoints() {
this.logger.info("Registering API Endpoints...");
const registrationResult = await this.endpointRegistry.registerEndpoints(this.app);
if (registrationResult.success) {
const endpointCount = registrationResult.endpoints.length;
this.logger.info(`Registered ${endpointCount} endpoint${endpointCount !== 1 ? "s" : ""} successfully`);
if (this.config.debug) {
this.logger.info("Available endpoints:");
registrationResult.endpoints.forEach((endpoint) => {
this.logger.info(`${endpoint.method} ${endpoint.path} - ${endpoint.name || "Unnamed endpoint"}`);
});
}
}
else {
this.logger.error(`Failed to register all endpoints: ${registrationResult.error}`);
if (registrationResult.failedEndpoints && registrationResult.failedEndpoints.length > 0) {
this.logger.error("Failed endpoints:");
registrationResult.failedEndpoints.forEach(({ path, error }) => {
this.logger.error(`- ${path}: ${error}`);
});
}
if (this.config.strictMode) {
throw new Error(`Failed to register all endpoints: ${registrationResult.error}`);
}
}
return registrationResult;
}
/**
* Adds a shutdown handler.
* This method is used to add a custom shutdown handler that will be called when the application is shutting down.
* @param handler - The shutdown handler function to add.
*/
addShutdownHandler(handler) {
this.shutdownHandlers.push(handler);
this.logger.debug("Added shutdown handler");
}
/**
* Sets up a health check endpoint.
* This method is used to set up a custom health check endpoint.
* @param path - The path for the health check endpoint.
* @param handler - The handler function for the health check endpoint.
*/
setHealthCheck(path, handler) {
this.app.get(path, handler);
this.logger.info(`Health check endpoint set up at ${path}`);
}
setupSwagger() {
this.logger.info("Setting up Swagger documentation...");
this.swaggerGenerator.setup(this.app, this.endpointRegistry.getRegisteredEndpoints());
}
applyPreMiddleware() {
if (this.config.middleware.before && this.config.middleware.before.length > 0) {
this.logger.debug("Applying pre-middleware...");
this.config.middleware.before.forEach((middleware) => {
this.app.use(middleware);
});
}
}
applyBuiltInMiddleware() {
if (this.config.security.trustProxy) {
this.app.set("trust proxy", true);
}
if (this.config.cors.enabled) {
this.logger.debug("Enabling CORS...");
this.app.use((0, cors_1.default)(this.config.cors.options));
}
if (this.config.security.helmet) {
this.logger.debug("Enabling Helmet...");
this.app.use((0, helmet_1.default)(this.config.security.helmetOptions));
}
if (this.config.security.additionalHeaders) {
this.logger.debug("Enabling additional security headers...");
this.app.use((0, security_1.securityHeaders)());
}
if (this.config.bodyParser.json) {
this.logger.debug("Enabling JSON body parser...");
this.app.use(express_1.default.json(this.config.bodyParser.jsonOptions));
}
if (this.config.bodyParser.urlencoded) {
this.logger.debug("Enabling URL-encoded body parser...");
this.app.use(express_1.default.urlencoded(this.config.bodyParser.urlencodedOptions));
}
if (this.config.cookies.enabled) {
this.logger.debug("Enabling cookie parser...");
this.app.use((0, cookie_parser_1.default)(this.config.cookies.secret));
}
if (this.config.compression.enabled) {
this.logger.debug("Enabling compression...");
this.app.use((0, compression_1.default)(this.config.compression.options));
}
if (this.config.logging.morgan) {
this.logger.debug("Enabling Morgan request logger...");
this.app.use((0, morgan_1.default)(this.config.logging.morganFormat, {
stream: logger_1.stream,
...this.config.logging.morganOptions,
}));
}
if (this.config.performance.monitor) {
this.logger.debug("Enabling performance monitoring...");
this.app.use((0, performance_1.performanceMonitor)());
}
if (this.config.rateLimiting.enabled) {
this.logger.debug("Enabling rate limiting...");
this.app.use((0, express_rate_limit_1.default)({
windowMs: this.config.rateLimiting.windowMs,
max: this.config.rateLimiting.max,
message: {
status: "error",
statusCode: 429,
message: this.config.rateLimiting.message,
errorCode: "RATE_LIMIT_EXCEEDED",
timestamp: Date.now(),
},
standardHeaders: this.config.rateLimiting.headers,
legacyHeaders: false,
skip: this.config.rateLimiting.skip,
}));
}
}
applyPostMiddleware() {
if (this.config.middleware.after && this.config.middleware.after.length > 0) {
this.logger.debug("Applying post-middleware...");
this.config.middleware.after.forEach((middleware) => {
this.app.use(middleware);
});
}
}
applyErrorHandlingMiddleware() {
this.app.use((req, res) => {
this.logger.warn(`Route not found: ${req.method} ${req.originalUrl}`);
res.status(404).json({
status: "error",
statusCode: 404,
message: `Route not found: ${req.method} ${req.originalUrl}`,
errorCode: "NOT_FOUND",
timestamp: Date.now(),
});
});
const errorHandler = (err, req, res, next) => {
if (err instanceof ApiError_1.ApiError) {
this.logger.warn(`API Error: ${err.message}`, { error: err });
res.status(err.statusCode).json({
status: "error",
statusCode: err.statusCode,
message: err.message,
errorCode: err.errorCode,
details: err.details,
timestamp: err.timestamp || Date.now(),
});
return;
}
this.logger.error(`Unhandled error: ${err.message}`, { error: err });
res.status(500).json({
status: "error",
statusCode: 500,
message: "Internal server error",
errorCode: "INTERNAL_ERROR",
error: this.config.debug ? err.message : undefined,
stack: this.config.debug ? err.stack : undefined,
timestamp: Date.now(),
});
return;
};
this.app.use(errorHandler);
}
setupGracefulShutdown() {
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
signals.forEach((signal) => {
process.on(signal, async () => {
this.logger.info(`Received ${signal}, gracefully shutting down...`);
try {
await this.stop();
this.logger.info("Graceful shutdown completed");
process.exit(0);
}
catch (error) {
this.logger.error("Error during graceful shutdown", { error });
process.exit(1);
}
});
});
this.logger.info("Graceful shutdown handlers configured");
}
logServerInfo() {
if (this.config.swagger.enabled) {
const protocol = this.config.https.enabled ? "https" : "http";
this.logger.info(`Swagger documentation: ${protocol}://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}${this.config.swagger.path}`);
}
if (this.config.debug) {
this.logger.info("=====================");
this.logger.info("DEBUGGING ENABLED");
this.logger.info("=====================");
}
this.logger.info(`Environment: ${process.env.NODE_ENV || "development"}`);
this.logger.info(`Node.js version: ${process.version}`);
const memoryUsage = process.memoryUsage();
this.logger.debug(`Memory usage: RSS ${Math.round(memoryUsage.rss / 1024 / 1024)}MB, Heap ${Math.round(memoryUsage.heapUsed / 1024 / 1024)}/${Math.round(memoryUsage.heapTotal / 1024 / 1024)}MB`);
}
}
exports.RestifyXApp = RestifyXApp;
//# sourceMappingURL=RestifyXApp.js.map