Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add global middleware registration #20

Merged
merged 5 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,26 @@ export abstract class Controller {
*/
getPath(): string;
/**
* Returns the router structure with handler and metadata of the controller
* Returns the router structure with handler and metadata of the controller.
* for converting to the modular express router.
* @returns The router structure (array of {@link Route})
*/
getRouter(): Route[];
}

export interface Middleware {
export abstract class Middleware {
/**
* Returns a function that have access to the request object,
* the response object, and the next middleware function.
* @param routeMetadata - A route metadata
*/
getHandler(routeMetadata: RouteMetadata): RouteHandler;
abstract getHandler(routeMetadata: RouteMetadata): RouteHandler;
/**
* Returns a boolean represent to the middleware register condition
* @param routeMethod - A route method for conditioning
* @param routeMetadata - A route metadata for conditioning
* @returns True if a route matches the middleware register condition, otherwise returns false
*/
getRegisterCondition(routeMethod: Methods, routeMetadata: RouteMetadata): boolean;
* Returns a boolean represent to the middleware register condition
* @param routeMetadata - A route metadata for conditioning
* @returns True if a route matches the middleware register condition, otherwise returns false
*/
getRegisterCondition(routeMetadata: RouteMetadata): boolean;
}

export class ControllerRegistry {
Expand All @@ -42,12 +41,17 @@ export class ControllerRegistry {
* into the controller registry for routing system.
*
* @remarks
* The middlewares run in the sequence as passed in the parameter.
* The middleware run in the sequence as passed in the array.
*
* @param controller - A controller instance
* @param middlewares - A single middleware or a group of middleware
* @param middleware - An array of middleware
*/
register(controller: Controller, middleware?: Middleware[]): void;
/**
* Registers global middleware in the application routing.
* @param middleware - A middleware or an array of middleware to register globally
*/
register(controller: Controller, ...middlewares: Middleware[]): void;
registerGlobalMiddleware(middleware: Middleware | Middleware[]): void;
/**
* @returns A count of registered controller
*/
Expand Down Expand Up @@ -83,19 +87,19 @@ export abstract class HttpException extends Error {
/**
* Throws a http response code 400
*/
export class BadRequestException extends HttpException {}
export class BadRequestException extends HttpException { }
/**
* Throws a http response code 403
*/
export class ForbiddenException extends HttpException {}
export class ForbiddenException extends HttpException { }
/**
* Throws a http response code 404
*/
export class NotFoundException extends HttpException {}
export class NotFoundException extends HttpException { }
/**
* Throws a http response code 401
*/
export class UnauthorizedException extends HttpException {}
export class UnauthorizedException extends HttpException { }

/**
* Accepted http request methods
Expand All @@ -120,15 +124,15 @@ export type Route = {
metadata: RouteMetadata,
};

export interface Request extends ExpressRequest {}
export interface Request extends ExpressRequest { }

export interface Response extends ExpressResponse {}
export interface Response extends ExpressResponse { }

export interface NextFunction extends ExpressNextFunction {}
export interface NextFunction extends ExpressNextFunction { }

export class RouteUtil {
/**
* Add data into the route metadata
* Add data into the route metadata.
* @param data - A key-value pairs of data to merge into the route metadata
*/
static addRouteMetadata(data: Record<string, any>): MethodDecorator;
Expand Down
16 changes: 10 additions & 6 deletions lib/controllers/ControllerRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,33 @@ class ControllerRegistry {
this.#app = app;
}

register(controller, ...middlewares) {
register(controller, middleware = []) {
const router = Router();
const controllerPath = controller.getPath();
const routes = controller.getRouter();

if (process.env.NODE_ENV !== 'production' && this.#controllers.includes(controllerPath)) {
if (this.#controllers.includes(controllerPath)) {
const controllerName = controller.constructor.name;
console.warn(`Register duplicated controller: ${controllerPath} in ${controllerName}`);
}

routes.forEach((route) => {
const routeMetadata = route.metadata;
const routeMethod = routeMetadata.method.toLowerCase();
const routeMethod = routeMetadata.method;
const routePath = routeMetadata.path;
const routerFunction = router[routeMethod];
const routerFunction = router[routeMethod.toLowerCase()];

const registerMiddleware = (middleware) => {
const canRegisterMiddleware = middleware
.getRegisterCondition(routeMethod, routeMetadata);
.getRegisterCondition(routeMetadata);

if (canRegisterMiddleware) {
return middleware.getHandler(routeMetadata);
}
};

const routeSubStack = [
...middlewares.map(registerMiddleware).filter(Boolean),
...middleware.map(registerMiddleware).filter(Boolean),
route.handler,
].map(this.#interceptError);

Expand All @@ -50,6 +50,10 @@ class ControllerRegistry {
this.#controllers.push(controllerPath);
}

registerGlobalMiddleware(middleware) {
this.#app.use(middleware.getHandler());
}

size() {
return this.#controllers.length;
}
Expand Down
21 changes: 21 additions & 0 deletions lib/controllers/Middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

class Middleware {

constructor() {
if (this.constructor == Middleware) {
throw new Error("Abstract classes can't be instantiated.");
}
}

getHandler() {
throw new Error("Method 'getHandler()' must be implemented.");
}

getRegisterCondition(routeMetadata) {
return true;
}

}

module.exports = Middleware;
2 changes: 1 addition & 1 deletion lib/exceptions/HttpException.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class HttpException extends Error {
this.name = this.constructor.name;

if (this.constructor == HttpException) {
throw new Error("Abstract classes can't be instandtiated.");
throw new Error("Abstract classes can't be instantiated.");
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/springpress.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require('reflect-metadata');
const Controller = require('./controllers/Controller');
const ControllerRegistry = require('./controllers/ControllerRegistry');
const Methods = require('./controllers/Methods');
const Middleware = require('./controllers/Middleware');

const ControllerDecorator = require('./decorators/ControllerDecorator');
const RequestDecorator = require('./decorators/RequestDecorator');
Expand All @@ -29,6 +30,7 @@ const Springpress = require('./server/Server');
exports.Controller = Controller;
exports.ControllerRegistry = ControllerRegistry;
exports.Methods = Methods;
exports.Middleware = Middleware;
exports.RouteUtil = RouteUtil;
exports.Springpress = Springpress;

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "jest"
"test": "jest --verbose"
},
"files": [
"LICENSE",
Expand All @@ -35,7 +35,9 @@
},
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/supertest": "^2.0.12",
"jest": "^27.5.1",
"supertest": "^6.2.3",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
}
Expand Down
27 changes: 14 additions & 13 deletions test/controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import express, { Express } from 'express';
import request from 'supertest';
import { ControllerRegistry } from '..';
import { MockIndexController, MockTestController } from './mock/MockController';
import { MockConditionalMiddleware, MockMiddleware } from './mock/MockMiddleware';
import { MockConditionalMiddleware, MockMiddleware, PingPongMiddleware } from './mock/MockMiddleware';

describe('Test the ControllerRegistry class implementation', () => {

const originalEnv = process.env;
let expressApp: Express;
let controllerRegistry: ControllerRegistry;

beforeEach(() => {
process.env = { ...originalEnv };

expressApp = express();
controllerRegistry = new ControllerRegistry(expressApp);
});

afterEach(() => {
jest.clearAllMocks();
process.env = originalEnv;
});

it('should registered the mock controllers correctly', () => {
Expand All @@ -29,16 +26,12 @@ describe('Test the ControllerRegistry class implementation', () => {
expect(findRouter(expressApp).length).toBe(2);
});

it('should warn an error correctly when register a duplicated controller', () => {
it('should warn an error when register a duplicated controller', () => {
jest.spyOn(console, 'warn').mockImplementation(jest.fn());

controllerRegistry.register(new MockIndexController());
controllerRegistry.register(new MockIndexController());

// It should not warn when NODE_ENV is production
process.env.NODE_ENV = 'production'
controllerRegistry.register(new MockIndexController());

expect(console.warn).toBeCalledTimes(1);
});

Expand All @@ -48,7 +41,7 @@ describe('Test the ControllerRegistry class implementation', () => {
// Unconditional middleware means this middleware should register for all routes.
// NOTE:
// - Expect 2 means the stack has middleware function and route handler function.
controllerRegistry.register(new MockIndexController(), new MockMiddleware());
controllerRegistry.register(new MockIndexController(), [new MockMiddleware()]);
router = findRouter(expressApp);
routes = router[0].handle.stack;
routes.forEach((layer: any) => expect(layer.route.stack.length).toBe(2));
Expand All @@ -59,7 +52,7 @@ describe('Test the ControllerRegistry class implementation', () => {
// - In MockTestController has only 1 route path named 'test2'.
// - Expect 1 means the stack has only route handler function.
// - Expect 2 means the stack has middleware function and route handler function.
controllerRegistry.register(new MockTestController(), new MockConditionalMiddleware());
controllerRegistry.register(new MockTestController(), [new MockConditionalMiddleware()]);
router = findRouter(expressApp);
routes = router[1].handle.stack;
expect(routes.find((layer: any) => layer.route.path === '/test').route.stack.length).toBe(1);
Expand All @@ -68,10 +61,18 @@ describe('Test the ControllerRegistry class implementation', () => {

it('should return a registered controller count correctly', () => {
controllerRegistry.register(new MockIndexController());
controllerRegistry.register(new MockTestController(), new MockMiddleware());
controllerRegistry.register(new MockTestController(), [new MockMiddleware()]);
expect(controllerRegistry.size()).toBe(2);
});

it('should contain ping in response header when register middleware globally', async () => {
controllerRegistry.registerGlobalMiddleware(new PingPongMiddleware());
controllerRegistry.register(new MockIndexController());

const response = await request(expressApp).get('/test');
expect(response.headers['ping']).toBe('pong');
});

});

const findRouter = (expressApp: Express) => {
Expand Down
27 changes: 17 additions & 10 deletions test/mock/MockMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { Methods, Middleware, RouteHandler, RouteMetadata } from '../..';
import { Middleware, RouteHandler, RouteMetadata } from '../..';

export class MockMiddleware implements Middleware {
export class MockMiddleware extends Middleware {

public getHandler(): RouteHandler {
public getHandler(routeMetadata: RouteMetadata): RouteHandler {
return async (req, res, next) => {
next();
};
}

public getRegisterCondition(routeMethod: Methods, routeMetadata: RouteMetadata): boolean {
return true;
}

}

export class MockConditionalMiddleware implements Middleware {
export class MockConditionalMiddleware extends Middleware {

public getHandler(): RouteHandler {
public getHandler(routeMetadata: RouteMetadata): RouteHandler {
return async (req, res, next) => {
next();
};
}

public getRegisterCondition(routeMethod: Methods, routeMetadata: RouteMetadata): boolean {
public getRegisterCondition(routeMetadata: RouteMetadata): boolean {
return routeMetadata.path === '/test2';
}

}

export class PingPongMiddleware extends Middleware {

public getHandler(routeMetadata: RouteMetadata): RouteHandler {
return async (req, res, next) => {
res.header('ping', 'pong');
next();
};
}

}
Loading