Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions core/controller-decorator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './src/util/MCPInfoUtil';
export * from './src/util/HTTPPriorityUtil';

export { default as ControllerInfoUtil } from './src/util/ControllerInfoUtil';
export { default as MethodInfoUtil } from './src/util/MethodInfoUtil';
7 changes: 6 additions & 1 deletion plugin/controller/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Application } from 'egg';
import { CONTROLLER_LOAD_UNIT, ControllerLoadUnit } from './lib/ControllerLoadUnit';
import { AppLoadUnitControllerHook } from './lib/AppLoadUnitControllerHook';
import { LoadUnitLifecycleContext } from '@eggjs/tegg-metadata';
import { GlobalGraph, LoadUnitLifecycleContext } from '@eggjs/tegg-metadata';
import { ControllerMetaBuilderFactory, ControllerType } from '@eggjs/tegg';
import { HTTPControllerRegister } from './lib/impl/http/HTTPControllerRegister';
import { ControllerRegisterFactory } from './lib/ControllerRegisterFactory';
Expand All @@ -12,6 +12,7 @@ import { EggControllerPrototypeHook } from './lib/EggControllerPrototypeHook';
import { RootProtoManager } from './lib/RootProtoManager';
import { EggControllerLoader } from './lib/EggControllerLoader';
import { MCPControllerRegister } from './lib/impl/mcp/MCPControllerRegister';
import { middlewareGraphHook } from './lib/MiddlewareGraphHook';
import assert from 'node:assert';

// Load Controller process
Expand Down Expand Up @@ -136,6 +137,10 @@ export default class ControllerAppBootHook {
this.app.config.mcp.hooks = MCPControllerRegister.hooks;
}

configDidLoad() {
GlobalGraph.instance!.registerBuildHook(middlewareGraphHook);
}

