From f4731e4711ca9426dffafe9f6ac18eae1f982abc Mon Sep 17 00:00:00 2001 From: "Mr.MKZ" Date: Fri, 13 Sep 2024 01:31:00 +0330 Subject: [PATCH] feat(Router): route params added to Request. fix(core): core freeze when loadConfig method didn't run. fix(types): Request and Response code suggestion didn't work for Javascript. chore(core): some changes in request handling structure. --- README.md | 1 + benchmark.py | 2 +- package-lock.json | 31 ++++++---- package.json | 5 +- src/core/coreTypes.ts | 20 ++++++- src/core/index.ts | 129 +++++++++++++++++++++++++++++++----------- src/index.ts | 24 ++++++-- src/types.ts | 17 ++---- tests/index.ts | 5 +- tests/routes/v1.ts | 6 +- 10 files changed, 171 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 592b211..24a71fd 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ AxonJs has some types which can help you in developing your applications for aut - `AxonCoreConfig`: Type of core config object for configuration Axon core as you want. - `Request`: Type of controller request param. (IncomingMessage) - `Response`: Type of controller response param. (ServerResponse) +- `Headers`: Type of response headers. (OutgoingHeaders) ## Contributing diff --git a/benchmark.py b/benchmark.py index 933c7a3..1551199 100644 --- a/benchmark.py +++ b/benchmark.py @@ -3,7 +3,7 @@ start_time = time.time() -duration = 1 +duration = 10 requests_count = 0 response_time = [] diff --git a/package-lock.json b/package-lock.json index c633c81..d94c101 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mr-mkz/axon", - "version": "0.0.4", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mr-mkz/axon", - "version": "0.0.4", + "version": "0.1.0", "license": "ISC", "dependencies": { "@spacingbat3/kolor": "^4.0.0", @@ -16,9 +16,10 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^22.1.0", + "@types/node": "^22.5.4", "jest": "^29.7.0", "nodemon": "^3.1.4", + "path-to-regexp": "^8.1.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsup": "^8.2.4", @@ -1388,13 +1389,13 @@ } }, "node_modules/@types/node": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", - "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.13.0" + "undici-types": "~6.19.2" } }, "node_modules/@types/stack-utils": { @@ -4253,6 +4254,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz", + "integrity": "sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5433,9 +5444,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", - "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 56dfd81..04777ce 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^22.1.0", "jest": "^29.7.0", "nodemon": "^3.1.4", + "path-to-regexp": "^8.1.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsup": "^8.2.4", @@ -51,8 +51,9 @@ }, "dependencies": { "@spacingbat3/kolor": "^4.0.0", + "moment": "^2.30.1", "pino": "^9.4.0", "pino-pretty": "^11.2.2", - "moment": "^2.30.1" + "@types/node": "^22.5.4" } } diff --git a/src/core/coreTypes.ts b/src/core/coreTypes.ts index 28e5ee1..8bfa398 100644 --- a/src/core/coreTypes.ts +++ b/src/core/coreTypes.ts @@ -2,8 +2,26 @@ interface AxonCoreConfig { DEBUG?: boolean; LOGGER?: boolean; LOGGER_VERBOSE?: boolean; + RESPONSE_MESSAGES?: AxonResponseMessage; +} + +interface AxonResponseMessage { + /** + * response error message for 404 not found response from core + */ + notFound?: string; + /** + * response error message for 500 internal server error response from core + */ + serverError?: string; + /** + * response error message for 405 method not allowed response from core + */ + methodNotAllowed?: string; + [key: string]: string | undefined; } export { - AxonCoreConfig + AxonCoreConfig, + AxonResponseMessage } \ No newline at end of file diff --git a/src/core/index.ts b/src/core/index.ts index 2937a2d..b1993d4 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,5 @@ import Router from "../Router"; -import { HttpMethods, JsonResponse, CoreReq } from "../types" +import { HttpMethods, JsonResponse } from "../types" import * as http from "http"; import { routeDuplicateException } from "./CoreExceptions"; import addRoutePrefix from "./utils/routePrefixHandler"; @@ -7,11 +7,20 @@ import { AxonCoreConfig } from "./coreTypes"; import { logger } from "./utils/coreLogger"; import { colors } from "@spacingbat3/kolor" import getRequestBody from "./utils/getRequestBody"; +import { Key, pathToRegexp, Keys } from "path-to-regexp"; +import { Request, Response, Headers } from ".."; + +const defaultResponses = { + notFound: "Not found", + serverError: "Internal server error", + methodNotAllowed: "Method not allowed" +} export default class AxonCore { private routes: HttpMethods; private config: AxonCoreConfig; private configsLoaded: boolean; + private passConfig: boolean; private routesLoaded: boolean; constructor() { @@ -27,10 +36,12 @@ export default class AxonCore { this.config = { DEBUG: false, LOGGER: true, - LOGGER_VERBOSE: false + LOGGER_VERBOSE: false, + RESPONSE_MESSAGES: defaultResponses }; this.configsLoaded = false; + this.passConfig = true; this.routesLoaded = false; } @@ -41,9 +52,11 @@ export default class AxonCore { * @param config core config object */ loadConfig(config: AxonCoreConfig) { + this.passConfig = false; this.config.DEBUG = config.DEBUG || false this.config.LOGGER = config.LOGGER || true this.config.LOGGER_VERBOSE = config.LOGGER_VERBOSE || false + this.config.RESPONSE_MESSAGES = { ...config.RESPONSE_MESSAGES } if (this.config.DEBUG) { logger.level = "debug" @@ -89,7 +102,7 @@ export default class AxonCore { * @param res server response * @returns */ - async #handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { + async #handleRequest(req: Request, res: Response) { // log incoming requests if (this.config.LOGGER_VERBOSE) { logger.request({ @@ -107,7 +120,7 @@ export default class AxonCore { res.statusCode = 405 res.write(JSON.stringify({ - msg: `Method ${req.method} not allowed` + message: this.config.RESPONSE_MESSAGES?.methodNotAllowed || defaultResponses.methodNotAllowed })); return res.end(); @@ -118,50 +131,93 @@ export default class AxonCore { (Object.keys(this.routes) as Array).forEach(async (method) => { if (method == req.method) { if (req.url) { - try { - controller = await this.routes[method][req.url]["controller"](req, res) - if (controller.responseMessage) { - res.statusMessage = controller.responseMessage - } + let findedRoute = false; + + if (Object.keys(this.routes[method]).length === 0) { + res.statusCode = 404 + res.write(JSON.stringify({ + message: this.config.RESPONSE_MESSAGES?.notFound || defaultResponses.notFound + })) + + return res.end() + } + + return Object.keys(this.routes[method]).forEach(async (route, index) => { + let keys: Keys; + const regexp = pathToRegexp(route); + keys = regexp.keys + const match: RegExpExecArray | null = regexp.regexp.exec(req.url as string); + + if (match) { + try { + if (!findedRoute) { + findedRoute = true + + const params: Record = {}; + + keys.forEach((key: Key, index: number) => { + params[key.name] = match[index + 1]; + }); + + req.params = params; + + controller = await this.routes[method][route]["controller"](req, res) + + if (controller.responseMessage) { + res.statusMessage = controller.responseMessage + } - res.statusCode = controller.responseCode + if (typeof controller.body !== "object") { + throw new TypeError(`Response body can't be ${typeof controller.body}`) + } - if (controller.headers) { - for (const key in controller.headers) { - if (controller.headers[key]) { - res.setHeader(key, controller.headers[key]) + if (typeof controller.responseCode !== "number") { + throw new TypeError(`Response code can't be ${typeof controller.responseCode}`); + } + + res.statusCode = controller.responseCode + + if (controller.headers) { + for (const key in controller.headers) { + if (controller.headers[key]) { + res.setHeader(key, controller.headers[key]) + } + } + } + + res.write(JSON.stringify(controller.body)) + + return res.end() + } else { + return; } + } catch (error) { + logger.error(error) + + res.statusCode = 500 + res.write(JSON.stringify({ + message: this.config.RESPONSE_MESSAGES?.serverError || defaultResponses.serverError + })) + return res.end() } } - res.write(JSON.stringify(controller.body)) - - return res.end() - } catch (error) { - if (error instanceof TypeError) { + if (!findedRoute && (Object.keys(this.routes[method]).length == (index + 1))) { res.statusCode = 404 res.write(JSON.stringify({ - message: "Not found" + message: this.config.RESPONSE_MESSAGES?.notFound || defaultResponses.notFound })) return res.end() } - res.statusCode = 500 - res.write(JSON.stringify({ - message: "Internal Server Error" - })) - return res.end() - } + }) } } }) } - async #responseHandler(req: CoreReq, res: http.ServerResponse) { } - - /** * Start listening to http incoming requests * @param {string} host server host address @@ -174,9 +230,18 @@ export default class AxonCore { const corePreloader = async (): Promise => { return new Promise((resolve) => { const interval = setInterval(() => { - if (this.routesLoaded && this.configsLoaded) { - clearInterval(interval); - resolve(); + if (this.passConfig) { + if (this.routesLoaded) { + logger.info("all routes loaded!"); + clearInterval(interval); + resolve(); + } + } else { + if (this.routesLoaded && this.configsLoaded) { + logger.info("all configs and routes loaded!"); + clearInterval(interval); + resolve(); + } } }, 100); }); diff --git a/src/index.ts b/src/index.ts index 1dec982..3362455 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,19 +3,32 @@ import AxonRouter from "./Router"; import { JsonResponse } from "./types"; import { AxonCoreConfig } from "./core/coreTypes"; import * as http from "http" - + const Router = () => { - return new AxonRouter() + return new AxonRouter() } declare module 'http' { interface IncomingMessage { + /** + * the body of request which sent from client + */ body?: any; + /** + * incoming request parameters in request path + * + * example: + * - route: `/api/v1/user/:id` + * - path: `/api/v1/user/12` + * - params: { "id": 12 } + */ + params: any; } } -type Request = http.IncomingMessage; -type Response = http.ServerResponse; +interface Request extends http.IncomingMessage {}; +interface Response extends http.ServerResponse {}; +interface Headers extends http.OutgoingHttpHeaders {}; export { AxonCore, @@ -23,5 +36,6 @@ export { JsonResponse, AxonCoreConfig, Request, - Response + Response, + Headers } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 635384a..8f7ae4c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,8 @@ -import * as http from "http"; +import { Request, Response, Headers } from "."; interface Route { - controller: (req: http.IncomingMessage, res: http.ServerResponse) => Promise; + controller: (req: Request, res: Response) => Promise; + middlewares?: Array<(req: Request, res: Response) => void>; } interface Routes { @@ -19,21 +20,11 @@ export type HttpMethods = { export type JsonResponse = { body: object; - headers?: http.OutgoingHttpHeaders; + headers?: Headers; responseCode: number; responseMessage?: string; } -export type CoreReq = { - http: http.IncomingMessage, - body: string -} - -export type CoreRes = { - http: http.ServerResponse, - status: () => void, -} - export interface ExceptionMeta { type: string; description: string; diff --git a/tests/index.ts b/tests/index.ts index c25f092..f22402d 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -11,7 +11,10 @@ const core = new AxonCore() core.loadConfig({ DEBUG: true, // default false LOGGER: true, // default true - LOGGER_VERBOSE: false // default false + LOGGER_VERBOSE: false, // default false + RESPONSE_MESSAGES: { + notFound: "route not found" + } }) core.loadRoute(v1Routes) diff --git a/tests/routes/v1.ts b/tests/routes/v1.ts index d03edd4..4622cac 100644 --- a/tests/routes/v1.ts +++ b/tests/routes/v1.ts @@ -2,13 +2,11 @@ import { Router } from "../../src"; const router = Router(); -router.get('/', async (req, res) => { - - console.log(req.url); +router.get('/user/:name', async (req, res) => { return { body: { - message: "hello" + message: `Hello ${req.params.name}` }, responseCode: 200 }