From 2200aadca77f85e435c0ca618468568a0e3aeccc Mon Sep 17 00:00:00 2001 From: Joao Castro Date: Sat, 6 Jul 2024 00:00:31 -0300 Subject: [PATCH] [feat] - Implementing Error handler and loadFile function --- .npmignore | 4 + CHANGELOG.md | 10 +++ README.md | 46 ++++++++--- README.pt-br.md | 47 +++++++++--- package.json | 4 +- .../ErrorHandler/errorHandler.interface.ts | 6 ++ src/main/ErrorHandler/errorHandler.ts | 42 ++++++++++ src/main/ErrorHandler/index.ts | 3 + src/main/Logger/logger.interface.ts | 6 +- src/main/Logger/logger.ts | 14 ++-- src/main/Router/router.ts | 23 ++++-- src/main/Server/server.interface.ts | 1 + src/main/Server/server.ts | 76 +++++++++---------- src/tests/ErrorHandler.test.ts | 51 +++++++++++++ src/tests/Logger.test.ts | 7 +- src/tests/Server.test.ts | 32 ++------ src/typings/errorHandler.d.ts | 6 ++ src/typings/logger.d.ts | 1 + src/typings/server.d.ts | 2 + 19 files changed, 274 insertions(+), 107 deletions(-) create mode 100644 src/main/ErrorHandler/errorHandler.interface.ts create mode 100644 src/main/ErrorHandler/errorHandler.ts create mode 100644 src/main/ErrorHandler/index.ts create mode 100644 src/tests/ErrorHandler.test.ts create mode 100644 src/typings/errorHandler.d.ts create mode 100644 src/typings/logger.d.ts diff --git a/.npmignore b/.npmignore index fcb1ae9..315cfd8 100644 --- a/.npmignore +++ b/.npmignore @@ -25,3 +25,7 @@ node_modules/ .editorconfig .gitignore .npmrc + +src/static/ + +src/dev.ts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e8fcbbe..c54b673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - yyyy-mm-dd +## [0.2.0] - 2024-07-06 + +### Added + +- Implementing Error handler and loadFile function + +### Fixed + +- Adjusting the serveStaticFiles function + ## [0.1.2] - 2024-07-05 ### Fixed diff --git a/README.md b/README.md index ffc1239..f629302 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,33 @@ simpler.router.addRoute("/static", ["GET"], (_req, res) => { }); ``` +### Loading Files + +You can load files with simpler with the function `loadFile`, it receives a res and the relative path to the file. + +Below you'll find an example of how to use it. + +```typescript +simpler.router.addRoute("/static-page", ["GET"], (_req, res) => { + simpler.loadFile(res, "./static/teste.html"); +}); +``` + +### Handling errors + +You can have custom error handlers using the function `errorHandler.setCustomErrorHandler`. It receives a function that will have a res and an error as parameters. + +Below you'll find an example of how to use it. + +```typescript +simpler.errorHandler.setCustomErrorHandler( + (res: ServerResponse, error: Error) => { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ message: "Custom Error", error: error.message })); + } +); +``` + ### Starting the Server To start the server, use the `listen` method. You can specify the port number, which defaults to 3000 if not provided. @@ -166,18 +193,17 @@ simpler.router.addRoute( simpler.router.addStaticDirectory("static"); -simpler.router.addRoute("/static", ["GET"], (_req, res) => { - const testePath = path.join(__dirname, "static", "teste.html"); - readFile(testePath, (err, data) => { - if (err) { - simpler.response(res, 500, "text/plain", "500 Internal Server Error"); - return; - } - - simpler.response(res, 200, "text/html", data); - }); +simpler.router.addRoute("/static-page", ["GET"], (_req, res) => { + simpler.loadFile(res, "./static/teste.html"); }); +simpler.errorHandler.setCustomErrorHandler( + (res: ServerResponse, error: Error) => { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ message: "Custom Error", error: error.message })); + } +); + simpler.listen(3001); ``` diff --git a/README.pt-br.md b/README.pt-br.md index 04a0415..5d48ec2 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -119,6 +119,34 @@ simpler.router.addRoute("/static", ["GET"], (_req, res) => { }); ``` +### Carregando arquivos + +Você pode carregar arquivos com simpler utilizando a função `loadFile`, a função recebe um res a o caminho relativo para o arquivo. + +Abaixo você encontrará um exemplo de como utilizar-la. + +```typescript +simpler.router.addRoute("/static-page", ["GET"], (_req, res) => { + simpler.loadFile(res, "./static/teste.html"); +}); +``` + +### Lidando com Erros + +Você pode lidar com erros de maneira customizada utilizando a função `errorHandler.setCustomErrorHandler`. A função recebe uma função como parâmetro que receberá res e error como parâmetros. + +Abaixo você encontrará um exemplo de como utilizar-la. + +```typescript +simpler.errorHandler.setCustomErrorHandler( + (res: ServerResponse, error: Error) => { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ message: "Custom Error", error: error.message })); + } +); +``` + + ### Iniciando o Servidor Para iniciar o servidor, use o método `listen`. Você pode especificar o número da porta, que por padrão é 3000 se não for fornecido. @@ -166,18 +194,17 @@ simpler.router.addRoute( simpler.router.addStaticDirectory("static"); -simpler.router.addRoute("/static", ["GET"], (_req, res) => { - const testePath = path.join(__dirname, "static", "teste.html"); - readFile(testePath, (err, data) => { - if (err) { - simpler.response(res, 500, "text/plain", "500 Internal Server Error"); - return; - } - - simpler.response(res, 200, "text/html", data); - }); +simpler.router.addRoute("/static-page", ["GET"], (_req, res) => { + simpler.loadFile(res, "./static/teste.html"); }); +simpler.errorHandler.setCustomErrorHandler( + (res: ServerResponse, error: Error) => { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ message: "Custom Error", error: error.message })); + } +); + simpler.listen(3001); ``` diff --git a/package.json b/package.json index 23eff5a..717fd56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simpler-server", - "version": "0.1.2", + "version": "0.2.0", "description": "Simpler is a simple Node.js server, designed to rapidly provide a base to start your server projects.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -16,7 +16,7 @@ "type": "git", "url": "git@github.com:JooLuiz/simpler.git" }, - "keywords": [], + "keywords": ["Node Server"], "author": "João Luiz de Castro (https://github.com/JooLuiz)", "license": "ISC", "devDependencies": { diff --git a/src/main/ErrorHandler/errorHandler.interface.ts b/src/main/ErrorHandler/errorHandler.interface.ts new file mode 100644 index 0000000..50d5489 --- /dev/null +++ b/src/main/ErrorHandler/errorHandler.interface.ts @@ -0,0 +1,6 @@ +interface IErrorHandler { + handleError: HandleErrorFunction; + setCustomErrorHandler: SetCustomErrorHandlerFunction; +} + +export default IErrorHandler; diff --git a/src/main/ErrorHandler/errorHandler.ts b/src/main/ErrorHandler/errorHandler.ts new file mode 100644 index 0000000..85a2815 --- /dev/null +++ b/src/main/ErrorHandler/errorHandler.ts @@ -0,0 +1,42 @@ +import { ServerResponse } from "http"; +import type ILogger from "../Logger/logger.interface"; +import type IErrorHandler from "./errorHandler.interface"; + +import Logger from "../Logger"; + +class ErrorHandler implements IErrorHandler { + private logger: ILogger; + + constructor(isVerbose: boolean) { + this.logger = new Logger(isVerbose); + } + + private defaultErrorHandler: HandleErrorFunction = ( + res: ServerResponse, + error: Error + ) => { + this.logger.error( + `An error happening when processing the callback: ${error.message}` + ); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ message: "Internal Server Error", error: error.message }) + ); + }; + + private customErrorHandler?: HandleErrorFunction; + + public handleError(res: ServerResponse, error: Error): void { + if (this.customErrorHandler) { + this.customErrorHandler(res, error); + } else { + this.defaultErrorHandler(res, error); + } + } + + public setCustomErrorHandler(handler: HandleErrorFunction): void { + this.customErrorHandler = handler; + } +} + +export default ErrorHandler; diff --git a/src/main/ErrorHandler/index.ts b/src/main/ErrorHandler/index.ts new file mode 100644 index 0000000..3c7b0f2 --- /dev/null +++ b/src/main/ErrorHandler/index.ts @@ -0,0 +1,3 @@ +import ErrorHandler from "./errorHandler"; + +export default ErrorHandler \ No newline at end of file diff --git a/src/main/Logger/logger.interface.ts b/src/main/Logger/logger.interface.ts index 5ae5c4b..15a03fb 100644 --- a/src/main/Logger/logger.interface.ts +++ b/src/main/Logger/logger.interface.ts @@ -1,7 +1,7 @@ interface ILogger { - log: (message: string) => void; - logIfVerbose: (message: string, isVerbose: boolean) => void; - error: (message: string) => void; + log: LoggerFunction; + logIfVerbose: LoggerFunction; + error: LoggerFunction; } export default ILogger; diff --git a/src/main/Logger/logger.ts b/src/main/Logger/logger.ts index 9ac4613..a10d2a4 100644 --- a/src/main/Logger/logger.ts +++ b/src/main/Logger/logger.ts @@ -1,19 +1,23 @@ import ILogger from "./logger.interface"; class Logger implements ILogger { - constructor() {} + private isVerbose: boolean; - log(message: string) { + constructor(isVerbose: boolean) { + this.isVerbose = isVerbose; + } + + public log(message: string) { console.log(message); } - logIfVerbose(message: string, isVerbose: boolean) { - if (isVerbose) { + public logIfVerbose(message: string) { + if (this.isVerbose) { this.log(message); } } - error(message: string) { + public error(message: string) { console.error(message); } } diff --git a/src/main/Router/router.ts b/src/main/Router/router.ts index febbc9d..d73cf00 100644 --- a/src/main/Router/router.ts +++ b/src/main/Router/router.ts @@ -6,7 +6,11 @@ class Router implements IRouter { constructor() {} - addRoute(url: string, method: METHODS[], callback: RouteCallback): void { + public addRoute( + url: string, + method: METHODS[], + callback: RouteCallback + ): void { this.routes.push({ url, method, @@ -14,13 +18,16 @@ class Router implements IRouter { }); } - addRoutes(routes: [Route]) { + public addRoutes(routes: [Route]) { routes.forEach((route) => { this.addRoute(route.url, route.method, route.callback); }); } - getRoute(url: string | undefined, method: string | undefined): Route | null { + public getRoute( + url: string | undefined, + method: string | undefined + ): Route | null { if (!url || !method) { return null; } @@ -45,7 +52,7 @@ class Router implements IRouter { return route; } - getPathVariables( + public getPathVariables( reqUrl: string | undefined, routeUrl: string ): Record { @@ -66,7 +73,7 @@ class Router implements IRouter { return pathVariables; } - getQueryParams(url: string | undefined): Record { + public getQueryParams(url: string | undefined): Record { if (!url) return {}; const queryParams: Record = {}; @@ -83,17 +90,17 @@ class Router implements IRouter { return queryParams; } - addStaticDirectory(directory: string) { + public addStaticDirectory(directory: string) { this.staticDirs.push(directory); } - addStaticDirectories(directories: string[]) { + public addStaticDirectories(directories: string[]) { directories.forEach((dir) => { this.addStaticDirectory(dir); }); } - getStaticDirs() { + public getStaticDirs() { return this.staticDirs; } } diff --git a/src/main/Server/server.interface.ts b/src/main/Server/server.interface.ts index d497941..284cbf4 100644 --- a/src/main/Server/server.interface.ts +++ b/src/main/Server/server.interface.ts @@ -2,6 +2,7 @@ interface IServer { handleRequest: HandleRequestFunction; listen: ListenFunction; response: ResponseFunction; + loadFile: LoadFileFunction; } export default IServer; diff --git a/src/main/Server/server.ts b/src/main/Server/server.ts index 3d7bb25..6536f73 100644 --- a/src/main/Server/server.ts +++ b/src/main/Server/server.ts @@ -1,26 +1,31 @@ import http, { IncomingMessage, ServerResponse } from "http"; + import type IServer from "./server.interface"; import type IRouter from "../Router/router.interface"; import type ILogger from "../Logger/logger.interface"; +import type IErrorHandler from "../ErrorHandler/errorHandler.interface"; + import Router from "../Router"; import Logger from "../Logger"; +import ErrorHandler from "../ErrorHandler"; + import path from "path"; import { fileTypes } from "../../utils/consts"; import { readFile } from "fs/promises"; class Server implements IServer { private httpServer: http.Server | undefined; - private isVerbose: boolean; public router: IRouter; private logger: ILogger; + public errorHandler: IErrorHandler; constructor(isVerbose: boolean = false) { this.router = new Router(); - this.logger = new Logger(); - this.isVerbose = isVerbose; + this.logger = new Logger(isVerbose); + this.errorHandler = new ErrorHandler(isVerbose); } - handleRequest( + public handleRequest( req: IncomingMessage, res: ServerResponse, route: Route, @@ -37,15 +42,10 @@ class Server implements IServer { req.on("end", async () => { try { - this.logger.logIfVerbose(`Request Body: ${body}`, this.isVerbose); + this.logger.logIfVerbose(`Request Body: ${body}`); route.callback(req, res, body, pathVariables, queryParams); } catch (err: unknown) { - this.logger.error( - `An error happening when processing the callback: ${ - (err as Error).message - }` - ); - this.errorResponse(res, err as Error); + this.errorHandler.handleError(res, err as Error); } }); } @@ -54,16 +54,14 @@ class Server implements IServer { const srvr = http.createServer( async (req: IncomingMessage, res: ServerResponse) => { try { + console.log(req.url); const isStaticPage = await this.serveStaticFiles(req, res); if (isStaticPage) { return; } const route = this.router.getRoute(req.url, req.method); if (!route) { - this.logger.logIfVerbose( - `Route ${req.url} not Found`, - this.isVerbose - ); + this.logger.logIfVerbose(`Route ${req.url} not Found`); this.response( res, 404, @@ -79,26 +77,18 @@ class Server implements IServer { ); this.logger.logIfVerbose( - `Path Variables: ${JSON.stringify(pathVariables)}`, - this.isVerbose + `Path Variables: ${JSON.stringify(pathVariables)}` ); const queryParams = this.router.getQueryParams(req.url); this.logger.logIfVerbose( - `Query Params: ${JSON.stringify(queryParams)}`, - this.isVerbose + `Query Params: ${JSON.stringify(queryParams)}` ); this.handleRequest(req, res, route, pathVariables, queryParams); } catch (err: unknown) { - this.logger.error( - `An error happening when getting the route: ${ - (err as Error).message - }` - ); - - this.errorResponse(res, err as Error); + this.errorHandler.handleError(res, err as Error); } } ); @@ -106,7 +96,7 @@ class Server implements IServer { this.httpServer = srvr; } - listen(port?: number) { + public listen(port?: number) { if (!this.httpServer) { this.createServer(); } @@ -134,29 +124,42 @@ class Server implements IServer { const staticDirs = this.router.getStaticDirs(); for (const dir of staticDirs) { - const publicDirectory = path.join(__dirname, `../${dir}`); + const publicDirectory = path.join(__dirname, "../../", dir); const filePath = path.join( publicDirectory, req.url === "/" ? "index.html" : req.url || "" ); - this.logger.logIfVerbose(`Static File Path: ${filePath}`, this.isVerbose); + this.logger.logIfVerbose(`Static File Path: ${filePath}`); const extname = String(path.extname(filePath)).toLowerCase(); const contentType = fileTypes[extname as FILE_EXTENSIONS] || "application/octet-stream"; try { const content = await readFile(filePath); this.response(res, 200, contentType, content); + this.logger.logIfVerbose(`Loaded Static File:${filePath}`); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { - this.logger.logIfVerbose((error as Error).message, this.isVerbose); + this.logger.logIfVerbose((error as Error).message); } } } return false; } - response( + public async loadFile(res: ServerResponse, urlFile: string) { + const filePath = path.join(__dirname, "../../", urlFile); + console.log("load file file path", filePath); + try { + const content = await readFile(filePath); + this.response(res, 200, "text/html", content); + return; + } catch (error) { + this.errorHandler.handleError(res, error as Error); + } + } + + public response( res: ServerResponse, status: number, contentType: string, @@ -165,17 +168,6 @@ class Server implements IServer { res.writeHead(status, { "Content-Type": contentType }); res.end(message); } - - private errorResponse(res: ServerResponse, err: Error) { - this.response( - res, - 500, - "application/json", - JSON.stringify({ - message: (err as Error).message || "Something went wrong", - }) - ); - } } export default Server; diff --git a/src/tests/ErrorHandler.test.ts b/src/tests/ErrorHandler.test.ts new file mode 100644 index 0000000..a2043e4 --- /dev/null +++ b/src/tests/ErrorHandler.test.ts @@ -0,0 +1,51 @@ +import { ServerResponse } from "http"; +import Logger from "../main/Logger"; +import ErrorHandler from "../main/ErrorHandler"; + +jest.mock("../main/Logger"); + +describe("ErrorHandler", () => { + let errorHandler: ErrorHandler; + let mockResponse: Partial; + let mockEnd: jest.Mock; + let mockWriteHead: jest.Mock; + let mockLogger: jest.Mocked; + let mockErrorHandler: jest.Mock; + + beforeEach(() => { + mockEnd = jest.fn(); + mockWriteHead = jest.fn(); + mockResponse = { + writeHead: mockWriteHead, + end: mockEnd, + }; + mockLogger = new Logger(true) as jest.Mocked; + errorHandler = new ErrorHandler(true); + }); + + it("should handle error using default error handler", () => { + const error = new Error("Test Error"); + + errorHandler.handleError(mockResponse as ServerResponse, error); + + expect(mockWriteHead).toHaveBeenCalledWith(500, { + "Content-Type": "application/json", + }); + expect(mockEnd).toHaveBeenCalledWith( + JSON.stringify({ message: "Internal Server Error", error: "Test Error" }) + ); + }); + + it("should handle error using custom error handler if set", () => { + const error = new Error("Test Error"); + mockErrorHandler = jest.fn(); + errorHandler.setCustomErrorHandler(mockErrorHandler); + + errorHandler.handleError(mockResponse as ServerResponse, error); + + expect(mockErrorHandler).toHaveBeenCalledWith(mockResponse, error); + expect(mockLogger.error).not.toHaveBeenCalled(); + expect(mockWriteHead).not.toHaveBeenCalled(); + expect(mockEnd).not.toHaveBeenCalled(); + }); +}); diff --git a/src/tests/Logger.test.ts b/src/tests/Logger.test.ts index 978dc18..b5c24f8 100644 --- a/src/tests/Logger.test.ts +++ b/src/tests/Logger.test.ts @@ -6,7 +6,7 @@ describe("Logger", () => { let consoleErrorSpy: jest.SpyInstance; beforeEach(() => { - logger = new Logger(); + logger = new Logger(false); consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {}); consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); }); @@ -22,14 +22,15 @@ describe("Logger", () => { }); test("logIfVerbose should call log if isVerbose is true", () => { + logger = new Logger(true); const message = "Verbose log message"; - logger.logIfVerbose(message, true); + logger.logIfVerbose(message); expect(consoleLogSpy).toHaveBeenCalledWith(message); }); test("logIfVerbose should not call log if isVerbose is false", () => { const message = "Non-verbose log message"; - logger.logIfVerbose(message, false); + logger.logIfVerbose(message); expect(consoleLogSpy).not.toHaveBeenCalled(); }); diff --git a/src/tests/Server.test.ts b/src/tests/Server.test.ts index 15d430e..f447d77 100644 --- a/src/tests/Server.test.ts +++ b/src/tests/Server.test.ts @@ -1,7 +1,5 @@ import http, { IncomingMessage, ServerResponse } from "http"; -import Server from "../main/Server"; // Ajuste o caminho conforme necessário -import { readFile } from "fs/promises"; -import path from "path"; +import Server from "../main/Server"; jest.mock("http"); jest.mock("fs/promises"); @@ -10,6 +8,8 @@ describe("Server", () => { let server: Server; let mockRequest: Partial; let mockResponse: Partial; + let mockEnd: jest.Mock; + let mockWriteHead: jest.Mock; beforeEach(() => { server = new Server(true); @@ -31,6 +31,11 @@ describe("Server", () => { writeHead: jest.fn(), end: jest.fn(), }; + mockWriteHead = jest.fn(); + mockResponse = { + writeHead: mockWriteHead, + end: mockEnd, + }; }); afterEach(() => { @@ -48,27 +53,6 @@ describe("Server", () => { expect(listenMock).toHaveBeenCalledWith(port, expect.any(Function)); }); - it("should serve static files", async () => { - (readFile as jest.Mock).mockResolvedValue("file content"); - - const req = { url: "/test.txt" } as IncomingMessage; - const res = mockResponse as ServerResponse; - const staticDirsSpy = jest - .spyOn(server.router, "getStaticDirs") - .mockReturnValue(["static"]); - const filePath = path.join(__dirname, "../main/static/test.txt"); - - const result = await server.serveStaticFiles(req, res); - - expect(result).toBe(true); - expect(readFile).toHaveBeenCalledWith(filePath); - expect(mockResponse.writeHead).toHaveBeenCalledWith(200, { - "Content-Type": "application/octet-stream", - }); - expect(mockResponse.end).toHaveBeenCalledWith("file content"); - staticDirsSpy.mockRestore(); - }); - it("should handle request and call route callback", () => { const route: Route = { url: "/test", diff --git a/src/typings/errorHandler.d.ts b/src/typings/errorHandler.d.ts new file mode 100644 index 0000000..61c4981 --- /dev/null +++ b/src/typings/errorHandler.d.ts @@ -0,0 +1,6 @@ +import { ServerResponse } from "http"; + +declare global { + type HandleErrorFunction = (res: ServerResponse, error: Error) => void; + type SetCustomErrorHandlerFunction = (handler: CustomErrorHandler) => void; +} diff --git a/src/typings/logger.d.ts b/src/typings/logger.d.ts new file mode 100644 index 0000000..d41c964 --- /dev/null +++ b/src/typings/logger.d.ts @@ -0,0 +1 @@ +type LoggerFunction = (message: string) => void; diff --git a/src/typings/server.d.ts b/src/typings/server.d.ts index c1ad132..6195b55 100644 --- a/src/typings/server.d.ts +++ b/src/typings/server.d.ts @@ -21,4 +21,6 @@ declare global { contentType: string, message: T ) => void; + + type LoadFileFunction = (res: ServerResponse, urlFile: string) => void; }