From 2e3f147fb4de6ffabcae6dfd61eab16a0000975b Mon Sep 17 00:00:00 2001 From: yldrmzffr Date: Mon, 6 May 2024 01:41:26 +0300 Subject: [PATCH] feat(middleware): add support for function-based middleware Implemented function-based middleware support to enhance route processing capabilities in the Muzu framework. Middleware functions can be easily attached to routes using the new @Middleware decorator. --- README.md | 53 +++++++++++ lib/controller/controller-factory.ts | 3 +- lib/handlers/request-handler.ts | 15 +++- lib/index.ts | 5 ++ lib/interfaces/index.ts | 2 + lib/middleware/middleware.ts | 19 ++++ package-lock.json | 4 +- package.json | 2 +- test/middleware.test.ts | 126 +++++++++++++++++++++++++++ 9 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 lib/middleware/middleware.ts create mode 100644 test/middleware.test.ts diff --git a/README.md b/README.md index e59ac7f..d346b05 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,59 @@ Inside the `login` method, you can implement the necessary logic to handle the i The server is started and set to listen on port 8080 using the `listen` method of the `MuzuServer` instance. +## Middleware + +Muzu supports middleware functions that can be applied to routes to execute specific logic before the route handler. Middleware functions can be used for tasks such as logging, authentication, and data validation. +`@Middleware` decorator can be used to apply middleware functions to specific routes. Let's see an example: + +```typescript +import {MuzuServer, Request, Response} from 'muzu'; +import {HttpException} from "./http.exception"; + +// Create a new instance of the MuzuServer +const app = new MuzuServer(); +const {Post, Controller, Middleware} = app; + +// Create a Middleware Function +function LoggerMiddleware(req: Request) { + console.log(`Request Received: ${req.url}`); +} + +// Create a Middleware Function +function AuthMiddleware(req: Request) { + const {token} = req.headers; + + if (!token) { + throw new HttpException('Unauthorized', 401); + } + + const user = getUserFromToken(token); + + // You can attact the custom data to the request object + req.user = user; +} + +@Controller('auth') +class TestController { + + @Post('login') + @Middleware(AuthMiddleware, LoggerMiddleware) // Apply Middleware to the 'login' route + login(req: Request, res: Response) { + const {user} = req; // Access the objects attached by the middleware + + // Implement your login logic here + + return { + status: true + }; + } + +} + +// Start the server and listen on port 8080 +app.listen(8080); +``` + ## Exception Handling Muzu provides a comprehensive exception handling mechanism. You can create custom exception classes by extending the `HttpException` class and utilize them within your application. Let's see an example: diff --git a/lib/controller/controller-factory.ts b/lib/controller/controller-factory.ts index 598d349..a589952 100644 --- a/lib/controller/controller-factory.ts +++ b/lib/controller/controller-factory.ts @@ -16,7 +16,7 @@ export class ControllerFactory { const routeHandler = target.prototype[property]; const method = Reflect.getMetadata('method', routeHandler); const url = Reflect.getMetadata('url', routeHandler); - + const middlewares = Reflect.getMetadata('middlewares', routeHandler); if (!method || !url) return; const fullPath = pathLib.join(path, url); @@ -25,6 +25,7 @@ export class ControllerFactory { method, url: fullPath, handler: routeHandler.bind(target.prototype), + middlewares, } as Route; }); diff --git a/lib/handlers/request-handler.ts b/lib/handlers/request-handler.ts index 5763acf..d089ab8 100644 --- a/lib/handlers/request-handler.ts +++ b/lib/handlers/request-handler.ts @@ -5,6 +5,7 @@ import {NotFoundException} from '../exceptions/not-found.exception'; import {getRequestBody, parseQueryParams} from '../utils'; import {MuzuException} from '../exceptions/muzu.exception'; import {BadRequestException} from '../exceptions/bad-request.exception'; + export class RequestHandler { private routeManager: RouteManager; @@ -29,7 +30,7 @@ export class RequestHandler { req.params = parseQueryParams(url!); - const route = await this.routeManager.find(path, method); + const route = this.routeManager.find(path, method); if (!route) { throw new NotFoundException(`Route ${method} ${path} not found`, { @@ -39,14 +40,22 @@ export class RequestHandler { } try { - const body = await getRequestBody(req); - req.body = body; + req.body = await getRequestBody(req); } catch (error) { console.log('🚨 Error parsing body', error); const err = error as MuzuException; throw new BadRequestException('Error parsing body', err.details); } + if (route.middlewares) { + for (const middleware of route.middlewares) { + const result = middleware(req, res); + if (result instanceof Promise) { + await result; + } + } + } + let result: Object = route.handler(req, res); if (result instanceof Promise) { diff --git a/lib/index.ts b/lib/index.ts index 86ab1ba..503417c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -12,6 +12,7 @@ import { ControllerFactory, } from './controller/controller-factory'; import {MethodDecorator, MethodFactory} from './methods/method-factory'; +import {MiddlewareDecorator, MiddlewareFactory} from './middleware/middleware'; export {Request, Response, RouteHandler, HttpException}; @@ -20,6 +21,7 @@ export class MuzuServer { public readonly routeManager: RouteManager; public readonly requestHandler: RequestHandler; public readonly Controller: ControllerDecorator; + public readonly Middleware: MiddlewareDecorator; public readonly Get: MethodDecorator; public readonly Post: MethodDecorator; public readonly Delete: MethodDecorator; @@ -33,6 +35,9 @@ export class MuzuServer { this.Controller = new ControllerFactory(this.routeManager).Controller; this.requestHandler = new RequestHandler(this.routeManager); + const middleware = new MiddlewareFactory(); + this.Middleware = middleware.Middleware; + this.server = createServer( this.requestHandler.handleRequest.bind(this.requestHandler) ); diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index 9ed0715..05c2124 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -5,9 +5,11 @@ export interface Route { method: string; url: string; handler: RouteHandler; + middlewares?: Function[]; } export interface Request extends IncomingMessage { params?: Record; body?: Record; + [key: string]: any; } diff --git a/lib/middleware/middleware.ts b/lib/middleware/middleware.ts new file mode 100644 index 0000000..9d2d0fa --- /dev/null +++ b/lib/middleware/middleware.ts @@ -0,0 +1,19 @@ +export type Middleware = ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor +) => PropertyDescriptor; + +export type MiddlewareDecorator = (...middlewares: Function[]) => Middleware; +export class MiddlewareFactory { + Middleware = (...middlewares: Function[]): Middleware => { + return ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) => { + Reflect.defineMetadata('middlewares', middlewares, target[propertyKey]); + return descriptor; + }; + }; +} diff --git a/package-lock.json b/package-lock.json index 62bd6e6..e962f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "muzu", - "version": "0.0.3", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "muzu", - "version": "0.0.3", + "version": "0.1.0", "license": "ISC", "dependencies": { "http-status": "^1.6.2", diff --git a/package.json b/package.json index 1ddb1b7..ccb76b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muzu", - "version": "0.1.0", + "version": "0.1.1", "description": "", "main": "build/lib/index.js", "types": "build/lib/index.d.ts", diff --git a/test/middleware.test.ts b/test/middleware.test.ts new file mode 100644 index 0000000..1804ffd --- /dev/null +++ b/test/middleware.test.ts @@ -0,0 +1,126 @@ +import {MuzuServer, Request, HttpException} from '../lib'; +import * as request from 'supertest'; + +const muzuServer = new MuzuServer(); +const {Controller, Get, Middleware} = muzuServer; + +function Logger() { + console.log('Logger Middleware'); +} + +function Auth(req: Request) { + req.user = {name: 'Muzu User'}; +} + +function AddId(req: Request) { + req.userId = '2342'; +} + +function Admin() { + throw new HttpException(403, 'Unauthorized'); +} + +async function AsyncLogger(): Promise { + return new Promise(resolve => { + setTimeout(() => { + console.log('Async Logger Middleware'); + resolve(); + }, 300); + }); +} + +@Controller('/api') +class TestController { + @Get('/hello') + @Middleware(Logger) + hello() { + return {message: 'Get Method Called'}; + } + + @Get('/hello-auth') + @Middleware(Auth) + helloAuth(req: Request) { + return {message: `Hello ${req.user.name}`}; + } + + @Get('/hello-admin') + @Middleware(Admin) + helloAdmin() { + return {message: 'Hello Admin'}; + } + + @Get('/hello-auth-id') + @Middleware(Auth, AddId) + helloAuthId(req: Request) { + const {user, userId} = req; + + return {message: `Hello ${user.name}`, userId}; + } + + @Get('/hello-async-logger') + @Middleware(AsyncLogger) + helloAsyncLogger() { + return {message: 'Hello Async Logger'}; + } +} + +const port = 3001; +muzuServer.listen(port); + +describe('MuzuServer', () => { + it('should return 200 on GET /api/hello', async () => { + const res = await request(muzuServer.server).get('/api/hello'); + expect(res.status).toBe(200); + expect(res.body).toEqual( + expect.objectContaining({ + message: 'Get Method Called', + }) + ); + }); + + it('should return 200 on GET /api/hello-auth', async () => { + const res = await request(muzuServer.server).get('/api/hello-auth'); + expect(res.status).toBe(200); + expect(res.body).toEqual( + expect.objectContaining({ + message: 'Hello Muzu User', + }) + ); + }); + + it('should return 403 on GET /api/hello-admin', async () => { + const res = await request(muzuServer.server).get('/api/hello-admin'); + expect(res.status).toBe(403); + expect(res.body).toEqual( + expect.objectContaining({ + status: 403, + message: 'Unauthorized', + }) + ); + }); + + it('should return 200 on GET /api/hello-auth-id', async () => { + const res = await request(muzuServer.server).get('/api/hello-auth-id'); + expect(res.status).toBe(200); + expect(res.body).toEqual( + expect.objectContaining({ + userId: '2342', + message: 'Hello Muzu User', + }) + ); + }); + + it('should return 200 on GET /api/hello-async-logger', async () => { + const res = await request(muzuServer.server).get('/api/hello-async-logger'); + expect(res.status).toBe(200); + expect(res.body).toEqual( + expect.objectContaining({ + message: 'Hello Async Logger', + }) + ); + }); +}); + +muzuServer.stop(() => { + console.log('Server stopped'); +});