Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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) {
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
Loading