Skip to content

Commit 260470b

Browse files
authored
feat: support optional inject (#254)
<!-- 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]. --> - [x] `npm test` passes - [x] tests and/or benchmarks are included - [x] documentation is changed or added - [x] 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 ## Release Notes - **New Features** - Introduced optional dependency injection capabilities, allowing services to be injected as optional. - Added a new `InjectOptional` decorator for marking parameters as optional. - **Improvements** - Enhanced error handling in the `EggPrototypeBuilder` for optional inject objects. - Streamlined logic for parameter handling in the `Inject` decorator. - Improved flexibility in service management by allowing optional dependencies in various services. - **Tests** - Expanded test coverage for optional injections in various scenarios. - Added new test cases to validate the functionality of optional dependencies. - **Documentation** - Updated documentation to include detailed sections on optional dependency injection and lifecycle hooks, enhancing clarity and usability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 3604a40 commit 260470b

31 files changed

Lines changed: 429 additions & 29 deletions

File tree

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,25 @@ Proto 中可以依赖其他的 Proto,或者 egg 中的对象。
474474
// 在某些情况不希望注入的原型和属性使用一个名称
475475
// 默认为属性名称
476476
proto?: string;
477+
// 注入对象是否为可选,默认为 false
478+
// 若为 false,当不存在该对象时,启动阶段将会抛出异常
479+
// 若为 true,且未找到对象时,该属性值为 undefined
480+
optional?: boolean;
481+
})
482+
```
483+
484+
对于 optional 为 true 的情况,也提供了 InjectOptional 的 alias 装饰器
485+
```typescript
486+
// 等价于 @Inject({ ...params, optional: true })
487+
@InjectOptional(params: {
488+
// 注入对象的名称,在某些情况下一个原型可能有多个实例
489+
// 比如说 egg 的 logger
490+
// 默认为属性名称
491+
name?: string;
492+
// 注入原型的名称
493+
// 在某些情况不希望注入的原型和属性使用一个名称
494+
// 默认为属性名称
495+
proto?: string;
477496
})
478497
```
479498

@@ -489,9 +508,17 @@ import { Inject } from '@eggjs/tegg';
489508
export class HelloService {
490509
@Inject()
491510
logger: EggLogger;
511+
512+
// 等价于 @Inject({ optional: true })
513+
@InjectOptional()
514+
maybeUndefinedLogger?: EggLogger;
492515

493516
async hello(user: User): Promise<string> {
494517
this.logger.info(`[HelloService] hello ${user.name}`);
518+
// optional inject 使用时,需要判断是否有值
519+
if (this.maybeUndefinedLogger) {
520+
this.maybeUndefinedLogger.info(`[HelloService] hello ${user.name}`);
521+
}
495522
const echoResponse = await this.echoAdapter.echo({ name: user.name });
496523
return `hello, ${echoResponse.name}`;
497524
}
@@ -506,11 +533,17 @@ import { Inject } from '@eggjs/tegg';
506533

507534
@ContextProto()
508535
export class HelloService {
509-
constructor(@Inject() readonly logger: EggLogger) {
510-
}
536+
constructor(
537+
@Inject() readonly logger: EggLogger,
538+
@InjectOptional() readonly maybeUndefinedLogger?: EggLogger,
539+
) {}
511540

512541
async hello(user: User): Promise<string> {
513542
this.logger.info(`[HelloService] hello ${user.name}`);
543+
// optional inject 使用时,需要判断是否有值
544+
if (this.maybeUndefinedLogger) {
545+
this.maybeUndefinedLogger.info(`[HelloService] hello ${user.name}`);
546+
}
514547
const echoResponse = await this.echoAdapter.echo({ name: user.name });
515548
return `hello, ${echoResponse.name}`;
516549
}

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { PrototypeUtil } from '../util/PrototypeUtil';
33
import { ObjectUtils } from '@eggjs/tegg-common-util';
44

55
export function Inject(param?: InjectParams | string) {
6+
const injectParam = typeof param === 'string' ? { name: param } : param;
7+
68
function propertyInject(target: any, propertyKey: PropertyKey) {
79
let objName: PropertyKey | undefined;
8-
if (!param) {
10+
if (!injectParam) {
911
// try to read design:type from proto
1012
const proto = PrototypeUtil.getDesignType(target, propertyKey);
1113
if (typeof proto === 'function' && proto !== Object) {
@@ -15,29 +17,36 @@ export function Inject(param?: InjectParams | string) {
1517
}
1618
} else {
1719
// params allow string or object
18-
objName = typeof param === 'string' ? param : param?.name;
20+
objName = injectParam?.name;
1921
}
2022

2123
const injectObject: InjectObjectInfo = {
2224
refName: propertyKey,
2325
objName: objName || propertyKey,
2426
};
2527

28+
if (injectParam?.optional) {
29+
injectObject.optional = true;
30+
}
31+
2632
PrototypeUtil.setInjectType(target.constructor, InjectType.PROPERTY);
2733
PrototypeUtil.addInjectObject(target.constructor as EggProtoImplClass, injectObject);
2834
}
2935

3036
function constructorInject(target: any, parameterIndex: number) {
3137
const argNames = ObjectUtils.getConstructorArgNameList(target);
3238
const argName = argNames[parameterIndex];
33-
// TODO get objName from design:type
34-
const objName = typeof param === 'string' ? param : param?.name;
3539
const injectObject: InjectConstructorInfo = {
3640
refIndex: parameterIndex,
3741
refName: argName,
38-
objName: objName || argName,
42+
// TODO get objName from design:type
43+
objName: injectParam?.name || argName,
3944
};
4045

46+
if (injectParam?.optional) {
47+
injectObject.optional = true;
48+
}
49+
4150
PrototypeUtil.setInjectType(target, InjectType.CONSTRUCTOR);
4251
PrototypeUtil.addInjectConstructor(target as EggProtoImplClass, injectObject);
4352
}
@@ -50,3 +59,12 @@ export function Inject(param?: InjectParams | string) {
5059
}
5160
};
5261
}
62+
63+
export function InjectOptional(param?: Omit<InjectParams, 'optional'> | string) {
64+
const injectParam = typeof param === 'string' ? { name: param } : param;
65+
66+
return Inject({
67+
...injectParam,
68+
optional: true,
69+
});
70+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ describe('test/decorator.test.ts', () => {
7373
}, {
7474
objName: 'testService4',
7575
refName: 'testService4',
76+
}, {
77+
objName: 'optionalService1',
78+
refName: 'optionalService1',
79+
optional: true,
80+
}, {
81+
objName: 'optionalService2',
82+
refName: 'optionalService2',
83+
optional: true,
7684
}];
7785
assert.deepStrictEqual(PrototypeUtil.getInjectObjects(CacheService), expectInjectInfo);
7886
});
@@ -82,6 +90,8 @@ describe('test/decorator.test.ts', () => {
8290
assert.deepStrictEqual(injectConstructors, [
8391
{ refIndex: 0, refName: 'xCache', objName: 'fooCache' },
8492
{ refIndex: 1, refName: 'cache', objName: 'cache' },
93+
{ refIndex: 2, refName: 'optional1', objName: 'optional1', optional: true },
94+
{ refIndex: 3, refName: 'optional2', objName: 'optional2', optional: true },
8595
]);
8696
});
8797
});

core/core-decorator/test/fixtures/decators/CacheService.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ContextProto, Inject } from '../../..';
1+
import { ContextProto } from '../../../src/decorator/ContextProto';
2+
import { Inject, InjectOptional } from '../../../src/decorator/Inject';
23
import { ICache } from './ICache';
34
import { TestService, TestService2 } from './OtherService';
45

@@ -37,4 +38,10 @@ export default class CacheService {
3738

3839
@Inject()
3940
testService4: any;
41+
42+
@Inject({ optional: true })
43+
optionalService1?: any;
44+
45+
@InjectOptional()
46+
optionalService2?: any;
4047
}

core/core-decorator/test/fixtures/decators/ConstructorObject.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SingletonProto } from '../../../src/decorator/SingletonProto';
22
import { ICache } from './ICache';
3-
import { Inject } from '../../../src/decorator/Inject';
3+
import { Inject, InjectOptional } from '../../../src/decorator/Inject';
44
import { InitTypeQualifier } from '../../../src/decorator/InitTypeQualifier';
55
import { ObjectInitType } from '@eggjs/tegg-types';
66
import { ModuleQualifier } from '../../../src/decorator/ModuleQualifier';
@@ -12,6 +12,8 @@ export class ConstructorObject {
1212
@ModuleQualifier('foo')
1313
@Inject({ name: 'fooCache'}) readonly xCache: ICache,
1414
@Inject() readonly cache: ICache,
15+
@Inject({ optional: true }) readonly optional1?: ICache,
16+
@InjectOptional() readonly optional2?: ICache,
1517
) {
1618
}
1719
}

core/metadata/src/impl/EggPrototypeBuilder.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class EggPrototypeBuilder {
6363
return builder.build();
6464
}
6565

66-
private tryFindDefaultPrototype(injectObject: InjectObject): EggPrototype {
66+
private tryFindDefaultPrototype(injectObject: InjectObject | InjectConstructor): EggPrototype {
6767
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
6868
const multiInstancePropertyQualifiers = this.properQualifiers[injectObject.refName as string] ?? [];
6969
return EggPrototypeFactory.instance.getPrototype(injectObject.objName, this.loadUnit, QualifierUtil.mergeQualifiers(
@@ -72,7 +72,7 @@ export class EggPrototypeBuilder {
7272
));
7373
}
7474

75-
private tryFindContextPrototype(injectObject: InjectObject): EggPrototype {
75+
private tryFindContextPrototype(injectObject: InjectObject | InjectConstructor): EggPrototype {
7676
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
7777
const multiInstancePropertyQualifiers = this.properQualifiers[injectObject.refName as string] ?? [];
7878
return EggPrototypeFactory.instance.getPrototype(injectObject.objName, this.loadUnit, QualifierUtil.mergeQualifiers(
@@ -85,7 +85,7 @@ export class EggPrototypeBuilder {
8585
));
8686
}
8787

88-
private tryFindSelfInitTypePrototype(injectObject: InjectObject): EggPrototype {
88+
private tryFindSelfInitTypePrototype(injectObject: InjectObject | InjectConstructor): EggPrototype {
8989
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
9090
const multiInstancePropertyQualifiers = this.properQualifiers[injectObject.refName as string] ?? [];
9191
return EggPrototypeFactory.instance.getPrototype(injectObject.objName, this.loadUnit, QualifierUtil.mergeQualifiers(
@@ -98,7 +98,7 @@ export class EggPrototypeBuilder {
9898
));
9999
}
100100

101-
private findInjectObjectPrototype(injectObject: InjectObject): EggPrototype {
101+
private findInjectObjectPrototype(injectObject: InjectObject | InjectConstructor): EggPrototype {
102102
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
103103
try {
104104
return this.tryFindDefaultPrototype(injectObject);
@@ -121,22 +121,34 @@ export class EggPrototypeBuilder {
121121
const injectObjectProtos: Array<InjectObjectProto | InjectConstructorProto> = [];
122122
for (const injectObject of this.injectObjects) {
123123
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
124-
const proto = this.findInjectObjectPrototype(injectObject);
125-
if (this.injectType === InjectType.PROPERTY) {
126-
injectObjectProtos.push({
127-
refName: injectObject.refName,
128-
objName: injectObject.objName,
129-
qualifiers: propertyQualifiers,
130-
proto,
131-
});
132-
} else {
133-
injectObjectProtos.push({
134-
refIndex: (injectObject as InjectConstructor).refIndex,
135-
refName: injectObject.refName,
136-
objName: injectObject.objName,
137-
qualifiers: propertyQualifiers,
138-
proto,
139-
});
124+
try {
125+
const proto = this.findInjectObjectPrototype(injectObject);
126+
let injectObjectProto: InjectObjectProto | InjectConstructorProto;
127+
if (this.injectType === InjectType.PROPERTY) {
128+
injectObjectProto = {
129+
refName: injectObject.refName,
130+
objName: injectObject.objName,
131+
qualifiers: propertyQualifiers,
132+
proto,
133+
};
134+
} else {
135+
injectObjectProto = {
136+
refIndex: (injectObject as InjectConstructor).refIndex,
137+
refName: injectObject.refName,
138+
objName: injectObject.objName,
139+
qualifiers: propertyQualifiers,
140+
proto,
141+
};
142+
}
143+
if (injectObject.optional) {
144+
injectObject.optional = true;
145+
}
146+
injectObjectProtos.push(injectObjectProto);
147+
} catch (e) {
148+
if (e instanceof EggPrototypeNotFound && injectObject.optional) {
149+
continue;
150+
}
151+
throw e;
140152
}
141153
}
142154
const id = IdenticalUtil.createProtoId(this.loadUnit.id, this.name);

core/metadata/test/LoadUnit.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ describe('test/LoadUnit/LoadUnit.test.ts', () => {
7575
});
7676
});
7777

78+
describe('optional inject', () => {
79+
it('should success', async () => {
80+
const optionalInjectModulePath = path.join(__dirname, './fixtures/modules/optional-inject-module');
81+
const loader = new TestLoader(optionalInjectModulePath);
82+
buildGlobalGraph([ optionalInjectModulePath ], [ loader ]);
83+
84+
const loadUnit = await LoadUnitFactory.createLoadUnit(optionalInjectModulePath, EggLoadUnitType.MODULE, loader);
85+
const optionalInjectServiceProto = loadUnit.getEggPrototype('optionalInjectService', [{ attribute: InitTypeQualifierAttribute, value: ObjectInitType.SINGLETON }]);
86+
assert.deepStrictEqual(optionalInjectServiceProto[0].injectObjects, []);
87+
});
88+
});
89+
7890
describe('invalidate load unit', () => {
7991
it('should init failed', async () => {
8092
const invalidateModulePath = path.join(__dirname, './fixtures/modules/invalidate-module');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Inject, InjectOptional, SingletonProto } from '@eggjs/core-decorator';
2+
3+
interface PersistenceService {
4+
}
5+
6+
@SingletonProto()
7+
export default class OptionalInjectService {
8+
@Inject({ optional: true })
9+
persistenceService?: PersistenceService;
10+
11+
@InjectOptional()
12+
persistenceService2?: PersistenceService;
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "optional-inject-service",
3+
"eggModule": {
4+
"name": "optionalInjectService"
5+
}
6+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export interface InjectParams {
22
// obj instance name, default is property name
33
name?: string;
4+
// optional inject, default is false which means it will throw error when there is no relative object
5+
optional?: boolean;
46
}

0 commit comments

Comments
 (0)