UNPKG

@nodeboot/http-server

Version:

Node-Boot http server package. It provides a simple way to create HTTP servers using Node.js, with support for routing, middleware, and request handling.

281 lines 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HttpDriver = void 0; const cookie_1 = require("cookie"); const context_1 = require("@nodeboot/context"); const engine_1 = require("@nodeboot/engine"); const error_1 = require("@nodeboot/error"); const cors_1 = require("../cors"); class HttpDriver extends engine_1.NodeBootDriver { logger; router; middlewaresBefore = []; middlewaresAfter = []; globalErrorHandler; customErrorHandler; serverOptions; constructor(options) { super(); this.logger = options.logger; this.app = options.server; this.router = options.router; this.serverOptions = options.serverConfigs || {}; this.globalErrorHandler = new engine_1.GlobalErrorHandler(); this.app.on("request", this.requestHandler.bind(this)); this.app.on("error", err => { this.logger.error("HTTP Server Error:", err); }); } initialize() { // Init if needed, e.g. register global middleware for cookies, CORS, etc. } registerRoutes() { // routes registered dynamically via `registerAction` method } /** * Registers middleware that run before controller actions. */ registerMiddleware(middleware, _) { // Register a custom error Handler if (middleware.instance.onError) { this.customErrorHandler = middleware.instance; } // if its a regular middleware then register it as fastify preHandler hook else if (middleware.instance.use) { if (middleware.type === "before") { this.middlewaresBefore.push(middleware); } else { this.middlewaresAfter.push(middleware); } } } registerAction(actionMetadata, executeAction) { const method = actionMetadata.type.toUpperCase(); const route = context_1.ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute); this.router.on(method.toUpperCase(), route.toString(), async (req, res, params, store, searchParams) => { const start = process.hrtime(); this.logger.debug(`==> Incoming HTTP request: ${req.method} ${req.url} | ${req.socket.remoteAddress} | ${req.headers["user-agent"]}`); const action = { request: req, response: res, context: { store, params: params || {}, searchParams: searchParams || {}, }, }; try { if (actionMetadata.isAuthorizedUsed) { await this.checkAuthorization(req, res, actionMetadata); } // You can add authorization check here await executeAction(action); } catch (error) { await this.handleError(error, action, actionMetadata); } const [sec, nano] = process.hrtime(start); const ms = (sec * 1e3 + nano / 1e6).toFixed(2); this.logger.debug(`<== Outgoing HTTP response: ${req.method} ${req.url} ${res.statusCode} - ${ms}ms | ${req.socket.remoteAddress} | ${req.headers["user-agent"]}`); }); } async requestHandler(req, res) { try { // handle CORS preflight requests if configured if (this.serverOptions.cors) { const shouldContinue = await (0, cors_1.applyCorsHeaders)(req, res, this.serverOptions.cors.options); if (!shouldContinue) return; } // Parse JSON body if applicable if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") { try { req.body = await this.parseJsonBody(req); } catch (err) { res.statusCode = 400; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ error: "Invalid JSON body" })); return; } } // Run before middlewares await this.runMiddlewares(req, res, this.middlewaresBefore); // Match route const route = this.router.find(req.method?.toUpperCase() || "GET", req.url || ""); if (!route || !route.handler) { res.statusCode = 404; res.end("Not Found"); return; } // Prepare params and searchParams and run the route handler await route.handler(req, res, route.params, route.store, route.searchParams); // Run after middlewares await this.runMiddlewares(req, res, this.middlewaresAfter); } catch (error) { this.handleError(error, { request: req, response: res, context: { store: {}, params: {}, searchParams: {}, }, }); } } async runMiddlewares(req, res, middlewares) { for (const middleware of middlewares) { // If response is already sent (e.g. res.end() was called), break if (res.writableEnded || res.headersSent) return; // Call the middleware await this.callGlobalMiddleware(req, res, middleware, {}); } } async callGlobalMiddleware(request, response, middleware, payload) { if (request.url?.startsWith(this.routePrefix || "/")) { try { await middleware.instance.use({ request, response: response, }, payload); } catch (error) { await this.handleError(error, { request, response: response, }); } } } async checkAuthorization(request, response, actionMetadata) { if (!this.authorizationChecker) throw new error_1.AuthorizationCheckerNotDefinedError(); const action = { request, response: response }; try { const checkResult = await this.authorizationChecker.check(action, actionMetadata.authorizedRoles); if (!checkResult) { const error = actionMetadata.authorizedRoles.length === 0 ? new error_1.AuthorizationRequiredError(action.request.method ?? "GET", action.request.url ?? "/") : new error_1.AccessDeniedError(action.request.method ?? "GET", action.request.url ?? "/"); await this.handleError(error, action, actionMetadata); } } catch (error) { await this.handleError(error, action, actionMetadata); } } getParamFromRequest(action, param) { const req = action.request; switch (param.type) { case "body": return req.body; case "body-param": return req.body[param.name]; case "param": return action.context.params[param.name]; case "params": return action.context.params; case "query": return action.context.searchParams[param.name]; case "queries": return action.context.searchParams; case "header": return req.headers[param.name.toLowerCase()]; case "headers": return req.headers; case "cookie": return (0, cookie_1.parse)(req.headers.cookie || "")[param.name]; case "cookies": return (0, cookie_1.parse)(req.headers.cookie || ""); // TODO: Add more cases for session, files, etc. default: return undefined; } } async handleError(error, action, actionMetadata) { if (actionMetadata) { Object.keys(actionMetadata.headers).forEach(name => { action.response.setHeader(name, actionMetadata.headers[name]); }); } this.logger.error(error); action.response.statusCode = error.httpCode || 500; if (!error.handled && this.customErrorHandler) { await this.customErrorHandler.onError(error, action, actionMetadata); } else { delete error.handled; const parsedError = this.globalErrorHandler.handleError(error); action.response.setHeader("Content-Type", "application/json"); action.response.end(JSON.stringify(parsedError)); } } handleSuccess(result, action, actionMetadata) { const res = action.response; // Set status code from metadata or default 200 res.statusCode = actionMetadata.successHttpCode || 200; // Set headers if any Object.entries(actionMetadata.headers).forEach(([k, v]) => res.setHeader(k, v)); // Handle redirects if (actionMetadata.redirect) { res.statusCode = 302; res.setHeader("Location", actionMetadata.redirect); res.end(); return; } // Handle templates (implement with a templating lib) if (actionMetadata.renderedTemplate) { // For example, use eta or nunjucks here // res.end(renderTemplate(actionMetadata.renderedTemplate, result)); res.end("Template rendering not implemented"); return; } // Handle undefined, null, buffers, streams, etc. if (result === undefined) { res.statusCode = 404; res.end("Not Found"); return; } if (result === null) { res.statusCode = actionMetadata.nullResultCode || 204; res.end(); return; } if (Buffer.isBuffer(result)) { res.end(result); return; } if (typeof result === "string") { res.setHeader("Content-Type", "text/plain"); res.end(result); return; } if (typeof result === "object") { res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(result)); return; } res.end(String(result)); } async parseJsonBody(req) { return new Promise((resolve, reject) => { let data = ""; req.on("data", chunk => (data += chunk)); req.on("end", () => { try { resolve(JSON.parse(data || "{}")); } catch (err) { reject(new Error("Invalid JSON body")); } }); req.on("error", reject); }); } } exports.HttpDriver = HttpDriver; //# sourceMappingURL=HttpDriver.js.map