From 945e1eb18237f40879acdd2e43cd53dd2e8272a9 Mon Sep 17 00:00:00 2001 From: Luo Yu <412799755@qq.com> Date: Thu, 26 Oct 2023 20:45:41 +0800 Subject: [PATCH] feat: support Request decorators for HTTPController (#159) --- core/controller-decorator/package.json | 3 +- .../src/decorator/http/HTTPParam.ts | 13 +++++ .../src/model/HTTPMethodMeta.ts | 10 ++++ .../src/model/HTTPRequest.ts | 4 ++ .../src/model/HTTPResponse.ts | 4 ++ core/controller-decorator/src/model/index.ts | 2 + core/controller-decorator/src/model/types.ts | 1 + .../lib/impl/http/HTTPMethodRegister.ts | 5 ++ plugin/controller/lib/impl/http/Req.ts | 13 +++++ .../app/controller/AppController.ts | 33 ++++++++++++ .../app/middleware/call_module.ts | 7 +++ .../app/middleware/count_mw.ts | 9 ++++ .../http-inject-app/app/middleware/log_mw.ts | 9 ++++ .../http-inject-app/config/config.default.js | 13 +++++ .../apps/http-inject-app/config/module.json | 1 + .../apps/http-inject-app/config/plugin.js | 16 ++++++ .../apps/http-inject-app/package.json | 3 ++ plugin/controller/test/http/params.test.ts | 1 + plugin/controller/test/http/request.test.ts | 53 +++++++++++++++++++ 19 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 core/controller-decorator/src/model/HTTPRequest.ts create mode 100644 core/controller-decorator/src/model/HTTPResponse.ts create mode 100644 plugin/controller/lib/impl/http/Req.ts create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/call_module.ts create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/count_mw.ts create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/log_mw.ts create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/config/config.default.js create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/config/module.json create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/config/plugin.js create mode 100644 plugin/controller/test/fixtures/apps/http-inject-app/package.json create mode 100644 plugin/controller/test/http/request.test.ts diff --git a/core/controller-decorator/package.json b/core/controller-decorator/package.json index 96c1a4f6..1075b4a9 100644 --- a/core/controller-decorator/package.json +++ b/core/controller-decorator/package.json @@ -41,7 +41,8 @@ "@eggjs/tegg-common-util": "^3.23.0", "@eggjs/tegg-metadata": "^3.23.0", "path-to-regexp": "^1.8.0", - "reflect-metadata": "^0.1.13" + "reflect-metadata": "^0.1.13", + "undici": "^5.26.5" }, "devDependencies": { "@types/mocha": "^10.0.1", diff --git a/core/controller-decorator/src/decorator/http/HTTPParam.ts b/core/controller-decorator/src/decorator/http/HTTPParam.ts index 247ee07c..459ad7bd 100644 --- a/core/controller-decorator/src/decorator/http/HTTPParam.ts +++ b/core/controller-decorator/src/decorator/http/HTTPParam.ts @@ -68,3 +68,16 @@ export function HTTPParam(param?: HTTPParamParams) { HTTPInfoUtil.setHTTPMethodParamName(name, parameterIndex, controllerClazz, methodName); }; } + +export function Request() { + return function(target: any, propertyKey: PropertyKey, parameterIndex: number) { + const [ nodeMajor ] = process.versions.node.split('.').map(v => Number(v)); + assert(nodeMajor >= 16, + `[controller/${target.name}] expect node version >=16, but now is ${nodeMajor}`); + assert(typeof propertyKey === 'string', + `[controller/${target.name}] expect method name be typeof string, but now is ${String(propertyKey)}`); + const methodName = propertyKey as string; + const controllerClazz = target.constructor as EggProtoImplClass; + HTTPInfoUtil.setHTTPMethodParamType(HTTPParamType.REQUEST, parameterIndex, controllerClazz, methodName); + }; +} diff --git a/core/controller-decorator/src/model/HTTPMethodMeta.ts b/core/controller-decorator/src/model/HTTPMethodMeta.ts index 59a40bbe..726746f5 100644 --- a/core/controller-decorator/src/model/HTTPMethodMeta.ts +++ b/core/controller-decorator/src/model/HTTPMethodMeta.ts @@ -8,6 +8,13 @@ export abstract class ParamMeta { abstract validate(httpPath: string); } +export class RequestParamMeta extends ParamMeta { + type = HTTPParamType.REQUEST; + + validate() { + return; + } +} export class BodyParamMeta extends ParamMeta { type = HTTPParamType.BODY; @@ -118,6 +125,9 @@ export class ParamMetaUtil { assert(name, 'query param must has name'); return new QueryParamMeta(name!); } + case HTTPParamType.REQUEST: { + return new RequestParamMeta(); + } default: assert.fail('never arrive'); } diff --git a/core/controller-decorator/src/model/HTTPRequest.ts b/core/controller-decorator/src/model/HTTPRequest.ts new file mode 100644 index 00000000..fada38f8 --- /dev/null +++ b/core/controller-decorator/src/model/HTTPRequest.ts @@ -0,0 +1,4 @@ +import undici from 'undici'; +// https://github.com/nodejs/undici/blob/main/index.js#L118 +// 只有 nodejs >= 16 才支持 Request +export class HTTPRequest extends (undici.Request || Object) {} diff --git a/core/controller-decorator/src/model/HTTPResponse.ts b/core/controller-decorator/src/model/HTTPResponse.ts new file mode 100644 index 00000000..520af119 --- /dev/null +++ b/core/controller-decorator/src/model/HTTPResponse.ts @@ -0,0 +1,4 @@ +import undici from 'undici'; +// https://github.com/nodejs/undici/blob/main/index.js#L118 +// 只有 nodejs >= 16 才支持 Request +export class HTTPResponse extends (undici.Response || Object) {} diff --git a/core/controller-decorator/src/model/index.ts b/core/controller-decorator/src/model/index.ts index 5aded2c0..ce5e1577 100644 --- a/core/controller-decorator/src/model/index.ts +++ b/core/controller-decorator/src/model/index.ts @@ -4,3 +4,5 @@ export * from './MethodMeta'; export * from './ControllerMetadata'; export * from './HTTPMethodMeta'; export * from './HTTPControllerMeta'; +export * from './HTTPRequest'; +export * from './HTTPResponse'; diff --git a/core/controller-decorator/src/model/types.ts b/core/controller-decorator/src/model/types.ts index b4c54910..761305a6 100644 --- a/core/controller-decorator/src/model/types.ts +++ b/core/controller-decorator/src/model/types.ts @@ -39,4 +39,5 @@ export enum HTTPParamType { QUERIES = 'QUERIES', BODY = 'BODY', PARAM = 'PARAM', + REQUEST = 'REQUEST', } diff --git a/plugin/controller/lib/impl/http/HTTPMethodRegister.ts b/plugin/controller/lib/impl/http/HTTPMethodRegister.ts index a4ab6c12..4e9b98b6 100644 --- a/plugin/controller/lib/impl/http/HTTPMethodRegister.ts +++ b/plugin/controller/lib/impl/http/HTTPMethodRegister.ts @@ -15,6 +15,7 @@ import { EggPrototype } from '@eggjs/tegg-metadata'; import { RootProtoManager } from '../../RootProtoManager'; import pathToRegexp from 'path-to-regexp'; import { aclMiddlewareFactory } from './Acl'; +import { HTTPRequest } from './Req'; import { RouterConflictError } from '../../errors'; import { FrameworkErrorFormater } from 'egg-errors'; import { EggRouter } from '@eggjs/router'; @@ -88,6 +89,10 @@ export class HTTPMethodRegister { args[index] = ctx.queries[queryParam.name]; break; } + case HTTPParamType.REQUEST: { + args[index] = new HTTPRequest(ctx); + break; + } default: assert.fail('never arrive'); } diff --git a/plugin/controller/lib/impl/http/Req.ts b/plugin/controller/lib/impl/http/Req.ts new file mode 100644 index 00000000..64ac0178 --- /dev/null +++ b/plugin/controller/lib/impl/http/Req.ts @@ -0,0 +1,13 @@ +import type { Context } from 'egg'; +import { HTTPRequest as BaseHTTPRequest } from '@eggjs/tegg'; +export class HTTPRequest extends BaseHTTPRequest { + constructor(ctx:Context) { + const request = ctx.request; + // href: https://github.com/eggjs/koa/blob/master/src/request.ts#L90C1-L98C4 + super(request.href, { + method: request.method, + headers: request.headers as Record, + body: (ctx.request as any).rawBody, + }); + } +} diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts b/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts new file mode 100644 index 00000000..93d04330 --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/app/controller/AppController.ts @@ -0,0 +1,33 @@ +import { Context as EggContext } from 'egg'; +import { + Context, + HTTPController, + HTTPMethod, + HTTPMethodEnum, + Middleware, + Request, + HTTPRequest, +} from '@eggjs/tegg'; +import { countMw } from '../middleware/count_mw'; + +@HTTPController({ + path: '/apps', +}) +@Middleware(countMw) +export class AppController { + + @HTTPMethod({ + method: HTTPMethodEnum.POST, + path: '/testRequest', + }) + async testRequest(@Context() ctx: EggContext, @Request() request: HTTPRequest) { + const traceId = await ctx.tracer.traceId; + return { + success: true, + traceId, + headers: Object.fromEntries(request.headers), + method: request.method, + requestBody: await request.text(), + }; + } +} diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/call_module.ts b/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/call_module.ts new file mode 100644 index 00000000..fc6d5c00 --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/call_module.ts @@ -0,0 +1,7 @@ +import { Context } from 'egg'; +import { Next } from '@eggjs/tegg'; + +export async function callModuleCtx(ctx: Context, next: Next) { + await (ctx.module as any).multiModuleService.appService.findApp('foo'); + await next(); +} diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/count_mw.ts b/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/count_mw.ts new file mode 100644 index 00000000..8c8fb039 --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/count_mw.ts @@ -0,0 +1,9 @@ +import { Context } from 'egg'; +import { Next } from '@eggjs/tegg'; + +let index = 0; + +export async function countMw(ctx: Context, next: Next) { + await next(); + if (ctx.body) ctx.body.count = index++; +} diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/log_mw.ts b/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/log_mw.ts new file mode 100644 index 00000000..a50e87e8 --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/app/middleware/log_mw.ts @@ -0,0 +1,9 @@ +import { Context } from 'egg'; +import { Next } from '@eggjs/tegg'; + +export function logMwFactory(log: string) { + return async function logMw(ctx: Context, next: Next) { + await next(); + ctx.body.log = log; + }; +} diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/config/config.default.js b/plugin/controller/test/fixtures/apps/http-inject-app/config/config.default.js new file mode 100644 index 00000000..586c9e0c --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/config/config.default.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = function() { + const config = { + keys: 'test key', + security: { + csrf: { + ignoreJSON: false, + } + }, + }; + return config; +}; diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/config/module.json b/plugin/controller/test/fixtures/apps/http-inject-app/config/module.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/config/module.json @@ -0,0 +1 @@ +[] diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/config/plugin.js b/plugin/controller/test/fixtures/apps/http-inject-app/config/plugin.js new file mode 100644 index 00000000..828aebbf --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/config/plugin.js @@ -0,0 +1,16 @@ +'use strict'; + +exports.tracer = { + package: 'egg-tracer', + enable: true, +}; + +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggConfig = { + package: '@eggjs/tegg-config', + enable: true, +}; diff --git a/plugin/controller/test/fixtures/apps/http-inject-app/package.json b/plugin/controller/test/fixtures/apps/http-inject-app/package.json new file mode 100644 index 00000000..dc36efc3 --- /dev/null +++ b/plugin/controller/test/fixtures/apps/http-inject-app/package.json @@ -0,0 +1,3 @@ +{ + "name": "http-inject-app" +} diff --git a/plugin/controller/test/http/params.test.ts b/plugin/controller/test/http/params.test.ts index 477b2da2..c80d7273 100644 --- a/plugin/controller/test/http/params.test.ts +++ b/plugin/controller/test/http/params.test.ts @@ -98,4 +98,5 @@ describe('test/params.test.ts', () => { }); }); }); + }); diff --git a/plugin/controller/test/http/request.test.ts b/plugin/controller/test/http/request.test.ts new file mode 100644 index 00000000..d65859bb --- /dev/null +++ b/plugin/controller/test/http/request.test.ts @@ -0,0 +1,53 @@ +import mm from 'egg-mock'; +import path from 'path'; +import assert from 'assert'; + +describe('test/params.test.ts', () => { + let app; + + beforeEach(() => { + mm(process.env, 'EGG_TYPESCRIPT', true); + }); + + afterEach(() => { + mm.restore(); + }); + + before(async () => { + mm(process.env, 'EGG_TYPESCRIPT', true); + mm(process, 'cwd', () => { + return path.join(__dirname, '../..'); + }); + app = mm.app({ + baseDir: path.join(__dirname, '../fixtures/apps/http-inject-app'), + framework: require.resolve('egg'), + }); + await app.ready(); + }); + + after(() => { + return app.close(); + }); + const [ nodeMajor ] = process.versions.node.split('.').map(v => Number(v)); + if (nodeMajor >= 16) { + it('Request should work', async () => { + app.mockCsrf(); + const param = { + name: 'foo', + desc: 'mock-desc', + }; + const headerKey = 'test-header'; + await app.httpRequest() + .post('/apps/testRequest') + .send(param) + .set('test', headerKey) + .expect(200) + .expect(res => { + assert(res.body.headers.test === headerKey); + assert(res.body.method === 'POST'); + assert(res.body.requestBody === JSON.stringify(param)); + }); + }); + } + +});