Skip to content

Commit

Permalink
feat: support optional inject (#254)
Browse files Browse the repository at this point in the history
<!--
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 -->
  • Loading branch information
gxkl authored Oct 28, 2024
1 parent 3604a40 commit 260470b
Show file tree
Hide file tree
Showing 31 changed files with 429 additions and 29 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,25 @@ Proto 中可以依赖其他的 Proto,或者 egg 中的对象。
// 在某些情况不希望注入的原型和属性使用一个名称
// 默认为属性名称
proto?: string;
// 注入对象是否为可选,默认为 false
// 若为 false,当不存在该对象时,启动阶段将会抛出异常
// 若为 true,且未找到对象时,该属性值为 undefined
optional?: boolean;
})
```

对于 optional 为 true 的情况,也提供了 InjectOptional 的 alias 装饰器
```typescript
// 等价于 @Inject({ ...params, optional: true })
@InjectOptional(params: {
// 注入对象的名称,在某些情况下一个原型可能有多个实例
// 比如说 egg 的 logger
// 默认为属性名称
name?: string;
// 注入原型的名称
// 在某些情况不希望注入的原型和属性使用一个名称
// 默认为属性名称
proto?: string;
})
```

Expand All @@ -489,9 +508,17 @@ import { Inject } from '@eggjs/tegg';
export class HelloService {
@Inject()
logger: EggLogger;

// 等价于 @Inject({ optional: true })
@InjectOptional()
maybeUndefinedLogger?: EggLogger;

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

@ContextProto()
export class HelloService {
constructor(@Inject() readonly logger: EggLogger) {
}
constructor(
@Inject() readonly logger: EggLogger,
@InjectOptional() readonly maybeUndefinedLogger?: EggLogger,
) {}

async hello(user: User): Promise<string> {
this.logger.info(`[HelloService] hello ${user.name}`);
// optional inject 使用时,需要判断是否有值
if (this.maybeUndefinedLogger) {
this.maybeUndefinedLogger.info(`[HelloService] hello ${user.name}`);
}
const echoResponse = await this.echoAdapter.echo({ name: user.name });
return `hello, ${echoResponse.name}`;
}
Expand Down
28 changes: 23 additions & 5 deletions core/core-decorator/src/decorator/Inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { PrototypeUtil } from '../util/PrototypeUtil';
import { ObjectUtils } from '@eggjs/tegg-common-util';

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

function propertyInject(target: any, propertyKey: PropertyKey) {
let objName: PropertyKey | undefined;
if (!param) {
if (!injectParam) {
// try to read design:type from proto
const proto = PrototypeUtil.getDesignType(target, propertyKey);
if (typeof proto === 'function' && proto !== Object) {
Expand All @@ -15,29 +17,36 @@ export function Inject(param?: InjectParams | string) {
}
} else {
// params allow string or object
objName = typeof param === 'string' ? param : param?.name;
objName = injectParam?.name;
}

const injectObject: InjectObjectInfo = {
refName: propertyKey,
objName: objName || propertyKey,
};

if (injectParam?.optional) {
injectObject.optional = true;
}

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,
// TODO get objName from design:type
objName: injectParam?.name || argName,
};

if (injectParam?.optional) {
injectObject.optional = true;
}

PrototypeUtil.setInjectType(target, InjectType.CONSTRUCTOR);
PrototypeUtil.addInjectConstructor(target as EggProtoImplClass, injectObject);
}
Expand All @@ -50,3 +59,12 @@ export function Inject(param?: InjectParams | string) {
}
};
}

export function InjectOptional(param?: Omit<InjectParams, 'optional'> | string) {
const injectParam = typeof param === 'string' ? { name: param } : param;

return Inject({
...injectParam,
optional: true,
});
}
10 changes: 10 additions & 0 deletions core/core-decorator/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ describe('test/decorator.test.ts', () => {
}, {
objName: 'testService4',
refName: 'testService4',
}, {
objName: 'optionalService1',
refName: 'optionalService1',
optional: true,
}, {
objName: 'optionalService2',
refName: 'optionalService2',
optional: true,
}];
assert.deepStrictEqual(PrototypeUtil.getInjectObjects(CacheService), expectInjectInfo);
});
Expand All @@ -82,6 +90,8 @@ describe('test/decorator.test.ts', () => {
assert.deepStrictEqual(injectConstructors, [
{ refIndex: 0, refName: 'xCache', objName: 'fooCache' },
{ refIndex: 1, refName: 'cache', objName: 'cache' },
{ refIndex: 2, refName: 'optional1', objName: 'optional1', optional: true },
{ refIndex: 3, refName: 'optional2', objName: 'optional2', optional: true },
]);
});
});
Expand Down
9 changes: 8 additions & 1 deletion core/core-decorator/test/fixtures/decators/CacheService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ContextProto, Inject } from '../../..';
import { ContextProto } from '../../../src/decorator/ContextProto';
import { Inject, InjectOptional } from '../../../src/decorator/Inject';
import { ICache } from './ICache';
import { TestService, TestService2 } from './OtherService';

Expand Down Expand Up @@ -37,4 +38,10 @@ export default class CacheService {

@Inject()
testService4: any;

@Inject({ optional: true })
optionalService1?: any;

@InjectOptional()
optionalService2?: any;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SingletonProto } from '../../../src/decorator/SingletonProto';
import { ICache } from './ICache';
import { Inject } from '../../../src/decorator/Inject';
import { Inject, InjectOptional } from '../../../src/decorator/Inject';
import { InitTypeQualifier } from '../../../src/decorator/InitTypeQualifier';
import { ObjectInitType } from '@eggjs/tegg-types';
import { ModuleQualifier } from '../../../src/decorator/ModuleQualifier';
Expand All @@ -12,6 +12,8 @@ export class ConstructorObject {
@ModuleQualifier('foo')
@Inject({ name: 'fooCache'}) readonly xCache: ICache,
@Inject() readonly cache: ICache,
@Inject({ optional: true }) readonly optional1?: ICache,
@InjectOptional() readonly optional2?: ICache,
) {
}
}
52 changes: 32 additions & 20 deletions core/metadata/src/impl/EggPrototypeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class EggPrototypeBuilder {
return builder.build();
}

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

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

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

private findInjectObjectPrototype(injectObject: InjectObject): EggPrototype {
private findInjectObjectPrototype(injectObject: InjectObject | InjectConstructor): EggPrototype {
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
try {
return this.tryFindDefaultPrototype(injectObject);
Expand All @@ -121,22 +121,34 @@ export class EggPrototypeBuilder {
const injectObjectProtos: Array<InjectObjectProto | InjectConstructorProto> = [];
for (const injectObject of this.injectObjects) {
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
const proto = this.findInjectObjectPrototype(injectObject);
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,
});
try {
const proto = this.findInjectObjectPrototype(injectObject);
let injectObjectProto: InjectObjectProto | InjectConstructorProto;
if (this.injectType === InjectType.PROPERTY) {
injectObjectProto = {
refName: injectObject.refName,
objName: injectObject.objName,
qualifiers: propertyQualifiers,
proto,
};
} else {
injectObjectProto = {
refIndex: (injectObject as InjectConstructor).refIndex,
refName: injectObject.refName,
objName: injectObject.objName,
qualifiers: propertyQualifiers,
proto,
};
}
if (injectObject.optional) {
injectObject.optional = true;
}
injectObjectProtos.push(injectObjectProto);
} catch (e) {
if (e instanceof EggPrototypeNotFound && injectObject.optional) {
continue;
}
throw e;
}
}
const id = IdenticalUtil.createProtoId(this.loadUnit.id, this.name);
Expand Down
12 changes: 12 additions & 0 deletions core/metadata/test/LoadUnit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ describe('test/LoadUnit/LoadUnit.test.ts', () => {
});
});

describe('optional inject', () => {
it('should success', async () => {
const optionalInjectModulePath = path.join(__dirname, './fixtures/modules/optional-inject-module');
const loader = new TestLoader(optionalInjectModulePath);
buildGlobalGraph([ optionalInjectModulePath ], [ loader ]);

const loadUnit = await LoadUnitFactory.createLoadUnit(optionalInjectModulePath, EggLoadUnitType.MODULE, loader);
const optionalInjectServiceProto = loadUnit.getEggPrototype('optionalInjectService', [{ attribute: InitTypeQualifierAttribute, value: ObjectInitType.SINGLETON }]);
assert.deepStrictEqual(optionalInjectServiceProto[0].injectObjects, []);
});
});

describe('invalidate load unit', () => {
it('should init failed', async () => {
const invalidateModulePath = path.join(__dirname, './fixtures/modules/invalidate-module');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Inject, InjectOptional, SingletonProto } from '@eggjs/core-decorator';

interface PersistenceService {
}

@SingletonProto()
export default class OptionalInjectService {
@Inject({ optional: true })
persistenceService?: PersistenceService;

@InjectOptional()
persistenceService2?: PersistenceService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "optional-inject-service",
"eggModule": {
"name": "optionalInjectService"
}
}
2 changes: 2 additions & 0 deletions core/types/core-decorator/Inject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface InjectParams {
// obj instance name, default is property name
name?: string;
// optional inject, default is false which means it will throw error when there is no relative object
optional?: boolean;
}
4 changes: 4 additions & 0 deletions core/types/core-decorator/model/InjectConstructorInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ export interface InjectConstructorInfo {
* obj's name will be injected
*/
objName: EggObjectName;
/**
* optional inject
*/
optional?: boolean;
}
4 changes: 4 additions & 0 deletions core/types/core-decorator/model/InjectObjectInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export interface InjectObjectInfo {
* obj's name will be injected
*/
objName: EggObjectName;
/**
* optional inject
*/
optional?: boolean;
}
16 changes: 16 additions & 0 deletions core/types/metadata/model/EggPrototype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export interface InjectObjectProto {
* inject qualifiers
*/
qualifiers: QualifierInfo[];
/**
* optional inject
*/
optional?: boolean;
/**
* inject prototype
*/
Expand All @@ -48,6 +52,10 @@ export interface InjectConstructorProto {
* inject qualifiers
*/
qualifiers: QualifierInfo[];
/**
* optional inject
*/
optional?: boolean;
/**
* inject prototype
*/
Expand All @@ -68,6 +76,10 @@ export interface InjectObject {
* if null same as current obj
*/
initType?: ObjectInitTypeLike;
/**
* optional inject
*/
optional?: boolean;
}

export interface InjectConstructor {
Expand All @@ -88,6 +100,10 @@ export interface InjectConstructor {
* if null same as current obj
*/
initType?: ObjectInitTypeLike;
/**
* optional inject
*/
optional?: boolean;
}

export type EggPrototypeClass = new (...args: any[]) => EggPrototype;
Expand Down
Loading

0 comments on commit 260470b

Please sign in to comment.