@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
JavaScript
"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