async willReady() {
if (this.mcpEnable()) {
await MCPControllerRegister.connectStatelessStreamTransport();
Expand Down
83 changes: 83 additions & 0 deletions plugin/controller/lib/MiddlewareGraphHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { GraphNode } from '@eggjs/tegg-common-util';
import {
ClassProtoDescriptor,
GlobalGraph,
ProtoDependencyMeta,
ProtoNode,
} from '@eggjs/tegg-metadata';
import { EggProtoImplClass, IAdvice } from '@eggjs/tegg-types';
import { ControllerInfoUtil, MethodInfoUtil } from '@eggjs/tegg';

export function middlewareGraphHook(globalGraph: GlobalGraph) {
for (const moduleNode of globalGraph.moduleGraph.nodes.values()) {
for (const controllerProtoNode of moduleNode.val.protos) {
const middlewareProtoNodes = findMiddlewareProtoNodes(globalGraph, controllerProtoNode);
if (!middlewareProtoNodes) continue;
for (const middlewareProtoNode of middlewareProtoNodes) {
const middlewareModuleNode = globalGraph.findModuleNode(middlewareProtoNode.val.proto.instanceModuleName);
if (!middlewareModuleNode) continue;
globalGraph.addInject(
moduleNode,
controllerProtoNode,
middlewareProtoNode,
middlewareProtoNode.val.proto.name);
}
}
}
}

function findMiddlewareProtoNodes(globalGraph: GlobalGraph, protoNode: GraphNode<ProtoNode, ProtoDependencyMeta>) {
const proto = protoNode.val.proto;
if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) {
return;
}

const middlewareClazzSet = new Set<EggProtoImplClass<IAdvice>>();

// Get AOP middlewares from controller class
const controllerAopMiddlewares = ControllerInfoUtil.getControllerAopMiddlewares(proto.clazz);
if (controllerAopMiddlewares && controllerAopMiddlewares.length > 0) {
for (const middlewareClazz of controllerAopMiddlewares) {
middlewareClazzSet.add(middlewareClazz);
}
}

// Get AOP middlewares from controller methods
const methods = MethodInfoUtil.getMethods(proto.clazz);
for (const methodName of methods) {
const methodAopMiddlewares = MethodInfoUtil.getMethodAopMiddlewares(proto.clazz, methodName);
if (methodAopMiddlewares && methodAopMiddlewares.length > 0) {
for (const middlewareClazz of methodAopMiddlewares) {
middlewareClazzSet.add(middlewareClazz);
}
}
}

if (middlewareClazzSet.size === 0) {
return;
}

const result: GraphNode<ProtoNode, ProtoDependencyMeta>[] = [];
for (const middlewareClazz of middlewareClazzSet) {
// Find the proto node for this middleware class
const middlewareProtoNode = findProtoNodeByClass(globalGraph, middlewareClazz);
if (middlewareProtoNode) {
result.push(middlewareProtoNode);
}
}

return result.length > 0 ? result : undefined;
}
Comment on lines +29 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function findMiddlewareProtoNodes has a few areas for improvement:

  1. Inconsistent Return Type: It can return undefined or an array. It would be cleaner to always return an array (GraphNode[]), which would be empty if no middlewares are found. This simplifies the calling code by removing the need for a null check.
  2. Redundant Checks: The if (array && array.length > 0) pattern is partially redundant, as a for...of loop won't execute on an empty array. This can be simplified.

I've provided a suggestion that addresses these points to make the function more robust and easier to read.

function findMiddlewareProtoNodes(globalGraph: GlobalGraph, protoNode: GraphNode<ProtoNode, ProtoDependencyMeta>): GraphNode<ProtoNode, ProtoDependencyMeta>[] {
  const proto = protoNode.val.proto;
  if (!ClassProtoDescriptor.isClassProtoDescriptor(proto)) {
    return [];
  }

  const middlewareClazzSet = new Set<EggProtoImplClass<IAdvice>>();

  // Get AOP middlewares from controller class
  const controllerAopMiddlewares = ControllerInfoUtil.getControllerAopMiddlewares(proto.clazz);
  if (controllerAopMiddlewares) {
    for (const middlewareClazz of controllerAopMiddlewares) {
      middlewareClazzSet.add(middlewareClazz);
    }
  }

  // Get AOP middlewares from controller methods
  const methods = MethodInfoUtil.getMethods(proto.clazz);
  for (const methodName of methods) {
    const methodAopMiddlewares = MethodInfoUtil.getMethodAopMiddlewares(proto.clazz, methodName);
    if (methodAopMiddlewares) {
      for (const middlewareClazz of methodAopMiddlewares) {
        middlewareClazzSet.add(middlewareClazz);
      }
    }
  }

  if (middlewareClazzSet.size === 0) {
    return [];
  }

  const result: GraphNode<ProtoNode, ProtoDependencyMeta>[] = [];
  for (const middlewareClazz of middlewareClazzSet) {
    // Find the proto node for this middleware class
    const middlewareProtoNode = findProtoNodeByClass(globalGraph, middlewareClazz);
    if (middlewareProtoNode) {
      result.push(middlewareProtoNode);
    }
  }

  return result;
}


function findProtoNodeByClass(
globalGraph: GlobalGraph,
clazz: EggProtoImplClass,
): GraphNode<ProtoNode, ProtoDependencyMeta> | undefined {
for (const protoNode of globalGraph.protoGraph.nodes.values()) {
const proto = protoNode.val.proto;
if (ClassProtoDescriptor.isClassProtoDescriptor(proto) && proto.clazz === clazz) {
return protoNode;
}
}
return undefined;
}
Comment on lines +72 to +83
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The findProtoNodeByClass function iterates over all prototypes in globalGraph.protoGraph for each middleware it needs to find. This can cause performance degradation in larger applications with many prototypes, as it results in a complexity of O(N) for each lookup.

To optimize this, consider creating a Map<EggProtoImplClass, GraphNode<ProtoNode, ProtoDependencyMeta>> of all prototypes once at the beginning of the middlewareGraphHook function. This would allow findProtoNodeByClass to perform an O(1) lookup, significantly improving performance.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default () => {
// This file is required by egg
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

module.exports = function() {
const config = {
keys: 'test key',
security: {
csrf: {
ignoreJSON: false,
},
},
};
return config;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"path": "../modules/advice-module"
},
{
"path": "../modules/controller-module"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'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,
};

exports.aopModule = {
package: '@eggjs/tegg-aop-plugin',
enable: true,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
Advice,
AdviceContext,
IAdvice,
} from '@eggjs/tegg/aop';
import { AccessLevel } from '@eggjs/tegg-types';

@Advice({
accessLevel: AccessLevel.PUBLIC,
})
export class AnotherAdvice implements IAdvice {
async around(_ctx: AdviceContext, next: () => Promise<any>) {
const body = await next();
if (body) {
body.anotherAdviceApplied = true;
}
return body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
Advice,
AdviceContext,
IAdvice,
} from '@eggjs/tegg/aop';
import { AccessLevel } from '@eggjs/tegg-types';

@Advice({
accessLevel: AccessLevel.PUBLIC,
})
export class TestAdvice implements IAdvice {
async around(_ctx: AdviceContext, next: () => Promise<any>) {
const body = await next();
if (body) {
body.adviceApplied = true;
}
return body;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "advice-module",
"eggModule": {
"name": "advice-module"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Middleware,
} from '@eggjs/tegg';
import { TestAdvice } from '../advice-module/advice/TestAdvice';
import { AnotherAdvice } from '../advice-module/advice/AnotherAdvice';

@HTTPController({
path: '/test',
})
@Middleware(TestAdvice)
export class TestController {
@HTTPMethod({
method: HTTPMethodEnum.GET,
path: '/class-middleware',
})
async classMiddleware() {
return {
message: 'class middleware',
};
}

@HTTPMethod({
method: HTTPMethodEnum.GET,
path: '/method-middleware',
})
@Middleware(AnotherAdvice)
async methodMiddleware() {
return {
message: 'method middleware',
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "controller-module",
"eggModule": {
"name": "controller-module"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "middleware-graph-app"
}
61 changes: 61 additions & 0 deletions plugin/controller/test/http/middleware-aop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import mm from 'egg-mock';
import path from 'path';
import assert from 'assert';

describe('plugin/controller/test/http/middleware-graph-hook.test.ts', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The describe block name plugin/controller/test/http/middleware-graph-hook.test.ts is inconsistent with the test file name middleware-aop.test.ts. To avoid confusion, it's best to keep these consistent. Consider renaming the describe block to better reflect the file's name or the feature being tested.

Suggested change
describe('plugin/controller/test/http/middleware-graph-hook.test.ts', () => {
describe('plugin/controller/test/http/middleware-aop.test.ts', () => {

let app;

beforeEach(() => {
mm(process.env, 'EGG_TYPESCRIPT', true);
});
Comment on lines +8 to +10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The beforeEach hook sets EGG_TYPESCRIPT to true, but this is already done in the before hook. This makes the setup in beforeEach redundant. You can safely remove this beforeEach block.


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/middleware-graph-app'),
framework: require.resolve('egg'),
});
await app.ready();
});

after(() => {
return app.close();
});

it('should add module reference for class level middleware', async () => {
assert.deepStrictEqual(app.moduleReferences.map(t => t.name), [
'advice-module',
'controller-module',
]);
Comment thread
killagu marked this conversation as resolved.
});

it('class level middleware should work', async () => {
app.mockCsrf();
const res = await app.httpRequest()
.get('/test/class-middleware')
.expect(200);
assert.deepStrictEqual(res.body, {
message: 'class middleware',
adviceApplied: true,
});
});

it('method level middleware should work', async () => {
app.mockCsrf();
const res = await app.httpRequest()
.get('/test/method-middleware')
.expect(200);
assert.deepStrictEqual(res.body, {
message: 'method middleware',
adviceApplied: true,
anotherAdviceApplied: true,
});
});
});
Loading