Skip to content

Commit 538ae80

Browse files
authored
feat: add default inject init type qualifier (#255)
<!-- Thank you for your pull request. Please review below requirements. Bug fixes and new features should include tests and possibly benchmarks. Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md 感谢您贡献代码。请确认下列 checklist 的完成情况。 Bug 修复和新功能必须包含测试,必要时请附上性能测试。 Contributors guide: https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md --> ##### Checklist <!-- Remove items that do not apply. For completed items, change [ ] to [x]. --> - [ ] `npm test` passes - [ ] tests and/or benchmarks are included - [ ] documentation is changed or added - [ ] commit message follows commit guidelines ##### Affected core subsystem(s) <!-- Provide affected core subsystem(s). --> ##### Description of change <!-- Provide a description of the change below this comment. --> <!-- - any feature? - close https://github.com/eggjs/egg/ISSUE_URL --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced dependency injection capabilities with new services and qualifiers. - Introduced multiple new classes and methods to improve service management. - Added configuration files for better plugin management. - **Bug Fixes** - Updated test cases to reflect changes in expected outputs for dependency injection. - **Documentation** - Added metadata in new `package.json` files for modules and services, improving clarity on module structure. - **Tests** - Expanded test coverage for new services and qualifiers to ensure correct functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 260470b commit 538ae80

File tree

18 files changed

+325
-27
lines changed

18 files changed

+325
-27
lines changed

core/core-decorator/src/decorator/Inject.ts

+56-8
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
1-
import { EggProtoImplClass, InjectObjectInfo, InjectConstructorInfo, InjectParams, InjectType } from '@eggjs/tegg-types';
1+
import {
2+
EggProtoImplClass,
3+
InjectObjectInfo,
4+
InjectConstructorInfo,
5+
InjectParams,
6+
InjectType,
7+
InitTypeQualifierAttribute,
8+
} from '@eggjs/tegg-types';
29
import { PrototypeUtil } from '../util/PrototypeUtil';
310
import { ObjectUtils } from '@eggjs/tegg-common-util';
11+
import { QualifierUtil } from '../util/QualifierUtil';
12+
13+
function guessInjectInfo(clazz: EggProtoImplClass, name: PropertyKey, proto: any) {
14+
let objName: PropertyKey | undefined;
15+
let initType: string | undefined;
16+
17+
if (typeof proto === 'function' && proto !== Object) {
18+
// if property type is function and not Object( means maybe proto class ), then try to read EggPrototypeInfo.name as obj name
19+
const info = PrototypeUtil.getProperty(proto as EggProtoImplClass);
20+
objName = info?.name;
21+
// try to read EggPrototypeInfo.initType as qualifier
22+
if (info?.initType) {
23+
const customInitType = QualifierUtil.getProperQualifier(clazz, name, InitTypeQualifierAttribute);
24+
if (!customInitType) {
25+
initType = info.initType;
26+
}
27+
}
28+
}
29+
30+
return {
31+
objName,
32+
initType,
33+
};
34+
}
435

536
export function Inject(param?: InjectParams | string) {
637
const injectParam = typeof param === 'string' ? { name: param } : param;
738

839
function propertyInject(target: any, propertyKey: PropertyKey) {
940
let objName: PropertyKey | undefined;
41+
let initType: string | undefined;
1042
if (!injectParam) {
1143
// try to read design:type from proto
1244
const proto = PrototypeUtil.getDesignType(target, propertyKey);
13-
if (typeof proto === 'function' && proto !== Object) {
14-
// if property type is function and not Object( means maybe proto class ), then try to read EggPrototypeInfo.name as obj name
15-
const info = PrototypeUtil.getProperty(proto as EggProtoImplClass);
16-
objName = info?.name;
17-
}
45+
({ objName, initType } = guessInjectInfo(target.constructor, propertyKey, proto));
1846
} else {
1947
// params allow string or object
2048
objName = injectParam?.name;
@@ -31,16 +59,32 @@ export function Inject(param?: InjectParams | string) {
3159

3260
PrototypeUtil.setInjectType(target.constructor, InjectType.PROPERTY);
3361
PrototypeUtil.addInjectObject(target.constructor as EggProtoImplClass, injectObject);
62+
63+
if (initType) {
64+
QualifierUtil.addProperQualifier(target.constructor, propertyKey, InitTypeQualifierAttribute, initType);
65+
}
3466
}
3567

3668
function constructorInject(target: any, parameterIndex: number) {
3769
const argNames = ObjectUtils.getConstructorArgNameList(target);
3870
const argName = argNames[parameterIndex];
71+
72+
let objName: PropertyKey | undefined;
73+
let initType: string | undefined;
74+
75+
if (!injectParam) {
76+
// try to read proto from design:paramtypes
77+
const protos = PrototypeUtil.getDesignParamtypes(target);
78+
({ objName, initType } = guessInjectInfo(target, argName, protos?.[parameterIndex]));
79+
} else {
80+
// params allow string or object
81+
objName = injectParam?.name;
82+
}
83+
3984
const injectObject: InjectConstructorInfo = {
4085
refIndex: parameterIndex,
4186
refName: argName,
42-
// TODO get objName from design:type
43-
objName: injectParam?.name || argName,
87+
objName: objName || argName,
4488
};
4589

4690
if (injectParam?.optional) {
@@ -49,6 +93,10 @@ export function Inject(param?: InjectParams | string) {
4993

5094
PrototypeUtil.setInjectType(target, InjectType.CONSTRUCTOR);
5195
PrototypeUtil.addInjectConstructor(target as EggProtoImplClass, injectObject);
96+
97+
if (initType) {
98+
QualifierUtil.addProperQualifier(target, argName, InitTypeQualifierAttribute, initType);
99+
}
52100
}
53101

54102
return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) {

core/core-decorator/src/util/PrototypeUtil.ts

+4
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,8 @@ export class PrototypeUtil {
284284
static getDesignType(clazz: EggProtoImplClass, propKey?: PropertyKey) {
285285
return MetadataUtil.getMetaData('design:type', clazz, propKey);
286286
}
287+
288+
static getDesignParamtypes(clazz: EggProtoImplClass, propKey?: PropertyKey) {
289+
return MetadataUtil.getMetaData<unknown[]>('design:paramtypes', clazz, propKey);
290+
}
287291
}

core/core-decorator/test/decorators.test.ts

+38-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SingletonCache from './fixtures/decators/SingletonCache';
1616
import { PrototypeUtil, QualifierUtil } from '..';
1717
import QualifierCacheService from './fixtures/decators/QualifierCacheService';
1818
import { FOO_ATTRIBUTE, FooLogger } from './fixtures/decators/FooLogger';
19-
import { ConstructorObject } from './fixtures/decators/ConstructorObject';
19+
import { ConstructorObject, ConstructorQualifierObject } from './fixtures/decators/ConstructorObject';
2020
import {
2121
ChildDynamicMultiInstanceProto,
2222
ChildSingletonProto,
@@ -90,8 +90,9 @@ describe('test/decorator.test.ts', () => {
9090
assert.deepStrictEqual(injectConstructors, [
9191
{ refIndex: 0, refName: 'xCache', objName: 'fooCache' },
9292
{ refIndex: 1, refName: 'cache', objName: 'cache' },
93-
{ refIndex: 2, refName: 'optional1', objName: 'optional1', optional: true },
94-
{ refIndex: 3, refName: 'optional2', objName: 'optional2', optional: true },
93+
{ refIndex: 2, refName: 'otherCache', objName: 'cacheService' },
94+
{ refIndex: 3, refName: 'optional1', objName: 'optional1', optional: true },
95+
{ refIndex: 4, refName: 'optional2', objName: 'optional2', optional: true },
9596
]);
9697
});
9798
});
@@ -107,6 +108,23 @@ describe('test/decorator.test.ts', () => {
107108
QualifierUtil.getProperQualifier(QualifierCacheService, property, InitTypeQualifierAttribute) === ObjectInitType.SINGLETON,
108109
);
109110
});
111+
112+
it('should set default initType in inject', () => {
113+
const properties = [
114+
{ property: 'interfaceService', expected: undefined },
115+
{ property: 'testContextService', expected: ObjectInitType.CONTEXT },
116+
{ property: 'testSingletonService', expected: ObjectInitType.SINGLETON },
117+
{ property: 'customNameService', expected: undefined },
118+
{ property: 'customQualifierService1', expected: ObjectInitType.CONTEXT },
119+
{ property: 'customQualifierService2', expected: ObjectInitType.CONTEXT },
120+
];
121+
122+
for (const { property, expected } of properties) {
123+
const qualifier = QualifierUtil.getProperQualifier(QualifierCacheService, property, InitTypeQualifierAttribute);
124+
assert.strictEqual(qualifier, expected, `expect initType for ${property} to be ${expected}`);
125+
}
126+
});
127+
110128
it('should work use Symbol.for', () => {
111129
assert(PrototypeUtil.isEggPrototype(QualifierCacheService));
112130
const property = 'cache';
@@ -117,6 +135,7 @@ describe('test/decorator.test.ts', () => {
117135
QualifierUtil.getProperQualifier(QualifierCacheService, property, Symbol.for('Qualifier.InitType')) === ObjectInitType.SINGLETON,
118136
);
119137
});
138+
120139
it('constructor should work', () => {
121140
const constructorQualifiers = QualifierUtil.getProperQualifiers(ConstructorObject, 'xCache');
122141
const constructorQualifiers2 = QualifierUtil.getProperQualifiers(ConstructorObject, 'cache');
@@ -126,6 +145,22 @@ describe('test/decorator.test.ts', () => {
126145
]);
127146
assert.deepStrictEqual(constructorQualifiers2, []);
128147
});
148+
149+
it('should set default initType in constructor inject', () => {
150+
const properties = [
151+
{ property: 'xCache', expected: undefined },
152+
{ property: 'cache', expected: ObjectInitType.SINGLETON },
153+
{ property: 'ContextCache', expected: ObjectInitType.CONTEXT },
154+
{ property: 'customNameCache', expected: undefined },
155+
{ property: 'customQualifierCache1', expected: ObjectInitType.CONTEXT },
156+
{ property: 'customQualifierCache2', expected: ObjectInitType.CONTEXT },
157+
];
158+
159+
for (const { property, expected } of properties) {
160+
const qualifier = QualifierUtil.getProperQualifier(ConstructorQualifierObject, property, InitTypeQualifierAttribute);
161+
assert.strictEqual(qualifier, expected, `expect initType for ${property} to be ${expected}`);
162+
}
163+
});
129164
});
130165

131166
describe('MultiInstanceProto', () => {
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import { ObjectInitType } from '@eggjs/tegg-types';
12
import { SingletonProto } from '../../../src/decorator/SingletonProto';
2-
import { ICache } from './ICache';
33
import { Inject, InjectOptional } from '../../../src/decorator/Inject';
44
import { InitTypeQualifier } from '../../../src/decorator/InitTypeQualifier';
5-
import { ObjectInitType } from '@eggjs/tegg-types';
65
import { ModuleQualifier } from '../../../src/decorator/ModuleQualifier';
6+
import { ContextProto } from '../../../src/decorator/ContextProto';
7+
import { ICache } from './ICache';
8+
9+
@SingletonProto()
10+
export class CacheService {}
11+
12+
@ContextProto()
13+
export class CacheContextService {}
714

815
@SingletonProto()
916
export class ConstructorObject {
@@ -12,8 +19,20 @@ export class ConstructorObject {
1219
@ModuleQualifier('foo')
1320
@Inject({ name: 'fooCache'}) readonly xCache: ICache,
1421
@Inject() readonly cache: ICache,
22+
@Inject() readonly otherCache: CacheService,
1523
@Inject({ optional: true }) readonly optional1?: ICache,
1624
@InjectOptional() readonly optional2?: ICache,
17-
) {
18-
}
25+
) {}
26+
}
27+
28+
@SingletonProto()
29+
export class ConstructorQualifierObject {
30+
constructor(
31+
@Inject() readonly xCache: ICache,
32+
@Inject() readonly cache: CacheService,
33+
@Inject() readonly ContextCache: CacheContextService,
34+
@Inject('cacheService') readonly customNameCache: CacheService,
35+
@InitTypeQualifier(ObjectInitType.CONTEXT) @Inject() readonly customQualifierCache1: CacheService,
36+
@Inject() @InitTypeQualifier(ObjectInitType.CONTEXT) readonly customQualifierCache2: CacheService,
37+
) {}
1938
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { ObjectInitType } from '@eggjs/tegg-types';
2-
import { ContextProto, InitTypeQualifier, Inject, ModuleQualifier } from '../../..';
2+
import { ContextProto, InitTypeQualifier, Inject, ModuleQualifier, SingletonProto } from '../../..';
33
import { ICache } from './ICache';
44

5+
@ContextProto()
6+
export class TestContextService {}
7+
8+
@SingletonProto()
9+
export class TestSingletonService {}
10+
511
@ContextProto()
612
export default class CacheService {
713
@Inject({
@@ -10,4 +16,24 @@ export default class CacheService {
1016
@InitTypeQualifier(ObjectInitType.SINGLETON)
1117
@ModuleQualifier('foo')
1218
cache: ICache;
19+
20+
@Inject()
21+
interfaceService: ICache;
22+
23+
@Inject()
24+
testContextService: TestContextService;
25+
26+
@Inject()
27+
testSingletonService: TestSingletonService;
28+
29+
@Inject('testSingletonService')
30+
customNameService: TestSingletonService;
31+
32+
@InitTypeQualifier(ObjectInitType.CONTEXT)
33+
@Inject()
34+
customQualifierService1: TestSingletonService;
35+
36+
@Inject()
37+
@InitTypeQualifier(ObjectInitType.CONTEXT)
38+
customQualifierService2: TestSingletonService;
1339
}

core/metadata/src/factory/EggPrototypeCreatorFactory.ts

+21-11
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,23 @@ export class EggPrototypeCreatorFactory {
2323

2424
static async createProto(clazz: EggProtoImplClass, loadUnit: LoadUnit): Promise<EggPrototype[]> {
2525
let properties: EggPrototypeInfo[] = [];
26+
const defaultQualifier = [{
27+
attribute: InitTypeQualifierAttribute,
28+
value: PrototypeUtil.getInitType(clazz, {
29+
unitPath: loadUnit.unitPath,
30+
moduleName: loadUnit.name,
31+
})!,
32+
}, {
33+
attribute: LoadUnitNameQualifierAttribute,
34+
value: loadUnit.name,
35+
}];
36+
2637
if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
2738
const multiInstanceProtoInfo = PrototypeUtil.getMultiInstanceProperty(clazz, {
2839
unitPath: loadUnit.unitPath,
2940
moduleName: loadUnit.name,
3041
})!;
3142
for (const obj of multiInstanceProtoInfo.objects) {
32-
const defaultQualifier = [{
33-
attribute: InitTypeQualifierAttribute,
34-
value: PrototypeUtil.getInitType(clazz, {
35-
unitPath: loadUnit.unitPath,
36-
moduleName: loadUnit.name,
37-
})!,
38-
}, {
39-
attribute: LoadUnitNameQualifierAttribute,
40-
value: loadUnit.name,
41-
}];
4243
defaultQualifier.forEach(qualifier => {
4344
if (!obj.qualifiers.find(t => t.attribute === qualifier.attribute)) {
4445
obj.qualifiers.push(qualifier);
@@ -56,7 +57,16 @@ export class EggPrototypeCreatorFactory {
5657
});
5758
}
5859
} else {
59-
properties = [ PrototypeUtil.getProperty(clazz)! ];
60+
const property = PrototypeUtil.getProperty(clazz)!;
61+
if (!property.qualifiers) {
62+
property.qualifiers = [];
63+
}
64+
defaultQualifier.forEach(qualifier => {
65+
if (!property.qualifiers!.find(t => t.attribute === qualifier.attribute)) {
66+
property.qualifiers!.push(qualifier);
67+
}
68+
});
69+
properties = [ property ];
6070
}
6171
const protos: EggPrototype[] = [];
6272
for (const property of properties) {

plugin/tegg/test/Inject.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import path from 'path';
33
import assert from 'assert';
44
import { BarService } from './fixtures/apps/optional-inject/app/modules/module-a/BarService';
55
import { FooService } from './fixtures/apps/optional-inject/app/modules/module-a/FooService';
6+
import { BarService1 } from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarService1';
7+
import { BarService2 } from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarService2';
8+
import {
9+
BarConstructorService1,
10+
} from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarConstructorService1';
11+
import {
12+
BarConstructorService2,
13+
} from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarConstructorService2';
614

715
describe('plugin/tegg/test/Inject.test.ts', () => {
816
let app;
@@ -47,6 +55,44 @@ describe('plugin/tegg/test/Inject.test.ts', () => {
4755
});
4856
});
4957

58+
describe('default initType qualifier', async () => {
59+
beforeEach(async () => {
60+
app = mm.app({
61+
baseDir: path.join(__dirname, 'fixtures/apps/same-name-singleton-and-context-proto'),
62+
framework: require.resolve('egg'),
63+
});
64+
await app.ready();
65+
});
66+
67+
it('should work with singletonProto', async () => {
68+
await app.mockModuleContextScope(async () => {
69+
const barService1: BarService1 = await app.getEggObject(BarService1);
70+
assert.strictEqual(barService1.type(), 'singleton');
71+
});
72+
});
73+
74+
it('should work with contextProto', async () => {
75+
await app.mockModuleContextScope(async () => {
76+
const barService2: BarService2 = await app.getEggObject(BarService2);
77+
assert.strictEqual(barService2.type(), 'context');
78+
});
79+
});
80+
81+
it('should work with singletonProto', async () => {
82+
await app.mockModuleContextScope(async () => {
83+
const barService1: BarConstructorService1 = await app.getEggObject(BarConstructorService1);
84+
assert.strictEqual(barService1.type(), 'singleton');
85+
});
86+
});
87+
88+
it('should work with contextProto', async () => {
89+
await app.mockModuleContextScope(async () => {
90+
const barService2: BarConstructorService2 = await app.getEggObject(BarConstructorService2);
91+
assert.strictEqual(barService2.type(), 'context');
92+
});
93+
});
94+
});
95+
5096
it('should throw error if no proto found', async () => {
5197
app = mm.app({
5298
baseDir: path.join(__dirname, 'fixtures/apps/invalid-inject'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Inject, SingletonProto } from '@eggjs/tegg';
2+
import { FooService } from '../module-foo/FooService';
3+
4+
@SingletonProto()
5+
export class BarConstructorService1 {
6+
constructor(
7+
@Inject() readonly fooService: FooService,
8+
) {}
9+
10+
type() {
11+
return this.fooService.type;
12+
}
13+
}

0 commit comments

Comments
 (0)