diff --git a/packages/@runners/express/package.json b/packages/@runners/express/package.json index 4c4e7da..7cd1603 100644 --- a/packages/@runners/express/package.json +++ b/packages/@runners/express/package.json @@ -1,6 +1,6 @@ { "name": "@abyss.ts/express-runner", - "version": "0.0.1", + "version": "0.0.2", "author": "Alpha", "repository": { "type": "git", @@ -8,8 +8,12 @@ }, "license": "MIT", "type": "module", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "main": "lib/index.js", + "types": "lib/index.d.ts" + }, "keywords": [ "abyss", "abyssts", @@ -27,7 +31,8 @@ "@abyss.ts/core": "workspace:^", "body-parser": "^1.20.2", "detect-port": "^1.6.1", - "express": "^4.19.2" + "express": "^4.19.2", + "nanoid": "^5.0.7" }, "devDependencies": { "@types/body-parser": "^1.19.5", diff --git a/packages/@runners/express/src/Application.ts b/packages/@runners/express/src/Application.ts index d2826cf..bfd7d72 100644 --- a/packages/@runners/express/src/Application.ts +++ b/packages/@runners/express/src/Application.ts @@ -1,15 +1,12 @@ +import { nanoid } from 'nanoid'; import bodyParser from 'body-parser'; import detectPort from 'detect-port'; -import express, { Router, type Express } from 'express'; +import express, { type Express } from 'express'; +import { AbyssApplication } from '@abyss.ts/core'; import { createServer as createHttpServer } from 'http'; -import { - combine, - AbyssApplication, - getControllerMetadata, - getControllerActionMetadata, -} from '@abyss.ts/core'; - -import { mapParameters } from './utils/actionUtils'; + +import { mapRoutes } from './utils/routeUtils'; +import { mapInjections } from './utils/injectionUtils'; import { mapController } from './utils/controllerUtils'; export class ExpressApplication extends AbyssApplication { @@ -40,32 +37,19 @@ export class ExpressApplication extends AbyssApplication { } public async run(): Promise { - const controllers = await mapController(); - - const routes: string[] = []; - const router = Router(); + this.#express.use((req, _res, next) => { + Object.assign(req, { + executionId: nanoid(), + }); - for (const controller of controllers) { - const { route } = getControllerMetadata(controller); - const actions = getControllerActionMetadata(controller); + next(); + }); - for (const action of actions) { - const { exec, httpMethod, route: actionRoute, propertyKey } = action; - const httpRoute = `/${combine({ joinWith: '/' }, route, actionRoute)}`; - - routes.push(httpRoute); + const controllers = await mapController(); - router[httpMethod](httpRoute, (req, res) => { - const params = mapParameters({ - controller, - propertyKey, - request: req, - }); + mapInjections(); - res.send(exec(...params)); - }); - } - } + const [routes, router] = mapRoutes(controllers); this.#express.use(router); @@ -81,7 +65,7 @@ export class ExpressApplication extends AbyssApplication { console.log(`Server is running on ${runningPort}`); if (routes.length) { - console.log(`\n\nList routes: \n\n${routes.join('\n')}`); + console.log(`\n\nList routes: \n${routes.join('\n')}`); } } } diff --git a/packages/@runners/express/src/index.ts b/packages/@runners/express/src/index.ts index 4862865..c520301 100644 --- a/packages/@runners/express/src/index.ts +++ b/packages/@runners/express/src/index.ts @@ -1,4 +1,17 @@ -export { Controller, Get, Query, Body, Request, Param } from '@abyss.ts/core'; +export { + Get, + Put, + Body, + Post, + Patch, + Query, + Param, + Delete, + Inject, + Request, + Controller, + Injectable, +} from '@abyss.ts/core'; export type { Request as TRequest } from 'express'; diff --git a/packages/@runners/express/src/utils/controllerUtils.ts b/packages/@runners/express/src/utils/controllerUtils.ts index f358eb4..f7f9be6 100644 --- a/packages/@runners/express/src/utils/controllerUtils.ts +++ b/packages/@runners/express/src/utils/controllerUtils.ts @@ -1,9 +1,11 @@ import { getControllerMetadata, loadControllers } from '@abyss.ts/core'; export async function mapController(): Promise { + console.log('Scanning controllers...'); + const importedControllers = await loadControllers(); - return importedControllers.reduce((result, module) => { + const controllers = importedControllers.reduce((result, module) => { const classes = Object.values(module); for (const klass of classes) { @@ -18,4 +20,8 @@ export async function mapController(): Promise { return result; }, []); + + console.log('Finish scanning controllers!\n'); + + return controllers; } diff --git a/packages/@runners/express/src/utils/injectionUtils.ts b/packages/@runners/express/src/utils/injectionUtils.ts new file mode 100644 index 0000000..d24194d --- /dev/null +++ b/packages/@runners/express/src/utils/injectionUtils.ts @@ -0,0 +1,109 @@ +import { + pushToIoCContainer, + getFromIoCContainer, + getInjectionMetadata, + getInjectParamMetadata, + type IInjectionProps, +} from '@abyss.ts/core'; + +export function mapInjections(): void { + console.log('Scanning injections...'); + + const injections = getInjectionMetadata(); + const groupedInjections = topologicalGroup(injections); + + for (const pack of groupedInjections) { + for (const injection of pack) { + const { target, scope } = injection; + + if (scope === 'singleton') { + const metadata = getInjectParamMetadata({ + target, + }); + + const params = metadata.map(({ extractor }) => { + return getFromIoCContainer(extractor); + }); + + const instance = new target(...params); + + pushToIoCContainer({ + target, + instance, + }); + } + } + } + + console.log('Finish scanning injections!\n'); +} + +export function topologicalGroup( + injections: Array, +): IInjectionProps[][] { + const visited: Record = {}; + const result: IInjectionProps[][] = []; + + while (injections.length) { + const removedIndexes = []; + + for (let i = 0, n = injections.length; i < n; i++) { + const injection = injections[i]; + + if (!injection) { + removedIndexes.push(i); + continue; + } + + const { target } = injection; + + const params = getInjectParamMetadata({ + target, + }); + + if (!params.length) { + result[0] ||= []; + result[0].push(injection); + removedIndexes.push(i); + + visited[target] = { + level: 0, + visited: true, + }; + + continue; + } + + let level = 0; + let visitedAllDependencies = true; + + params.forEach((injectable) => { + const { extractor } = injectable; + const visitedData = visited[extractor]; + + if (visitedData) { + level = Math.max(level, visitedData.level); + } else { + visitedAllDependencies = false; + } + }); + + if (visitedAllDependencies) { + result[level + 1] ||= []; + result[level + 1]!.push(injection); + removedIndexes.push(i); + + visited[target] = { + visited: true, + level: level + 1, + }; + } + } + + removedIndexes.reverse().forEach((index) => { + injections.splice(index, 1); + }); + } + + return result; +} diff --git a/packages/@runners/express/src/utils/routeUtils.ts b/packages/@runners/express/src/utils/routeUtils.ts new file mode 100644 index 0000000..883ec69 --- /dev/null +++ b/packages/@runners/express/src/utils/routeUtils.ts @@ -0,0 +1,52 @@ +import { Router } from 'express'; +import { + combine, + getFromIoCContainer, + getControllerMetadata, + getInjectParamMetadata, + getControllerActionMetadata, +} from '@abyss.ts/core'; + +import { mapParameters } from './actionUtils'; + +export function mapRoutes(controllers: TAny[]): [string[], Router] { + console.log('Initializing routes...'); + + const routes: string[] = []; + const router = Router(); + + for (const controller of controllers) { + const { route } = getControllerMetadata(controller); + const actions = getControllerActionMetadata(controller); + const injects = getInjectParamMetadata({ target: controller }); + + const injections = injects.map(({ extractor }) => { + return getFromIoCContainer(extractor); + }); + + const controllerInstance = new controller(...injections); + + for (const action of actions) { + const { exec, httpMethod, route: actionRoute, propertyKey } = action; + const httpRoute = `/${combine({ joinWith: '/' }, route, actionRoute)}`; + + routes.push( + combine({ joinWith: ' ' }, httpMethod.toUpperCase(), httpRoute), + ); + + router[httpMethod](httpRoute, (req, res) => { + const params = mapParameters({ + controller, + propertyKey, + request: req, + }); + + res.send(exec.bind(controllerInstance)(...params)); + }); + } + } + + console.log('Finish initializing routes!\n'); + + return [routes, router]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c78cdd2..8bf66ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: express: specifier: ^4.19.2 version: 4.19.2 + nanoid: + specifier: ^5.0.7 + version: 5.0.7 devDependencies: '@types/body-parser': specifier: ^1.19.5