Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions core/dynamic-inject/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from '@eggjs/tegg-types/dynamic-inject';
export * from './src/QualifierImplUtil';
export * from './src/QualifierImplDecoratorUtil';
export * from './src/FactoryQualifier';
export * from './src/FactoryQualifierUtil';
8 changes: 8 additions & 0 deletions core/dynamic-inject/src/FactoryQualifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { EggAbstractClazz, EggProtoImplClass } from '@eggjs/tegg-types';
import { FactoryQualifierUtil } from './FactoryQualifierUtil';

export function FactoryQualifier(dynamics: EggAbstractClazz | EggAbstractClazz[]) {
return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) {
FactoryQualifierUtil.addProperQualifier(target as EggProtoImplClass, propertyKey, parameterIndex, dynamics);
};
}
31 changes: 31 additions & 0 deletions core/dynamic-inject/src/FactoryQualifierUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MetadataUtil } from '@eggjs/core-decorator';
import { MapUtil, ObjectUtils } from '@eggjs/tegg-common-util';
import { DYNAMIC_RANGE_META_DATA } from '@eggjs/tegg-types';
import type { EggAbstractClazz, EggProtoImplClass } from '@eggjs/tegg-types';

export class FactoryQualifierUtil {

static addProperQualifier(clazz: EggProtoImplClass, property: PropertyKey | undefined, parameterIndex: number | undefined, value: EggAbstractClazz | EggAbstractClazz[]) {
if (typeof parameterIndex === 'number') {
const argNames = ObjectUtils.getConstructorArgNameList(clazz);
const argName = argNames[parameterIndex];
const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, clazz, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
MapUtil.getOrStore(properQualifiers, argName, value);
Comment on lines +8 to +13
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add bounds checking for parameterIndex.

Line 11 accesses argNames[parameterIndex] without verifying that parameterIndex is within bounds. If parameterIndex >= argNames.length, argName will be undefined, which could lead to incorrect metadata storage or silent failures.

🔎 Add validation
   static addProperQualifier(clazz: EggProtoImplClass, property: PropertyKey | undefined, parameterIndex: number | undefined, value: EggAbstractClazz | EggAbstractClazz[]) {
     if (typeof parameterIndex === 'number') {
       const argNames = ObjectUtils.getConstructorArgNameList(clazz);
+      if (parameterIndex >= argNames.length) {
+        throw new Error(`Parameter index ${parameterIndex} is out of bounds for constructor with ${argNames.length} arguments`);
+      }
       const argName = argNames[parameterIndex];
       const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, clazz, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
       MapUtil.getOrStore(properQualifiers, argName, value);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static addProperQualifier(clazz: EggProtoImplClass, property: PropertyKey | undefined, parameterIndex: number | undefined, value: EggAbstractClazz | EggAbstractClazz[]) {
if (typeof parameterIndex === 'number') {
const argNames = ObjectUtils.getConstructorArgNameList(clazz);
const argName = argNames[parameterIndex];
const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, clazz, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
MapUtil.getOrStore(properQualifiers, argName, value);
static addProperQualifier(clazz: EggProtoImplClass, property: PropertyKey | undefined, parameterIndex: number | undefined, value: EggAbstractClazz | EggAbstractClazz[]) {
if (typeof parameterIndex === 'number') {
const argNames = ObjectUtils.getConstructorArgNameList(clazz);
if (parameterIndex >= argNames.length) {
throw new Error(`Parameter index ${parameterIndex} is out of bounds for constructor with ${argNames.length} arguments`);
}
const argName = argNames[parameterIndex];
const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, clazz, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
MapUtil.getOrStore(properQualifiers, argName, value);
🤖 Prompt for AI Agents
In core/dynamic-inject/src/DynamicRangeUtil.ts around lines 8 to 13,
parameterIndex is used to index argNames without bounds checking which can yield
undefined argName; add a guard that validates parameterIndex is a number within
0 <= parameterIndex < argNames.length before using it, and if it is out of range
either throw a clear RangeError (including clazz.name and the invalid index) or
return early (no-op) depending on expected behavior; ensure argName is only
read/used after this validation so metadata is not stored under an undefined
key.

} else {
const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, (clazz as any).constructor, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
MapUtil.getOrStore(properQualifiers, property, value);
}
Comment on lines +14 to +17
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Clarify type handling and validate property parameter.

Two concerns:

  1. Type inconsistency: The parameter is typed as clazz: EggProtoImplClass (a class constructor), but when parameterIndex is undefined (property decorator case), clazz is actually the prototype, and line 15 uses (clazz as any).constructor to get the class. This makes the API unclear. Consider either:

    • Typing the parameter as target: any to reflect the actual decorator contract
    • Normalizing the input at the function start to always work with the class
    • Adding a JSDoc comment explaining the dual nature
  2. Undefined property: When parameterIndex is undefined, property could also be undefined (per its type PropertyKey | undefined), but line 16 uses it directly as a Map key without validation.

🔎 Add property validation
     } else {
+      if (property === undefined) {
+        throw new Error('Property key is required when parameterIndex is undefined');
+      }
       const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, (clazz as any).constructor, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
       MapUtil.getOrStore(properQualifiers, property, value);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, (clazz as any).constructor, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
MapUtil.getOrStore(properQualifiers, property, value);
}
} else {
if (property === undefined) {
throw new Error('Property key is required when parameterIndex is undefined');
}
const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, (clazz as any).constructor, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
MapUtil.getOrStore(properQualifiers, property, value);
}
🤖 Prompt for AI Agents
In core/dynamic-inject/src/DynamicRangeUtil.ts around lines 14 to 17, the
function treats its first argument as both a class constructor and a prototype
and uses property as a Map key without checking for undefined; normalize the
target at the top (e.g., detect if the passed value is a prototype and replace
it with its constructor) or change the parameter name/type to reflect it accepts
either prototype or constructor and add a brief JSDoc explaining the dual
nature, then validate that property is not undefined when parameterIndex is
undefined (throw or handle the error) before calling
MetadataUtil.initOwnMapMetaData/MapUtil.getOrStore so you never use an undefined
key.

}
Comment on lines +8 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The type of clazz is declared as EggProtoImplClass, which is a constructor type. However, when used with a property decorator, this method receives the class's prototype, not the constructor. The code currently handles this with (clazz as any).constructor, but the type signature is misleading and could lead to bugs if this code is refactored.

To improve type safety and clarity, the signature and implementation could be adjusted to correctly reflect the different target types from property and parameter decorators. This change would also require a small adjustment in DynamicRange.ts.

  static addProperQualifier(target: object, property: PropertyKey | undefined, parameterIndex: number | undefined, value: EggAbstractClazz | EggAbstractClazz[]) {
    if (typeof parameterIndex === 'number') {
      // Parameter decorator, target is the constructor
      const clazz = target as EggProtoImplClass;
      const argNames = ObjectUtils.getConstructorArgNameList(clazz);
      const argName = argNames[parameterIndex];
      const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, clazz, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
      MapUtil.getOrStore(properQualifiers, argName, value);
    } else {
      // Property decorator, target is the prototype
      const clazz = (target as any).constructor as EggProtoImplClass;
      const properQualifiers = MetadataUtil.initOwnMapMetaData(DYNAMIC_RANGE_META_DATA, clazz, new Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]>());
      MapUtil.getOrStore(properQualifiers, property, value);
    }
  }


static getProperQualifiers(clazz: EggProtoImplClass, property: PropertyKey): EggAbstractClazz[] {
const properQualifiers: Map<PropertyKey, EggAbstractClazz | EggAbstractClazz[]> | undefined = MetadataUtil.getMetaData(DYNAMIC_RANGE_META_DATA, clazz);
const dynamics = properQualifiers?.get(property);
if (!dynamics) {
return [];
}
if (Array.isArray(dynamics)) {
return dynamics;
}
return [ dynamics ];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ContextProto, Inject } from '@eggjs/core-decorator';
import { EggObjectFactory } from '@eggjs/tegg-dynamic-inject';
import { FactoryQualifier } from '../../../../src/FactoryQualifier';
import { AbstractContextHello } from '../base/AbstractContextHello';
import { ContextHelloType } from '../base/FooType';

@ContextProto()
export class HelloService {
@Inject()
@FactoryQualifier([ AbstractContextHello ])
private readonly eggObjectFactory: EggObjectFactory;

async hello(): Promise<string[]> {
const helloImpls = await Promise.all([
this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.FOO),
this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.BAR),
]);
const msgs = helloImpls.map(helloImpl => helloImpl.hello());
return msgs;
}
}
9 changes: 9 additions & 0 deletions core/dynamic-inject/test/typing.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import path from 'path';
import coffee from 'coffee';
import { HelloService } from './fixtures/modules/dynamicRange/HelloService';
import { AbstractContextHello } from './fixtures/modules/base/AbstractContextHello';
import { FactoryQualifierUtil } from '../src/FactoryQualifierUtil';
import assert from 'assert';

describe('test/typing.test.ts', () => {
it('should check enum value', async () => {
Expand All @@ -23,4 +27,9 @@ describe('test/typing.test.ts', () => {
.notExpect('code', 0)
.end();
});

it('should dynamic range run', async () => {
const dynamics = FactoryQualifierUtil.getProperQualifiers(HelloService, 'eggObjectFactory');
assert(dynamics.includes(AbstractContextHello));
});
});
27 changes: 26 additions & 1 deletion core/runtime/src/impl/EggObjectImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
EggObjectLifecycle,
EggObjectLifeCycleContext,
EggObjectName,
EggProtoImplClass,
EggPrototype, ObjectInfo, QualifierInfo,
} from '@eggjs/tegg-types';
import { EggObjectStatus, InjectType, ObjectInitType } from '@eggjs/tegg-types';
Expand All @@ -12,6 +13,7 @@ import { EggObjectLifecycleUtil } from '../model/EggObject';
import { EggContainerFactory } from '../factory/EggContainerFactory';
import { EggObjectUtil } from './EggObjectUtil';
import { ContextHandler } from '../model/ContextHandler';
import { FactoryQualifierUtil } from '@eggjs/tegg-dynamic-inject';

export default class EggObjectImpl implements EggObject {
private _obj: object;
Expand Down Expand Up @@ -59,7 +61,30 @@ export default class EggObjectImpl implements EggObject {
if (this.proto.initType !== ObjectInitType.CONTEXT && injectObject.proto.initType === ObjectInitType.CONTEXT) {
this.injectProperty(injectObject.refName, EggObjectUtil.contextEggObjectGetProperty(proto, injectObject.objName));
} else {
const injectObj = await EggContainerFactory.getOrCreateEggObject(proto, injectObject.objName);
let injectObj = await EggContainerFactory.getOrCreateEggObject(proto, injectObject.objName);
if (injectObj.proto.name === 'eggObjectFactory') {
Copy link
Contributor

Choose a reason for hiding this comment

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

这个逻辑应该写在 eggObjectFactory 内部,不应该写在 EggObjectImpl。

const dynamics = FactoryQualifierUtil.getProperQualifiers(this._obj.constructor as unknown as EggProtoImplClass, injectObject.refName);
if (dynamics.length > 0) {
const obj: any = {
dynamics,
};
const originalGetEggObject = (injectObj.obj as any).getEggObject;
Object.setPrototypeOf(obj, injectObj.obj);
obj.getEggObject = function(...args: any[]) {
if (!this.dynamics.includes(args[0])) {
throw new Error(`${args[0].name} is not in dynamic range: ${this.dynamics.map(item => item.name).join(', ')}`);
}
return originalGetEggObject.apply(this, args);
Comment on lines +73 to +77
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add validation and fix reference equality check.

Two issues:

  1. Missing argument validation: Line 73 accesses args[0] without checking if arguments were provided, which could cause runtime errors.

  2. Reference equality issue: Line 74 uses this.dynamics.includes(args[0]) which performs reference equality checks. If the abstract class passed at runtime is a different reference to the same class (e.g., re-imported or loaded from different module context), the check will fail even though it logically represents the same type.

🔎 Proposed fix
 obj.getEggObject = function(...args: any[]) {
+  if (args.length === 0 || !args[0]) {
+    throw new Error('Dynamic class argument is required');
+  }
-  if (!this.dynamics.includes(args[0])) {
+  const requestedDynamic = args[0];
+  const isAllowed = this.dynamics.some((allowedDynamic: any) => 
+    allowedDynamic === requestedDynamic || 
+    allowedDynamic.name === requestedDynamic.name
+  );
+  if (!isAllowed) {
     throw new Error(`${args[0].name} is not in dynamic range: ${this.dynamics.map(item => item.name).join(', ')}`);
   }
   return originalGetEggObject.apply(this, args);
 };
🤖 Prompt for AI Agents
In core/runtime/src/impl/EggObjectImpl.ts around lines 73 to 77, add argument
validation and replace the strict reference equality check: first ensure args[0]
exists and is a valid constructor/type (throw a clear error if missing or
invalid), then determine membership in this.dynamics by comparing logical
identity rather than reference — e.g., search this.dynamics for an entry that
either is the same reference or has the same name (or other stable identifier)
as args[0]; if no match is found throw the existing error message using the
provided argument name, and only then call originalGetEggObject.apply(this,
args).

};
injectObj = Object.create(injectObj);
Object.defineProperty(injectObj, 'obj', {
value: obj,
writable: true,
enumerable: true,
configurable: true,
});
}
}
this.injectProperty(injectObject.refName, EggObjectUtil.eggObjectGetProperty(injectObj));
}
}));
Expand Down
2 changes: 2 additions & 0 deletions core/types/core-decorator/enum/Qualifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export const QUALIFIER_META_DATA = Symbol.for('EggPrototype#qualifier');

export const PROPERTY_QUALIFIER_META_DATA = Symbol.for('EggPrototype#propertyQualifier');

export const DYNAMIC_RANGE_META_DATA = Symbol.for('EggPrototype#FactoryQualifier');

export const CONSTRUCTOR_QUALIFIER_META_DATA = Symbol.for('EggPrototype#constructorQualifier');
17 changes: 17 additions & 0 deletions plugin/tegg/test/DynamicInject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,21 @@ describe('plugin/tegg/test/DynamicInject.test.ts', () => {
'hello, bar(singleton:1)',
]);
});

it('dynamic range should work', async () => {
app.mockCsrf();
const res = await app.httpRequest()
.get('/factoryQualifier')
.expect(200);
assert.deepStrictEqual(res.body, [
'AbstractContextHello is not in dynamic range: AbstractSingletonHello',
]);
const res2 = await app.httpRequest()
.get('/singletonDynamicInject')
.expect(200);
assert.deepStrictEqual(res2.body, [
'hello, foo(singleton:2)',
'hello, bar(singleton:2)',
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ export default class App extends Controller {
this.ctx.status = 200;
this.ctx.body = msgs;
}
async factoryQualifier() {
const helloService: HelloService = await (this.ctx.module as any).dynamicInjectModule.factoryQualifierService;
const msgs = await helloService.hello();
this.ctx.status = 200;
this.ctx.body = msgs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { Application } from 'egg';
module.exports = (app: Application) => {
app.router.get('/dynamicInject', app.controller.app.dynamicInject);
app.router.get('/singletonDynamicInject', app.controller.app.singletonDynamicInject);
app.router.get('/factoryQualifier', app.controller.app.factoryQualifier);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AccessLevel, Inject, EggObjectFactory, ContextProto, FactoryQualifier } from '@eggjs/tegg';
import { ContextHelloType } from './FooType';
import { AbstractSingletonHello } from './AbstractSingletonHello';
import { AbstractContextHello } from './AbstractContextHello';

@ContextProto({
accessLevel: AccessLevel.PUBLIC,
})
export class FactoryQualifierService {
@Inject()
@FactoryQualifier(AbstractSingletonHello)
private readonly eggObjectFactory: EggObjectFactory;

async hello(): Promise<string[]> {
try {
const helloImpl = await this.eggObjectFactory.getEggObject(AbstractContextHello, ContextHelloType.FOO);
const msgs = [ helloImpl.hello() ];
return msgs;
} catch (err) {
return [ err.message ];
}
}
}
Loading