Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support optional inject #254

Merged
merged 8 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
gxkl marked this conversation as resolved.
Show resolved Hide resolved
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 {
gxkl marked this conversation as resolved.
Show resolved Hide resolved
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;
gxkl marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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
Loading