diff --git a/README.md b/README.md index 9defd8c4..05366fb2 100644 --- a/README.md +++ b/README.md @@ -498,6 +498,25 @@ export class HelloService { } ``` +也可在构造函数中使用 `Inject` 注解。注意 property 和 构造函数两种模式只能选一种,不能混用。 + +```typescript +import { EggLogger } from 'egg'; +import { Inject } from '@eggjs/tegg'; + +@ContextProto() +export class HelloService { + constructor(@Inject() readonly logger: EggLogger) { + } + + async hello(user: User): Promise { + this.logger.info(`[HelloService] hello ${user.name}`); + const echoResponse = await this.echoAdapter.echo({ name: user.name }); + return `hello, ${echoResponse.name}`; + } +} +``` + ##### 复杂示例 ```typescript diff --git a/core/common-util/src/ObjectUtils.ts b/core/common-util/src/ObjectUtils.ts index 3fc97298..ec2a04fe 100644 --- a/core/common-util/src/ObjectUtils.ts +++ b/core/common-util/src/ObjectUtils.ts @@ -1,3 +1,5 @@ +import { EggProtoImplClass } from '@eggjs/tegg-types'; + export class ObjectUtils { static getProperties(obj: object): string[] { const properties: string[] = []; @@ -34,4 +36,19 @@ export class ObjectUtils { }).filter(arg => arg.length); return argNames; } + + static getConstructorArgNameList(clazz: EggProtoImplClass): string[] { + if (clazz.length === 0) { + return []; + } + const classString = clazz.toString(); + const constructorMatch = classString.match(/constructor\s*\(([^)]+)\)/); + if (!constructorMatch) { + return []; + } + const params = constructorMatch[1].split(',').map(param => param.trim()); + return params.map(param => param.match(/(\w+)\s*(?=\s*(?:=|\/\/|\s*$))/)) + .filter(Boolean) + .map(match => match![0].trim()); + } } diff --git a/core/common-util/test/ObjectUtil.test.ts b/core/common-util/test/ObjectUtil.test.ts index d76d1a59..5d2a0b65 100644 --- a/core/common-util/test/ObjectUtil.test.ts +++ b/core/common-util/test/ObjectUtil.test.ts @@ -1,6 +1,27 @@ import assert from 'node:assert'; import { ObjectUtils } from '..'; +export function InitTypeQualifier() { + return function(_target: any, _propertyKey?: PropertyKey, _parameterIndex?: number) { + console.log(_target, _propertyKey, _parameterIndex); + // ... + }; +} + +export function ModuleQualifier(_foo: string) { + return function(_target: any, _propertyKey?: PropertyKey, _parameterIndex?: number) { + console.log(_target, _propertyKey, _parameterIndex, _foo); + // ... + }; +} + +export function Inject(_arg?: any) { + return function(_target: any, _propertyKey?: PropertyKey, _parameterIndex?: number) { + console.log(_target, _propertyKey, _parameterIndex, _arg); + // ... + }; +} + describe('test/ObjectUtil.test.ts', () => { it('should work', () => { function mockFunction(/* test */ctx: object, foo: string, bar = '233') { @@ -11,4 +32,24 @@ describe('test/ObjectUtil.test.ts', () => { const argNames = ObjectUtils.getFunctionArgNameList(mockFunction); assert.deepStrictEqual(argNames, [ 'ctx', 'foo', 'bar' ]); }); + + it('getConstructorArgNameList should work', () => { + class ConstructorObject { + constructor( + @InitTypeQualifier() + @ModuleQualifier('foo') + @Inject({ name: 'fooCache' }) readonly xCache: any, // fpp... + /* test */ @Inject() readonly cache: unknown, + readonly v233 = 666, + ) { + } + } + + const argNames = ObjectUtils.getConstructorArgNameList(ConstructorObject); + assert.deepStrictEqual(argNames, [ + 'xCache', + 'cache', + 'v233', + ]); + }); }); diff --git a/core/core-decorator/src/decorator/ConfigSource.ts b/core/core-decorator/src/decorator/ConfigSource.ts index f8e1508f..178c72db 100644 --- a/core/core-decorator/src/decorator/ConfigSource.ts +++ b/core/core-decorator/src/decorator/ConfigSource.ts @@ -3,7 +3,7 @@ import { ConfigSourceQualifierAttribute } from '@eggjs/tegg-types'; import type { EggProtoImplClass } from '@eggjs/tegg-types'; export function ConfigSourceQualifier(moduleName: string) { - return function(target: any, propertyKey: PropertyKey) { - QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, propertyKey, ConfigSourceQualifierAttribute, moduleName); + return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) { + QualifierUtil.addInjectQualifier(target as EggProtoImplClass, propertyKey, parameterIndex, ConfigSourceQualifierAttribute, moduleName); }; } diff --git a/core/core-decorator/src/decorator/EggQualifier.ts b/core/core-decorator/src/decorator/EggQualifier.ts index af291945..ad953136 100644 --- a/core/core-decorator/src/decorator/EggQualifier.ts +++ b/core/core-decorator/src/decorator/EggQualifier.ts @@ -3,7 +3,7 @@ import type { EggProtoImplClass, EggType } from '@eggjs/tegg-types'; import { QualifierUtil } from '../util/QualifierUtil'; export function EggQualifier(eggType: EggType) { - return function(target: any, propertyKey: PropertyKey) { - QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, propertyKey, EggQualifierAttribute, eggType); + return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) { + QualifierUtil.addInjectQualifier(target as EggProtoImplClass, propertyKey, parameterIndex, EggQualifierAttribute, eggType); }; } diff --git a/core/core-decorator/src/decorator/InitTypeQualifier.ts b/core/core-decorator/src/decorator/InitTypeQualifier.ts index 3da0826a..7ffdaee0 100644 --- a/core/core-decorator/src/decorator/InitTypeQualifier.ts +++ b/core/core-decorator/src/decorator/InitTypeQualifier.ts @@ -3,7 +3,7 @@ import type { EggProtoImplClass, ObjectInitTypeLike } from '@eggjs/tegg-types'; import { QualifierUtil } from '../util/QualifierUtil'; export function InitTypeQualifier(initType: ObjectInitTypeLike) { - return function(target: any, propertyKey: PropertyKey) { - QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, propertyKey, InitTypeQualifierAttribute, initType); + return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) { + QualifierUtil.addInjectQualifier(target as EggProtoImplClass, propertyKey, parameterIndex, InitTypeQualifierAttribute, initType); }; } diff --git a/core/core-decorator/src/decorator/Inject.ts b/core/core-decorator/src/decorator/Inject.ts index f5b758a9..8dbef837 100644 --- a/core/core-decorator/src/decorator/Inject.ts +++ b/core/core-decorator/src/decorator/Inject.ts @@ -1,8 +1,9 @@ -import type { EggProtoImplClass, InjectObjectInfo, InjectParams } from '@eggjs/tegg-types'; +import { EggProtoImplClass, InjectObjectInfo, InjectConstructorInfo, InjectParams, InjectType } from '@eggjs/tegg-types'; import { PrototypeUtil } from '../util/PrototypeUtil'; +import { ObjectUtils } from '@eggjs/tegg-common-util'; export function Inject(param?: InjectParams | string) { - return function(target: any, propertyKey: PropertyKey) { + function propertyInject(target: any, propertyKey: PropertyKey) { let objName: PropertyKey | undefined; if (!param) { // try to read design:type from proto @@ -22,6 +23,30 @@ export function Inject(param?: InjectParams | string) { objName: objName || propertyKey, }; + PrototypeUtil.setInjectType(target.constructor, InjectType.PROPERTY); PrototypeUtil.addInjectObject(target.constructor as EggProtoImplClass, injectObject); + } + + function constructorInject(target: any, parameterIndex: number) { + const argNames = ObjectUtils.getConstructorArgNameList(target); + const argName = argNames[parameterIndex]; + // TODO get objName from design:type + const objName = typeof param === 'string' ? param : param?.name; + const injectObject: InjectConstructorInfo = { + refIndex: parameterIndex, + refName: argName, + objName: objName || argName, + }; + + PrototypeUtil.setInjectType(target, InjectType.CONSTRUCTOR); + PrototypeUtil.addInjectConstructor(target as EggProtoImplClass, injectObject); + } + + return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) { + if (typeof parameterIndex === 'undefined') { + propertyInject(target, propertyKey!); + } else { + constructorInject(target, parameterIndex!); + } }; } diff --git a/core/core-decorator/src/decorator/ModuleQualifier.ts b/core/core-decorator/src/decorator/ModuleQualifier.ts index c69e9265..b4b59818 100644 --- a/core/core-decorator/src/decorator/ModuleQualifier.ts +++ b/core/core-decorator/src/decorator/ModuleQualifier.ts @@ -3,7 +3,7 @@ import type { EggProtoImplClass } from '@eggjs/tegg-types'; import { QualifierUtil } from '../util/QualifierUtil'; export function ModuleQualifier(moduleName: string) { - return function(target: any, propertyKey: PropertyKey) { - QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, propertyKey, LoadUnitNameQualifierAttribute, moduleName); + return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) { + QualifierUtil.addInjectQualifier(target as EggProtoImplClass, propertyKey, parameterIndex, LoadUnitNameQualifierAttribute, moduleName); }; } diff --git a/core/core-decorator/src/util/MetadataUtil.ts b/core/core-decorator/src/util/MetadataUtil.ts index 5f57991f..4621d362 100644 --- a/core/core-decorator/src/util/MetadataUtil.ts +++ b/core/core-decorator/src/util/MetadataUtil.ts @@ -31,6 +31,20 @@ export class MetadataUtil { return this.getMetaData(metadataKey, clazz) || []; } + /** + * init array metadata + * not inherit parent metadata + * return value true means use default value + * return value false means use map value + */ + static initArrayMetaData(metadataKey: MetaDataKey, clazz: EggProtoImplClass, defaultValue: Array): Array { + const ownMetaData: Array | undefined = this.getOwnMetaData(metadataKey, clazz); + if (!ownMetaData) { + this.defineMetaData(metadataKey, defaultValue, clazz); + } + return this.getOwnMetaData>(metadataKey, clazz)!; + } + /** * init own array metadata * if parent metadata exists, inherit diff --git a/core/core-decorator/src/util/PrototypeUtil.ts b/core/core-decorator/src/util/PrototypeUtil.ts index 650c2a37..27f4921a 100644 --- a/core/core-decorator/src/util/PrototypeUtil.ts +++ b/core/core-decorator/src/util/PrototypeUtil.ts @@ -1,10 +1,12 @@ -import type { +import { EggMultiInstanceCallbackPrototypeInfo, EggMultiInstancePrototypeInfo, EggProtoImplClass, EggPrototypeInfo, EggPrototypeName, + InjectConstructorInfo, InjectObjectInfo, + InjectType, MultiInstancePrototypeGetObjectsContext, } from '@eggjs/tegg-types'; import { MetadataUtil } from './MetadataUtil'; @@ -17,6 +19,8 @@ export class PrototypeUtil { static readonly MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY = Symbol.for('EggPrototype.MultiInstanceStaticProperty'); static readonly MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY = Symbol.for('EggPrototype.MultiInstanceCallbackProperty'); static readonly INJECT_OBJECT_NAME_SET = Symbol.for('EggPrototype.injectObjectNames'); + static readonly INJECT_TYPE = Symbol.for('EggPrototype.injectType'); + static readonly INJECT_CONSTRUCTOR_NAME_SET = Symbol.for('EggPrototype.injectConstructorNames'); static readonly CLAZZ_PROTO = Symbol.for('EggPrototype.clazzProto'); /** @@ -24,7 +28,7 @@ export class PrototypeUtil { * @param {Function} clazz - */ static setIsEggPrototype(clazz: EggProtoImplClass) { - MetadataUtil.defineMetaData(this.IS_EGG_OBJECT_PROTOTYPE, true, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.IS_EGG_OBJECT_PROTOTYPE, true, clazz); } /** @@ -32,7 +36,7 @@ export class PrototypeUtil { * @param {Function} clazz - */ static isEggPrototype(clazz: EggProtoImplClass): boolean { - return MetadataUtil.getBooleanMetaData(this.IS_EGG_OBJECT_PROTOTYPE, clazz); + return MetadataUtil.getBooleanMetaData(PrototypeUtil.IS_EGG_OBJECT_PROTOTYPE, clazz); } /** @@ -40,7 +44,7 @@ export class PrototypeUtil { * @param {Function} clazz - */ static setIsEggMultiInstancePrototype(clazz: EggProtoImplClass) { - MetadataUtil.defineMetaData(this.IS_EGG_OBJECT_MULTI_INSTANCE_PROTOTYPE, true, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.IS_EGG_OBJECT_MULTI_INSTANCE_PROTOTYPE, true, clazz); } /** @@ -48,7 +52,7 @@ export class PrototypeUtil { * @param {Function} clazz - */ static isEggMultiInstancePrototype(clazz: EggProtoImplClass): boolean { - return MetadataUtil.getBooleanMetaData(this.IS_EGG_OBJECT_MULTI_INSTANCE_PROTOTYPE, clazz); + return MetadataUtil.getBooleanMetaData(PrototypeUtil.IS_EGG_OBJECT_MULTI_INSTANCE_PROTOTYPE, clazz); } /** @@ -57,7 +61,7 @@ export class PrototypeUtil { * @param {string} filePath - */ static setFilePath(clazz: EggProtoImplClass, filePath: string) { - MetadataUtil.defineMetaData(this.FILE_PATH, filePath, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.FILE_PATH, filePath, clazz); } /** @@ -65,7 +69,7 @@ export class PrototypeUtil { * @param {Function} clazz - */ static getFilePath(clazz: EggProtoImplClass): string | undefined { - return MetadataUtil.getMetaData(this.FILE_PATH, clazz); + return MetadataUtil.getMetaData(PrototypeUtil.FILE_PATH, clazz); } /** @@ -74,7 +78,7 @@ export class PrototypeUtil { * @param {EggPrototypeInfo} property - */ static setProperty(clazz: EggProtoImplClass, property: EggPrototypeInfo) { - MetadataUtil.defineMetaData(this.PROTOTYPE_PROPERTY, property, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.PROTOTYPE_PROPERTY, property, clazz); } /** @@ -83,25 +87,25 @@ export class PrototypeUtil { * @return {EggPrototypeInfo} - */ static getProperty(clazz: EggProtoImplClass): EggPrototypeInfo | undefined { - return MetadataUtil.getMetaData(this.PROTOTYPE_PROPERTY, clazz); + return MetadataUtil.getMetaData(PrototypeUtil.PROTOTYPE_PROPERTY, clazz); } static getInitType(clazz: EggProtoImplClass, ctx: MultiInstancePrototypeGetObjectsContext): string | undefined { - const property = this.getProperty(clazz) ?? this.getMultiInstanceProperty(clazz, ctx); + const property = PrototypeUtil.getProperty(clazz) ?? PrototypeUtil.getMultiInstanceProperty(clazz, ctx); return property?.initType; } static getAccessLevel(clazz: EggProtoImplClass, ctx: MultiInstancePrototypeGetObjectsContext): string | undefined { - const property = this.getProperty(clazz) ?? this.getMultiInstanceProperty(clazz, ctx); + const property = PrototypeUtil.getProperty(clazz) ?? PrototypeUtil.getMultiInstanceProperty(clazz, ctx); return property?.accessLevel; } static getObjNames(clazz: EggProtoImplClass, ctx: MultiInstancePrototypeGetObjectsContext): EggPrototypeName[] { - const property = this.getProperty(clazz); + const property = PrototypeUtil.getProperty(clazz); if (property) { return [ property.name ]; } - const multiInstanceProperty = this.getMultiInstanceProperty(clazz, ctx); + const multiInstanceProperty = PrototypeUtil.getMultiInstanceProperty(clazz, ctx); return multiInstanceProperty?.objects.map(t => t.name) || []; } @@ -111,7 +115,7 @@ export class PrototypeUtil { * @param {EggPrototypeInfo} property - */ static setMultiInstanceStaticProperty(clazz: EggProtoImplClass, property: EggMultiInstancePrototypeInfo) { - MetadataUtil.defineMetaData(this.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, property, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, property, clazz); } /** @@ -120,7 +124,7 @@ export class PrototypeUtil { * @param {EggPrototypeInfo} property - */ static setMultiInstanceCallbackProperty(clazz: EggProtoImplClass, property: EggMultiInstanceCallbackPrototypeInfo) { - MetadataUtil.defineMetaData(this.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, property, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, property, clazz); } /** @@ -129,11 +133,11 @@ export class PrototypeUtil { * @param {MultiInstancePrototypeGetObjectsContext} ctx - */ static getMultiInstanceProperty(clazz: EggProtoImplClass, ctx: MultiInstancePrototypeGetObjectsContext): EggMultiInstancePrototypeInfo | undefined { - const metadata = MetadataUtil.getMetaData(this.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, clazz); + const metadata = MetadataUtil.getMetaData(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, clazz); if (metadata) { return metadata; } - const callBackMetadata = MetadataUtil.getMetaData(this.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, clazz); + const callBackMetadata = MetadataUtil.getMetaData(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, clazz); if (callBackMetadata) { return { ...callBackMetadata, @@ -142,24 +146,61 @@ export class PrototypeUtil { } } + static setInjectType(clazz: EggProtoImplClass, type: InjectType) { + const injectType: InjectType | undefined = MetadataUtil.getMetaData(PrototypeUtil.INJECT_TYPE, clazz); + if (!injectType) { + MetadataUtil.defineMetaData(PrototypeUtil.INJECT_TYPE, type, clazz); + } else if (injectType !== type) { + throw new Error(`class ${clazz.name} already use inject type ${injectType} can not use ${type}`); + } + } + static addInjectObject(clazz: EggProtoImplClass, injectObject: InjectObjectInfo) { - const objs: InjectObjectInfo[] = MetadataUtil.initOwnArrayMetaData(this.INJECT_OBJECT_NAME_SET, clazz, []); + const objs: InjectObjectInfo[] = MetadataUtil.initOwnArrayMetaData(PrototypeUtil.INJECT_OBJECT_NAME_SET, clazz, []); objs.push(injectObject); - MetadataUtil.defineMetaData(this.INJECT_OBJECT_NAME_SET, objs, clazz); + MetadataUtil.defineMetaData(PrototypeUtil.INJECT_OBJECT_NAME_SET, objs, clazz); + } + + static addInjectConstructor(clazz: EggProtoImplClass, injectConstructorInfo: InjectConstructorInfo) { + const objs: InjectConstructorInfo[] = MetadataUtil.initArrayMetaData(PrototypeUtil.INJECT_CONSTRUCTOR_NAME_SET, clazz, []); + objs.push(injectConstructorInfo); + MetadataUtil.defineMetaData(PrototypeUtil.INJECT_CONSTRUCTOR_NAME_SET, objs, clazz); } - static getInjectObjects(clazz: EggProtoImplClass): Array { - return MetadataUtil.getArrayMetaData(this.INJECT_OBJECT_NAME_SET, clazz); + static getInjectType(clazz: EggProtoImplClass): InjectType | undefined { + const injectType: InjectType | undefined = MetadataUtil.getMetaData(PrototypeUtil.INJECT_TYPE, clazz); + return injectType; } + static getInjectObjects(clazz: EggProtoImplClass): Array { + const injectType: InjectType | undefined = MetadataUtil.getMetaData(PrototypeUtil.INJECT_TYPE, clazz); + if (!injectType) { + return []; + } + if (injectType === InjectType.CONSTRUCTOR) { + return MetadataUtil.getArrayMetaData(PrototypeUtil.INJECT_CONSTRUCTOR_NAME_SET, clazz) + .sort((a, b) => { + return a.refIndex - b.refIndex; + }); + } + return MetadataUtil.getArrayMetaData(PrototypeUtil.INJECT_OBJECT_NAME_SET, clazz); + } + + // static getInjectConstructors(clazz: EggProtoImplClass): Array { + // return MetadataUtil.getArrayMetaData(PrototypeUtil.INJECT_CONSTRUCTOR_NAME_SET, clazz) + // .sort((a, b) => { + // return a.refIndex - b.refIndex; + // }); + // } + // TODO fix proto type static getClazzProto(clazz: EggProtoImplClass): object | undefined { - return MetadataUtil.getMetaData(this.CLAZZ_PROTO, clazz); + return MetadataUtil.getMetaData(PrototypeUtil.CLAZZ_PROTO, clazz); } // TODO fix proto type static setClazzProto(clazz: EggProtoImplClass, proto: object) { - return MetadataUtil.defineMetaData(this.CLAZZ_PROTO, proto, clazz); + return MetadataUtil.defineMetaData(PrototypeUtil.CLAZZ_PROTO, proto, clazz); } static getDesignType(clazz: EggProtoImplClass, propKey?: PropertyKey) { diff --git a/core/core-decorator/src/util/QualifierUtil.ts b/core/core-decorator/src/util/QualifierUtil.ts index 17ff6081..5b7a4ebf 100644 --- a/core/core-decorator/src/util/QualifierUtil.ts +++ b/core/core-decorator/src/util/QualifierUtil.ts @@ -1,4 +1,4 @@ -import { MapUtil } from '@eggjs/tegg-common-util'; +import { MapUtil, ObjectUtils } from '@eggjs/tegg-common-util'; import { PROPERTY_QUALIFIER_META_DATA, QUALIFIER_META_DATA } from '@eggjs/tegg-types'; import type { EggProtoImplClass, QualifierAttribute, QualifierInfo, QualifierValue } from '@eggjs/tegg-types'; import { MetadataUtil } from './MetadataUtil'; @@ -24,6 +24,16 @@ export class QualifierUtil { return res; } + static addInjectQualifier(clazz: EggProtoImplClass, property: PropertyKey | undefined, parameterIndex: number | undefined, attribute: QualifierAttribute, value: QualifierValue) { + if (typeof parameterIndex === 'number') { + const argNames = ObjectUtils.getConstructorArgNameList(clazz); + const argName = argNames[parameterIndex]; + QualifierUtil.addProperQualifier(clazz, argName, attribute, value); + } else { + QualifierUtil.addProperQualifier((clazz as any).constructor, property!, attribute, value); + } + } + static addProperQualifier(clazz: EggProtoImplClass, property: PropertyKey, attribute: QualifierAttribute, value: QualifierValue) { const properQualifiers = MetadataUtil.initOwnMapMetaData(PROPERTY_QUALIFIER_META_DATA, clazz, new Map>()); const qualifiers = MapUtil.getOrStore(properQualifiers, property, new Map()); diff --git a/core/core-decorator/test/decorators.test.ts b/core/core-decorator/test/decorators.test.ts index 9bbcfb7e..13cb6c4e 100644 --- a/core/core-decorator/test/decorators.test.ts +++ b/core/core-decorator/test/decorators.test.ts @@ -15,6 +15,7 @@ import SingletonCache from './fixtures/decators/SingletonCache'; import { PrototypeUtil, QualifierUtil } from '..'; import QualifierCacheService from './fixtures/decators/QualifierCacheService'; import { FOO_ATTRIBUTE, FooLogger } from './fixtures/decators/FooLogger'; +import { ConstructorObject } from './fixtures/decators/ConstructorObject'; describe('test/decorator.test.ts', () => { describe('ContextProto', () => { @@ -66,6 +67,14 @@ describe('test/decorator.test.ts', () => { }]; assert.deepStrictEqual(PrototypeUtil.getInjectObjects(CacheService), expectInjectInfo); }); + + it('constructor should work', () => { + const injectConstructors = PrototypeUtil.getInjectObjects(ConstructorObject); + assert.deepStrictEqual(injectConstructors, [ + { refIndex: 0, refName: 'xCache', objName: 'fooCache' }, + { refIndex: 1, refName: 'cache', objName: 'cache' }, + ]); + }); }); describe('Qualifier', () => { @@ -89,6 +98,15 @@ describe('test/decorator.test.ts', () => { QualifierUtil.getProperQualifier(QualifierCacheService, property, Symbol.for('Qualifier.InitType')) === ObjectInitType.SINGLETON, ); }); + it('constructor should work', () => { + const constructorQualifiers = QualifierUtil.getProperQualifiers(ConstructorObject, 'xCache'); + const constructorQualifiers2 = QualifierUtil.getProperQualifiers(ConstructorObject, 'cache'); + assert.deepStrictEqual(constructorQualifiers, [ + { attribute: Symbol.for('Qualifier.LoadUnitName'), value: 'foo' }, + { attribute: Symbol.for('Qualifier.InitType'), value: ObjectInitType.SINGLETON }, + ]); + assert.deepStrictEqual(constructorQualifiers2, []); + }); }); describe('MultiInstanceProto', () => { diff --git a/core/core-decorator/test/fixtures/decators/ConstructorObject.ts b/core/core-decorator/test/fixtures/decators/ConstructorObject.ts new file mode 100644 index 00000000..49768dd3 --- /dev/null +++ b/core/core-decorator/test/fixtures/decators/ConstructorObject.ts @@ -0,0 +1,17 @@ +import { SingletonProto } from '../../../src/decorator/SingletonProto'; +import { ICache } from './ICache'; +import { Inject } from '../../../src/decorator/Inject'; +import { InitTypeQualifier } from '../../../src/decorator/InitTypeQualifier'; +import { ObjectInitType } from '@eggjs/tegg-types'; +import { ModuleQualifier } from '../../../src/decorator/ModuleQualifier'; + +@SingletonProto() +export class ConstructorObject { + constructor( + @InitTypeQualifier(ObjectInitType.SINGLETON) + @ModuleQualifier('foo') + @Inject({ name: 'fooCache'}) readonly xCache: ICache, + @Inject() readonly cache: ICache, + ) { + } +} diff --git a/core/dal-decorator/src/decorator/DataSourceQualifier.ts b/core/dal-decorator/src/decorator/DataSourceQualifier.ts index ab755b6d..44e50f81 100644 --- a/core/dal-decorator/src/decorator/DataSourceQualifier.ts +++ b/core/dal-decorator/src/decorator/DataSourceQualifier.ts @@ -3,8 +3,8 @@ import type { EggProtoImplClass } from '@eggjs/tegg-types'; import { QualifierUtil } from '@eggjs/core-decorator'; export function DataSourceQualifier(dataSourceName: string) { - return function(target: any, propertyKey: PropertyKey) { - QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, - propertyKey, DataSourceQualifierAttribute, dataSourceName); + return function(target: any, propertyKey: PropertyKey, parameterIndex?: number) { + QualifierUtil.addInjectQualifier(target.constructor as EggProtoImplClass, + propertyKey, parameterIndex, DataSourceQualifierAttribute, dataSourceName); }; } diff --git a/core/metadata/src/impl/EggPrototypeBuilder.ts b/core/metadata/src/impl/EggPrototypeBuilder.ts index 222e4705..d5f8aaf1 100644 --- a/core/metadata/src/impl/EggPrototypeBuilder.ts +++ b/core/metadata/src/impl/EggPrototypeBuilder.ts @@ -1,22 +1,23 @@ import assert from 'node:assert'; -import { PrototypeUtil, QualifierUtil } from '@eggjs/core-decorator'; -import { - ObjectInitType, - InitTypeQualifierAttribute, - DEFAULT_PROTO_IMPL_TYPE, -} from '@eggjs/tegg-types'; +import { InjectType, PrototypeUtil, QualifierUtil } from '@eggjs/core-decorator'; import type { AccessLevel, EggProtoImplClass, EggPrototype, EggPrototypeLifecycleContext, - EggPrototypeName, + EggPrototypeName, InjectConstructor, InjectObject, InjectObjectProto, LoadUnit, ObjectInitTypeLike, QualifierInfo, } from '@eggjs/tegg-types'; +import { + DEFAULT_PROTO_IMPL_TYPE, + InitTypeQualifierAttribute, + InjectConstructorProto, + ObjectInitType, +} from '@eggjs/tegg-types'; import { EggPrototypeFactory } from '../factory/EggPrototypeFactory'; import { IdenticalUtil } from '@eggjs/tegg-lifecycle'; import { EggPrototypeImpl } from './EggPrototypeImpl'; @@ -29,7 +30,8 @@ export class EggPrototypeBuilder { private initType: ObjectInitTypeLike; private accessLevel: AccessLevel; private filepath: string; - private injectObjects: Array = []; + private injectType: InjectType | undefined; + private injectObjects: Array = []; private loadUnit: LoadUnit; private qualifiers: QualifierInfo[] = []; private className?: string; @@ -45,6 +47,7 @@ export class EggPrototypeBuilder { builder.initType = ctx.prototypeInfo.initType; builder.accessLevel = ctx.prototypeInfo.accessLevel; builder.filepath = filepath!; + builder.injectType = PrototypeUtil.getInjectType(clazz); builder.injectObjects = PrototypeUtil.getInjectObjects(clazz) || []; builder.loadUnit = loadUnit; builder.qualifiers = [ @@ -103,16 +106,26 @@ export class EggPrototypeBuilder { } public build(): EggPrototype { - const injectObjectProtos: InjectObjectProto[] = []; + const injectObjectProtos: Array = []; for (const injectObject of this.injectObjects) { const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName); const proto = this.findInjectObjectPrototype(injectObject); - injectObjectProtos.push({ - refName: injectObject.refName, - objName: injectObject.objName, - qualifiers: propertyQualifiers, - proto, - }); + if (this.injectType === InjectType.PROPERTY) { + injectObjectProtos.push({ + refName: injectObject.refName, + objName: injectObject.objName, + qualifiers: propertyQualifiers, + proto, + }); + } else { + injectObjectProtos.push({ + refIndex: (injectObject as InjectConstructor).refIndex, + refName: injectObject.refName, + objName: injectObject.objName, + qualifiers: propertyQualifiers, + proto, + }); + } } const id = IdenticalUtil.createProtoId(this.loadUnit.id, this.name); return new EggPrototypeImpl( @@ -126,6 +139,7 @@ export class EggPrototypeBuilder { this.loadUnit.id, this.qualifiers, this.className, + this.injectType, ); } } diff --git a/core/metadata/src/impl/EggPrototypeImpl.ts b/core/metadata/src/impl/EggPrototypeImpl.ts index 020144f6..47ad91f6 100644 --- a/core/metadata/src/impl/EggPrototypeImpl.ts +++ b/core/metadata/src/impl/EggPrototypeImpl.ts @@ -1,15 +1,16 @@ -import { MetadataUtil } from '@eggjs/core-decorator'; +import { InjectType, MetadataUtil } from '@eggjs/core-decorator'; import type { AccessLevel, - EggPrototype, EggProtoImplClass, + EggPrototype, EggPrototypeName, + Id, + InjectConstructorProto, + InjectObjectProto, MetaDataKey, ObjectInitTypeLike, QualifierInfo, QualifierValue, - Id, - InjectObjectProto, } from '@eggjs/tegg-types'; export class EggPrototypeImpl implements EggPrototype { @@ -21,7 +22,8 @@ export class EggPrototypeImpl implements EggPrototype { readonly name: EggPrototypeName; readonly initType: ObjectInitTypeLike; readonly accessLevel: AccessLevel; - readonly injectObjects: InjectObjectProto[]; + readonly injectObjects: Array; + readonly injectType: InjectType; readonly loadUnitId: Id; readonly className?: string; @@ -32,10 +34,11 @@ export class EggPrototypeImpl implements EggPrototype { filepath: string, initType: ObjectInitTypeLike, accessLevel: AccessLevel, - injectObjectMap: InjectObjectProto[], + injectObjectMap: Array, loadUnitId: Id, qualifiers: QualifierInfo[], className?: string, + injectType?: InjectType, ) { this.id = id; this.clazz = clazz; @@ -47,6 +50,7 @@ export class EggPrototypeImpl implements EggPrototype { this.loadUnitId = loadUnitId; this.qualifiers = qualifiers; this.className = className; + this.injectType = injectType || InjectType.PROPERTY; } verifyQualifiers(qualifiers: QualifierInfo[]): boolean { @@ -67,8 +71,8 @@ export class EggPrototypeImpl implements EggPrototype { return this.qualifiers.find(t => t.attribute === attribute)?.value; } - constructEggObject(): object { - return Reflect.construct(this.clazz, []); + constructEggObject(...args: any): object { + return Reflect.construct(this.clazz, args); } getMetaData(metadataKey: MetaDataKey): T | undefined { diff --git a/core/metadata/test/LoadUnit.test.ts b/core/metadata/test/LoadUnit.test.ts index 008783fd..d1f8ef42 100644 --- a/core/metadata/test/LoadUnit.test.ts +++ b/core/metadata/test/LoadUnit.test.ts @@ -7,6 +7,27 @@ import { FOO_ATTRIBUTE } from './fixtures/modules/multi-instance-module/MultiIns describe('test/LoadUnit/LoadUnit.test.ts', () => { + describe('inject with constructor', () => { + it('should not inherit parent class', async () => { + const extendsConstructorModule = path.join(__dirname, './fixtures/modules/extends-constructor-module'); + const loader = new TestLoader(extendsConstructorModule); + const loadUnit = await LoadUnitFactory.createLoadUnit(extendsConstructorModule, EggLoadUnitType.MODULE, loader); + + const fooConstructor = loadUnit.getEggPrototype('fooConstructor', [{ attribute: InitTypeQualifierAttribute, value: ObjectInitType.CONTEXT }]); + const fooConstructorLogger = loadUnit.getEggPrototype('fooConstructorLogger', [{ attribute: InitTypeQualifierAttribute, value: ObjectInitType.CONTEXT }]); + + assert.strictEqual(fooConstructor.length, 1); + assert.strictEqual(fooConstructor[0].injectObjects!.length, 1); + assert.strictEqual(fooConstructor[0].injectObjects![0].refName, 'bar'); + + assert.strictEqual(fooConstructorLogger.length, 1); + assert.strictEqual(fooConstructorLogger[0].injectObjects!.length, 2); + assert.strictEqual(fooConstructorLogger[0].injectObjects![0].refName, 'bar'); + assert.strictEqual(fooConstructorLogger[0].injectObjects![1].refName, 'logger'); + await LoadUnitFactory.destroyLoadUnit(loadUnit); + }); + + }); describe('ModuleLoadUnit', () => { it('should create success', async () => { const repoModulePath = path.join(__dirname, './fixtures/modules/load-unit'); diff --git a/core/metadata/test/ModuleGraph.test.ts b/core/metadata/test/ModuleGraph.test.ts index 3f4050ba..04158187 100644 --- a/core/metadata/test/ModuleGraph.test.ts +++ b/core/metadata/test/ModuleGraph.test.ts @@ -1,3 +1,4 @@ +import assert from 'node:assert'; import path from 'path'; import { TestLoader } from './fixtures/TestLoader'; import { ModuleGraph } from '../src/impl/ModuleLoadUnit'; @@ -10,4 +11,19 @@ describe('test/ModuleGraph.test.ts', () => { const graph = new ModuleGraph(clazzList, modulePath, 'foo'); graph.sort(); }); + + it('should sort constructor extends class success', () => { + const modulePath = path.join(__dirname, './fixtures/modules/extends-constructor-module'); + const loader = new TestLoader(modulePath); + const clazzList = loader.load(); + const graph = new ModuleGraph(clazzList, modulePath, 'foo'); + graph.sort(); + assert.deepStrictEqual(graph.clazzList.map(t => t.name), [ + 'Logger', + 'Bar', + 'ConstructorBase', + 'FooConstructor', + 'FooConstructorLogger', + ]); + }); }); diff --git a/core/metadata/test/fixtures/modules/app-graph-modules/root/RootConstructor.ts b/core/metadata/test/fixtures/modules/app-graph-modules/root/RootConstructor.ts new file mode 100644 index 00000000..0a278724 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-graph-modules/root/RootConstructor.ts @@ -0,0 +1,8 @@ +import { SingletonProto, Inject } from '@eggjs/core-decorator'; +import { UsedProto } from '../used/Used'; + +@SingletonProto() +export class RootConstructorProto { + constructor(@Inject() readonly usedProto: UsedProto) { + } +} diff --git a/core/metadata/test/fixtures/modules/extends-constructor-module/Base.ts b/core/metadata/test/fixtures/modules/extends-constructor-module/Base.ts new file mode 100644 index 00000000..74ee5a02 --- /dev/null +++ b/core/metadata/test/fixtures/modules/extends-constructor-module/Base.ts @@ -0,0 +1,29 @@ +import { ContextProto, Inject } from '@eggjs/core-decorator'; + +@ContextProto() +export class Logger { +} + +@ContextProto() +export class Bar { +} + +@ContextProto() +export class ConstructorBase { + constructor(@Inject() readonly logger: Logger) { + } +} + +@ContextProto() +export class FooConstructor extends ConstructorBase { + constructor(@Inject() readonly bar: Bar) { + super(console); + } +} + +@ContextProto() +export class FooConstructorLogger extends ConstructorBase { + constructor(@Inject() readonly bar: Bar, @Inject() readonly logger: Logger) { + super(logger); + } +} diff --git a/core/metadata/test/fixtures/modules/extends-constructor-module/package.json b/core/metadata/test/fixtures/modules/extends-constructor-module/package.json new file mode 100644 index 00000000..b4430b03 --- /dev/null +++ b/core/metadata/test/fixtures/modules/extends-constructor-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "multi-module-service", + "eggModule": { + "name": "extendsModule" + } +} diff --git a/core/runtime/src/impl/EggObjectImpl.ts b/core/runtime/src/impl/EggObjectImpl.ts index d706ec48..cc90e43d 100644 --- a/core/runtime/src/impl/EggObjectImpl.ts +++ b/core/runtime/src/impl/EggObjectImpl.ts @@ -1,6 +1,12 @@ import { LoadUnitFactory } from '@eggjs/tegg-metadata'; -import { ObjectInitType, EggObjectStatus } from '@eggjs/tegg-types'; -import type { EggObject, EggObjectName, EggObjectLifecycle, EggObjectLifeCycleContext, EggPrototype } from '@eggjs/tegg-types'; +import type { + EggObject, + EggObjectLifecycle, + EggObjectLifeCycleContext, + EggObjectName, + EggPrototype, +} from '@eggjs/tegg-types'; +import { EggObjectStatus, InjectType, ObjectInitType } from '@eggjs/tegg-types'; import { IdenticalUtil } from '@eggjs/tegg-lifecycle'; import { EggObjectLifecycleUtil } from '../model/EggObject'; import { EggContainerFactory } from '../factory/EggContainerFactory'; @@ -22,7 +28,7 @@ export default class EggObjectImpl implements EggObject { this.id = IdenticalUtil.createObjectId(this.proto.id, ctx?.id); } - async init(ctx: EggObjectLifeCycleContext) { + async initWithInjectProperty(ctx: EggObjectLifeCycleContext) { // 1. create obj // 2. call obj lifecycle preCreate // 3. inject deps @@ -79,6 +85,71 @@ export default class EggObjectImpl implements EggObject { } } + async initWithInjectConstructor(ctx: EggObjectLifeCycleContext) { + // 1. create inject deps + // 2. create obj + // 3. call obj lifecycle preCreate + // 4. call obj lifecycle postCreate + // 5. success create + try { + const constructArgs = await Promise.all(this.proto.injectObjects!.map(async injectObject => { + const proto = injectObject.proto; + const loadUnit = LoadUnitFactory.getLoadUnitById(proto.loadUnitId); + if (!loadUnit) { + throw new Error(`can not find load unit: ${proto.loadUnitId}`); + } + if (this.proto.initType !== ObjectInitType.CONTEXT && injectObject.proto.initType === ObjectInitType.CONTEXT) { + return EggObjectUtil.contextEggObjectProxy(proto, injectObject.objName); + } + const injectObj = await EggContainerFactory.getOrCreateEggObject(proto, injectObject.objName); + return EggObjectUtil.eggObjectProxy(injectObj); + })); + + this._obj = this.proto.constructEggObject(...constructArgs); + const objLifecycleHook = this._obj as EggObjectLifecycle; + + // global hook + await EggObjectLifecycleUtil.objectPreCreate(ctx, this); + // self hook + const postConstructMethod = EggObjectLifecycleUtil.getLifecycleHook('postConstruct', this.proto) ?? 'postConstruct'; + if (objLifecycleHook[postConstructMethod]) { + await objLifecycleHook[postConstructMethod](ctx, this); + } + + const preInjectMethod = EggObjectLifecycleUtil.getLifecycleHook('preInject', this.proto) ?? 'preInject'; + if (objLifecycleHook[preInjectMethod]) { + await objLifecycleHook[preInjectMethod](ctx, this); + } + + // global hook + await EggObjectLifecycleUtil.objectPostCreate(ctx, this); + + // self hook + const postInjectMethod = EggObjectLifecycleUtil.getLifecycleHook('postInject', this.proto) ?? 'postInject'; + if (objLifecycleHook[postInjectMethod]) { + await objLifecycleHook[postInjectMethod](ctx, this); + } + + const initMethod = EggObjectLifecycleUtil.getLifecycleHook('init', this.proto) ?? 'init'; + if (objLifecycleHook[initMethod]) { + await objLifecycleHook[initMethod](ctx, this); + } + + this.status = EggObjectStatus.READY; + } catch (e) { + this.status = EggObjectStatus.ERROR; + throw e; + } + } + + async init(ctx: EggObjectLifeCycleContext) { + if (this.proto.injectType === InjectType.CONSTRUCTOR) { + await this.initWithInjectConstructor(ctx); + } else { + await this.initWithInjectProperty(ctx); + } + } + async destroy(ctx: EggObjectLifeCycleContext) { if (this.status === EggObjectStatus.READY) { this.status = EggObjectStatus.DESTROYING; diff --git a/core/runtime/src/impl/EggObjectUtil.ts b/core/runtime/src/impl/EggObjectUtil.ts index b9261d32..e79f3c12 100644 --- a/core/runtime/src/impl/EggObjectUtil.ts +++ b/core/runtime/src/impl/EggObjectUtil.ts @@ -26,4 +26,146 @@ export class EggObjectUtil { } return proto[PROTO_OBJ_GETTER]; } + + static eggObjectProxy(eggObject: EggObject): PropertyDescriptor { + let _obj: object; + function getObj() { + if (!_obj) { + _obj = eggObject.obj; + } + return _obj; + } + + const proxy = new Proxy({}, { + defineProperty(_target: {}, property: string | symbol, attributes: PropertyDescriptor): boolean { + const obj = getObj(); + Object.defineProperty(obj, property, attributes); + return true; + }, + deleteProperty(_target: {}, p: string | symbol): boolean { + const obj = getObj(); + delete obj[p]; + return true; + }, + get(target: {}, p: string | symbol): any { + // make get be lazy + if (p === 'then') return; + if (Object.prototype[p]) { + return target[p]; + } + const obj = getObj(); + return obj[p]; + }, + getOwnPropertyDescriptor(_target: {}, p: string | symbol): PropertyDescriptor | undefined { + const obj = getObj(); + return Object.getOwnPropertyDescriptor(obj, p); + }, + getPrototypeOf(): object | null { + const obj = getObj(); + return Object.getPrototypeOf(obj); + }, + has(_target: {}, p: string | symbol): boolean { + const obj = getObj(); + return p in obj; + }, + isExtensible(): boolean { + const obj = getObj(); + return Object.isExtensible(obj); + }, + ownKeys(): ArrayLike { + const obj = getObj(); + return Reflect.ownKeys(obj); + }, + preventExtensions(): boolean { + const obj = getObj(); + Object.preventExtensions(obj); + return true; + }, + set(_target: {}, p: string | symbol, newValue: any): boolean { + const obj = getObj(); + obj[p] = newValue; + return true; + }, + setPrototypeOf(_target: {}, v: object | null): boolean { + const obj = getObj(); + Object.setPrototypeOf(obj, v); + return true; + }, + }); + return proxy; + } + + static contextEggObjectProxy(proto: EggPrototype, objName: PropertyKey): PropertyDescriptor { + const PROTO_OBJ_PROXY = Symbol(`EggPrototype#objProxy#${String(objName)}`); + if (!proto[PROTO_OBJ_PROXY]) { + proto[PROTO_OBJ_PROXY] = new Proxy({}, { + defineProperty(_target: {}, property: string | symbol, attributes: PropertyDescriptor): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + Object.defineProperty(obj, property, attributes); + return true; + }, + deleteProperty(_target: {}, p: string | symbol): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + delete obj[p]; + return true; + }, + get(target: {}, p: string | symbol): any { + // make get be lazy + if (p === 'then') return; + if (Object.prototype[p]) { + return target[p]; + } + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + return obj[p]; + }, + getOwnPropertyDescriptor(_target: {}, p: string | symbol): PropertyDescriptor | undefined { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + return Object.getOwnPropertyDescriptor(obj, p); + }, + getPrototypeOf(): object | null { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + return Object.getPrototypeOf(obj); + }, + has(_target: {}, p: string | symbol): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + return p in obj; + }, + isExtensible(): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + return Object.isExtensible(obj); + }, + ownKeys(): ArrayLike { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + return Reflect.ownKeys(obj); + }, + preventExtensions(): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + Object.preventExtensions(obj); + return true; + }, + set(_target: {}, p: string | symbol, newValue: any): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + obj[p] = newValue; + return true; + }, + setPrototypeOf(_target: {}, v: object | null): boolean { + const eggObject = EggContainerFactory.getEggObject(proto, objName); + const obj = eggObject.obj; + Object.setPrototypeOf(obj, v); + return true; + }, + }); + } + return proto[PROTO_OBJ_PROXY]; + } } diff --git a/core/runtime/test/EggObject.test.ts b/core/runtime/test/EggObject.test.ts index 339af024..97407703 100644 --- a/core/runtime/test/EggObject.test.ts +++ b/core/runtime/test/EggObject.test.ts @@ -8,6 +8,7 @@ import { Foo, Bar } from './fixtures/modules/lifecycle-hook/object'; import { Bar as ExtendsBar } from './fixtures/modules/extends-module/Base'; import { ContextHandler } from '../src/model/ContextHandler'; import { SingletonBar } from './fixtures/modules/inject-context-to-singleton/object'; +import { SingletonConstructorBar } from './fixtures/modules/inject-constructor-context-to-singleton/object'; describe('test/EggObject.test.ts', () => { let ctx: EggTestContext; @@ -113,6 +114,25 @@ describe('test/EggObject.test.ts', () => { }); }); + describe('constructor inject context to singleton', () => { + it('should work', async () => { + mm(ContextHandler, 'getContext', () => { + return; + }); + const instance = await TestUtil.createLoadUnitInstance('inject-constructor-context-to-singleton'); + const barProto = EggPrototypeFactory.instance.getPrototype('singletonConstructorBar'); + mm(ContextHandler, 'getContext', () => { + return ctx; + }); + const barObj = await EggContainerFactory.getOrCreateEggObject(barProto, barProto.name); + const bar = barObj.obj as SingletonConstructorBar; + const msg = await bar.hello(); + assert(msg === 'hello from depth2'); + await TestUtil.destroyLoadUnitInstance(instance); + await ctx.destroy({}); + }); + }); + describe('property mock', () => { beforeEach(() => { mm(ContextHandler, 'getContext', () => { diff --git a/core/runtime/test/fixtures/modules/inject-constructor-context-to-singleton/object.ts b/core/runtime/test/fixtures/modules/inject-constructor-context-to-singleton/object.ts new file mode 100644 index 00000000..47a1a810 --- /dev/null +++ b/core/runtime/test/fixtures/modules/inject-constructor-context-to-singleton/object.ts @@ -0,0 +1,60 @@ +import { AccessLevel } from '@eggjs/tegg-types'; +import { ContextProto, Inject, SingletonProto } from '@eggjs/core-decorator'; + +@ContextProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class ContextFooDepth2 { + async hello() { + return 'hello from depth2'; + } +} + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class SingletonConstructorBarDepth3 { + constructor(@Inject() readonly contextFooDepth2: ContextFooDepth2) { + } + + async hello() { + return this.contextFooDepth2.hello(); + } +} + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class SingletonBarConstructorDepth2 { + constructor(@Inject() readonly singletonConstructorBarDepth3: SingletonConstructorBarDepth3) { + } + + async hello() { + return this.singletonConstructorBarDepth3.hello(); + } +} + + +@ContextProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class ContextConstructorFoo { + constructor(@Inject() readonly singletonBarConstructorDepth2: SingletonBarConstructorDepth2) { + } + + async hello() { + return this.singletonBarConstructorDepth2.hello(); + } +} + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class SingletonConstructorBar { + constructor(@Inject() readonly contextConstructorFoo: ContextConstructorFoo) { + } + + async hello() { + return this.contextConstructorFoo.hello(); + } +} diff --git a/core/runtime/test/fixtures/modules/inject-constructor-context-to-singleton/package.json b/core/runtime/test/fixtures/modules/inject-constructor-context-to-singleton/package.json new file mode 100644 index 00000000..b4430b03 --- /dev/null +++ b/core/runtime/test/fixtures/modules/inject-constructor-context-to-singleton/package.json @@ -0,0 +1,6 @@ +{ + "name": "multi-module-service", + "eggModule": { + "name": "extendsModule" + } +} diff --git a/core/types/core-decorator/enum/InjectType.ts b/core/types/core-decorator/enum/InjectType.ts new file mode 100644 index 00000000..fcfb7275 --- /dev/null +++ b/core/types/core-decorator/enum/InjectType.ts @@ -0,0 +1,4 @@ +export enum InjectType { + PROPERTY = 'PROPERTY', + CONSTRUCTOR = 'CONSTRUCTOR', +} diff --git a/core/types/core-decorator/enum/Qualifier.ts b/core/types/core-decorator/enum/Qualifier.ts index 93ae00a4..1732a800 100644 --- a/core/types/core-decorator/enum/Qualifier.ts +++ b/core/types/core-decorator/enum/Qualifier.ts @@ -9,3 +9,5 @@ export const LoadUnitNameQualifierAttribute = Symbol.for('Qualifier.LoadUnitName export const QUALIFIER_META_DATA = Symbol.for('EggPrototype#qualifier'); export const PROPERTY_QUALIFIER_META_DATA = Symbol.for('EggPrototype#propertyQualifier'); + +export const CONSTRUCTOR_QUALIFIER_META_DATA = Symbol.for('EggPrototype#constructorQualifier'); diff --git a/core/types/core-decorator/index.ts b/core/types/core-decorator/index.ts index 9b74827e..61118ac3 100644 --- a/core/types/core-decorator/index.ts +++ b/core/types/core-decorator/index.ts @@ -2,9 +2,11 @@ export * from './enum/AccessLevel'; export * from './enum/EggType'; export * from './enum/ObjectInitType'; export * from './enum/Qualifier'; +export * from './enum/InjectType'; export * from './model/EggPrototypeInfo'; export * from './model/InjectObjectInfo'; +export * from './model/InjectConstructorInfo'; export * from './model/QualifierInfo'; export * from './model/EggMultiInstancePrototypeInfo'; diff --git a/core/types/core-decorator/model/InjectConstructorInfo.ts b/core/types/core-decorator/model/InjectConstructorInfo.ts new file mode 100644 index 00000000..2b58471c --- /dev/null +++ b/core/types/core-decorator/model/InjectConstructorInfo.ts @@ -0,0 +1,16 @@ +import { EggObjectName } from './InjectObjectInfo'; + +export interface InjectConstructorInfo { + /** + * inject args index + */ + refIndex: number; + /** + * inject args name + */ + refName: string; + /** + * obj's name will be injected + */ + objName: EggObjectName; +} diff --git a/core/types/metadata/model/EggPrototype.ts b/core/types/metadata/model/EggPrototype.ts index 5074ed5e..3567325e 100644 --- a/core/types/metadata/model/EggPrototype.ts +++ b/core/types/metadata/model/EggPrototype.ts @@ -2,7 +2,7 @@ import { AccessLevel, EggProtoImplClass, EggPrototypeInfo, - EggPrototypeName, + EggPrototypeName, InjectType, MetaDataKey, ObjectInitTypeLike, QualifierAttribute, @@ -31,6 +31,29 @@ export interface InjectObjectProto { proto: EggPrototype; } +export interface InjectConstructorProto { + /** + * inject args index + */ + refIndex: number; + /** + * property name obj inject to + */ + refName: PropertyKey; + /** + * obj's name will be injected + */ + objName: PropertyKey; + /** + * inject qualifiers + */ + qualifiers: QualifierInfo[]; + /** + * inject prototype + */ + proto: EggPrototype; +} + export interface InjectObject { /** * property name obj inject to @@ -47,6 +70,26 @@ export interface InjectObject { initType?: ObjectInitTypeLike; } +export interface InjectConstructor { + /** + * property name obj inject to + */ + refIndex: number; + /** + * property name obj inject to + */ + refName: PropertyKey; + /** + * obj's name will be injected + */ + objName: PropertyKey; + /** + * obj's initType will be injected + * if null same as current obj + */ + initType?: ObjectInitTypeLike; +} + export type EggPrototypeClass = new (...args: any[]) => EggPrototype; export interface EggPrototypeLifecycleContext extends LifecycleContext { @@ -63,7 +106,8 @@ export interface EggPrototype extends LifecycleObject; + readonly injectType?: InjectType; readonly className?: string; /** @@ -88,7 +132,7 @@ export interface EggPrototype extends LifecycleObject { + let app; + const fixtureDir = path.join(__dirname, 'fixtures/apps/constructor-module-config'); + + after(async () => { + await app.close(); + }); + + afterEach(() => { + mm.restore(); + }); + + before(async () => { + mm(process.env, 'EGG_TYPESCRIPT', true); + mm(process, 'cwd', () => { + return path.join(__dirname, '..'); + }); + app = mm.app({ + baseDir: fixtureDir, + framework: require.resolve('egg'), + }); + await app.ready(); + }); + + it('should work', async () => { + await app.httpRequest() + .get('/config') + .expect(200) + .expect(res => { + assert.deepStrictEqual(res.body, { + foo: 'bar', + bar: 'foo', + }); + }); + }); +}); diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/app.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/app.ts new file mode 100644 index 00000000..1cb530f4 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/app.ts @@ -0,0 +1,15 @@ +import { Application } from 'egg'; + +export default class AppBoot { + app: Application; + + constructor(app: Application) { + this.app = app; + } + + configWillLoad() { + if (this.app.moduleConfigs?.overwrite?.config) { + (this.app.moduleConfigs.overwrite.config as Record).features.dynamic.bar = 'overwrite foo'; + } + } +} diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/app/controller/app.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/controller/app.ts new file mode 100644 index 00000000..df79b358 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/controller/app.ts @@ -0,0 +1,10 @@ +import { Controller } from 'egg'; + +export default class App extends Controller { + async baseDir() { + this.ctx.body = { + foo: this.app.module.constructorSimple.foo.foo, + bar: this.app.module.constructorSimple.foo.bar, + }; + } +} diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/app/extend/application.unittest.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/extend/application.unittest.ts new file mode 100644 index 00000000..985b7b62 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/extend/application.unittest.ts @@ -0,0 +1,11 @@ +import { MockApplication } from 'egg-mock'; + +export default { + mockUser(this: MockApplication) { + this.mockContext({ + user: { + userName: 'mock_user', + }, + }); + }, +}; diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/app/extend/context.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/extend/context.ts new file mode 100644 index 00000000..934ed2a2 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/extend/context.ts @@ -0,0 +1,14 @@ +const COUNTER = Symbol('Context#counter'); + +export default { + get counter() { + if (!this[COUNTER]) { + this[COUNTER] = 0; + } + return this[COUNTER]++; + }, + + get user() { + return {}; + }, +}; diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/app/router.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/router.ts new file mode 100644 index 00000000..fd5b81c1 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/router.ts @@ -0,0 +1,5 @@ +import { Application } from 'egg'; + +module.exports = (app: Application) => { + app.router.get('/config', app.controller.app.baseDir); +}; diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/app/typings/index.d.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/typings/index.d.ts new file mode 100644 index 00000000..6a147811 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/app/typings/index.d.ts @@ -0,0 +1,11 @@ +import 'egg'; +import { Foo } from '../../modules/module-with-config/foo'; +import { Bar } from '../../modules/module-with-overwrite-config/bar'; + +declare module 'egg' { + export interface EggModule { + constructorSimple: { + foo: Foo; + }, + } +} diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/config/config.default.js b/plugin/tegg/test/fixtures/apps/constructor-module-config/config/config.default.js new file mode 100644 index 00000000..6d1b8de5 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/config/config.default.js @@ -0,0 +1,20 @@ +'use strict'; + +const path = require('path'); + +module.exports = function(appInfo) { + const config = { + keys: 'test key', + customLogger: { + xxLogger: { + file: path.join(appInfo.root, 'logs/xx.log'), + }, + }, + security: { + csrf: { + ignoreJSON: false, + } + }, + }; + return config; +}; diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/config/plugin.js b/plugin/tegg/test/fixtures/apps/constructor-module-config/config/plugin.js new file mode 100644 index 00000000..10d5c293 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/config/plugin.js @@ -0,0 +1,13 @@ +'use strict'; + +exports.tracer = { + package: 'egg-tracer', + enable: true, +}; + +exports.teggConfig = { + package: '@eggjs/tegg-config', + enable: true, +}; + +exports.watcher = false; diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/foo.ts b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/foo.ts new file mode 100644 index 00000000..80b8a28a --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/foo.ts @@ -0,0 +1,14 @@ +import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg'; + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class Foo { + readonly foo: string; + readonly bar: string; + + constructor(@Inject() moduleConfig: Record) { + this.foo = moduleConfig.features.dynamic.foo; + this.bar = moduleConfig.features.dynamic.bar; + } +} diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/module.unittest.yml b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/module.unittest.yml new file mode 100644 index 00000000..0bd1bbaf --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/module.unittest.yml @@ -0,0 +1,3 @@ +features: + dynamic: + bar: 'foo' diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/module.yml b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/module.yml new file mode 100644 index 00000000..a0046c04 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/module.yml @@ -0,0 +1,3 @@ +features: + dynamic: + foo: 'bar' diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/package.json b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/package.json new file mode 100644 index 00000000..28825d15 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/modules/module-with-config/package.json @@ -0,0 +1,6 @@ +{ + "name": "constructorSimple", + "eggModule": { + "name": "constructorSimple" + } +} diff --git a/plugin/tegg/test/fixtures/apps/constructor-module-config/package.json b/plugin/tegg/test/fixtures/apps/constructor-module-config/package.json new file mode 100644 index 00000000..978d31f2 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/constructor-module-config/package.json @@ -0,0 +1,3 @@ +{ + "name": "egg-app" +}