diff --git a/package.json b/package.json index 86d55d3..2fa52cc 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "license": "ISC", "devDependencies": { "@types/express": "^4.17.13", + "@types/node": "^16.9.1", "@typescript-eslint/eslint-plugin": "^4.30.0", "@typescript-eslint/parser": "^4.30.0", "dotenv": "^10.0.0", @@ -36,12 +37,12 @@ "lint-staged": "^11.1.2", "prettier": "2.3.2", "ts-node-dev": "^1.1.8", - "typescript": "^4.4.2", - "@types/node": "^16.9.1" + "typescript": "^4.4.2" }, "dependencies": { + "@cubetiq/ts-common": "1.0.0", "express": "^4.17.1", - "@cubetiq/ts-common": "1.0.0" + "reflect-metadata": "^0.1.13" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" diff --git a/src/app.ts b/src/app.ts index 38d3148..135f264 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,8 @@ // app config import "./dotenv" +// reflection metadata for decorator +import "reflect-metadata" + // core app -import "./server" \ No newline at end of file +import "./server" diff --git a/src/app/application.ts b/src/app/application.ts deleted file mode 100644 index a597d54..0000000 --- a/src/app/application.ts +++ /dev/null @@ -1,23 +0,0 @@ -import express, { Application as ExpressApp } from "express" - -class Application { - private readonly _instance: ExpressApp - - get instance(): ExpressApp { - return this._instance - } - - constructor() { - this._instance = express() - this._instance.use(express.json()) - this.registerRoutes() - } - - private registerRoutes(): void { - this._instance.get("/", (req, res) => { - res.send("Hello World!") - }) - } -} - -export default new Application() diff --git a/src/app/routes.ts b/src/app/routes.ts deleted file mode 100644 index 9a4b9e1..0000000 --- a/src/app/routes.ts +++ /dev/null @@ -1,3 +0,0 @@ -const index = (req: any, res: any) => { - res.send("Hello World!") -} diff --git a/src/application.ts b/src/application.ts new file mode 100644 index 0000000..7315a7a --- /dev/null +++ b/src/application.ts @@ -0,0 +1,52 @@ +import express, { Application as ExpressApp, Handler } from "express" +import { controllers } from "./controller" +import { RouteHandler } from "./decorators/handlers.decorator" +import { MetadataKeys } from "./constants/metadata.keys" + +class Application { + private readonly _instance: ExpressApp + + get instance(): ExpressApp { + return this._instance + } + + constructor() { + this._instance = express() + this.registerRoutes() + } + + private registerRoutes(): void { + const info: Array<{ api: string; handler: string }> = [] + + controllers.forEach((controller) => { + const controllerInstance: { [handlerName: string]: Handler } = + new controller() as any + const basePath: string = Reflect.getMetadata( + MetadataKeys.BASE_PATH, + controller + ) + const routers: RouteHandler[] = Reflect.getMetadata( + MetadataKeys.ROUTERS, + controller + ) + const exRouter = express.Router() + + routers.forEach(({ method, path, handlerName }) => { + exRouter[method]( + path, + controllerInstance[String(handlerName)] + ).bind(controllerInstance) + info.push({ + api: `${method.toLocaleUpperCase()} ${basePath + path}`, + handler: `${controller.name}.${String(handlerName)}`, + }) + }) + + this._instance.use(basePath, exRouter) + }) + + console.table(info) + } +} + +export default new Application() diff --git a/src/constants/metadata.keys.ts b/src/constants/metadata.keys.ts new file mode 100644 index 0000000..8a02767 --- /dev/null +++ b/src/constants/metadata.keys.ts @@ -0,0 +1,4 @@ +export enum MetadataKeys { + BASE_PATH = "base_path", + ROUTERS = "routers", +} diff --git a/src/controller/index.controller.ts b/src/controller/index.controller.ts new file mode 100644 index 0000000..77b6926 --- /dev/null +++ b/src/controller/index.controller.ts @@ -0,0 +1,13 @@ +import { Request, Response } from "express" +import Controller from "../decorators/controller.decorator" +import { Get } from "../decorators/handlers.decorator" + +@Controller("/hello") +export default class IndexController { + constructor() {} + + @Get("") + public index(req: Request, res: Response) { + res.send(`Hello Index`) + } +} diff --git a/src/controller/index.ts b/src/controller/index.ts new file mode 100644 index 0000000..c026208 --- /dev/null +++ b/src/controller/index.ts @@ -0,0 +1,3 @@ +import IndexController from "./index.controller" + +export const controllers = [IndexController] diff --git a/src/decorators/controller.decorator.ts b/src/decorators/controller.decorator.ts new file mode 100644 index 0000000..7b5b082 --- /dev/null +++ b/src/decorators/controller.decorator.ts @@ -0,0 +1,9 @@ +import { MetadataKeys } from "../constants/metadata.keys" + +const Controller = (basePath: string): ClassDecorator => { + return (target: any) => { + Reflect.defineMetadata(MetadataKeys.BASE_PATH, basePath, target) + } +} + +export default Controller diff --git a/src/decorators/handlers.decorator.ts b/src/decorators/handlers.decorator.ts new file mode 100644 index 0000000..7302240 --- /dev/null +++ b/src/decorators/handlers.decorator.ts @@ -0,0 +1,41 @@ +import { MetadataKeys } from "../constants/metadata.keys" + +export enum Method { + GET = "get", + POST = "post", +} + +export interface RouteHandler { + method: Method + path: string + handlerName: string | symbol +} + +const methodDecoratorFactory = (method: Method) => { + return (path: string): MethodDecorator => { + return (target: any, propertyKey: string | symbol): void => { + const controllerClass = target.constructor + const routers: RouteHandler[] = Reflect.hasMetadata( + MetadataKeys.ROUTERS, + controllerClass + ) + ? Reflect.getMetadata(MetadataKeys.ROUTERS, controllerClass) + : [] + + routers.push({ + method, + path, + handlerName: propertyKey, + }) + + Reflect.defineMetadata( + MetadataKeys.ROUTERS, + routers, + controllerClass + ) + } + } +} + +export const Get = methodDecoratorFactory(Method.GET) +export const Post = methodDecoratorFactory(Method.POST) diff --git a/src/server.ts b/src/server.ts index 546c6e0..1d1847c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,7 @@ import { createServer } from "http" import { SERVER_PORT } from "./app.config" import { info } from "@cubetiq/ts-common/dist/log" -import application from "./app/application" +import application from "./application" // get current host id const hostId = `${require("os").hostname()}#${process.pid}` @@ -11,7 +11,7 @@ const app = application.instance const httpServer = createServer(app) info( - `Application server running on: ${hostId} at: http://0.0.0.0:${SERVER_PORT} and started at: ${startedAt}` + `Application server running on: ${hostId} at port: ${SERVER_PORT} and started at: ${startedAt}` ) httpServer.listen(SERVER_PORT) diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..c38e4b5 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1 @@ +// nothing here for now diff --git a/tsconfig.json b/tsconfig.json index 65b3593..788d9de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,16 @@ { - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "outDir": "dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "lib": ["ES2015"], - "skipLibCheck": true - }, - "exclude": ["node_modules"] -} \ No newline at end of file + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "lib": ["ES2015"], + "skipLibCheck": true, + "experimentalDecorators": true + }, + "exclude": ["node_modules"] +}