Skip to content

Commit

Permalink
feat: add default inject init type qualifier (#255)
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]. -->

- [ ] `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 -->
  • Loading branch information
gxkl authored Oct 30, 2024
1 parent 260470b commit 538ae80
Show file tree
Hide file tree
Showing 18 changed files with 325 additions and 27 deletions.
64 changes: 56 additions & 8 deletions core/core-decorator/src/decorator/Inject.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
import { EggProtoImplClass, InjectObjectInfo, InjectConstructorInfo, InjectParams, InjectType } from '@eggjs/tegg-types';
import {
EggProtoImplClass,
InjectObjectInfo,
InjectConstructorInfo,
InjectParams,
InjectType,
InitTypeQualifierAttribute,
} from '@eggjs/tegg-types';
import { PrototypeUtil } from '../util/PrototypeUtil';
import { ObjectUtils } from '@eggjs/tegg-common-util';
import { QualifierUtil } from '../util/QualifierUtil';

function guessInjectInfo(clazz: EggProtoImplClass, name: PropertyKey, proto: any) {
let objName: PropertyKey | undefined;
let initType: string | undefined;

if (typeof proto === 'function' && proto !== Object) {
// if property type is function and not Object( means maybe proto class ), then try to read EggPrototypeInfo.name as obj name
const info = PrototypeUtil.getProperty(proto as EggProtoImplClass);
objName = info?.name;
// try to read EggPrototypeInfo.initType as qualifier
if (info?.initType) {
const customInitType = QualifierUtil.getProperQualifier(clazz, name, InitTypeQualifierAttribute);
if (!customInitType) {
initType = info.initType;
}
}
}

return {
objName,
initType,
};
}

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

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

PrototypeUtil.setInjectType(target.constructor, InjectType.PROPERTY);
PrototypeUtil.addInjectObject(target.constructor as EggProtoImplClass, injectObject);

if (initType) {
QualifierUtil.addProperQualifier(target.constructor, propertyKey, InitTypeQualifierAttribute, initType);
}
}

function constructorInject(target: any, parameterIndex: number) {
const argNames = ObjectUtils.getConstructorArgNameList(target);
const argName = argNames[parameterIndex];

let objName: PropertyKey | undefined;
let initType: string | undefined;

if (!injectParam) {
// try to read proto from design:paramtypes
const protos = PrototypeUtil.getDesignParamtypes(target);
({ objName, initType } = guessInjectInfo(target, argName, protos?.[parameterIndex]));
} else {
// params allow string or object
objName = injectParam?.name;
}

const injectObject: InjectConstructorInfo = {
refIndex: parameterIndex,
refName: argName,
// TODO get objName from design:type
objName: injectParam?.name || argName,
objName: objName || argName,
};

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

PrototypeUtil.setInjectType(target, InjectType.CONSTRUCTOR);
PrototypeUtil.addInjectConstructor(target as EggProtoImplClass, injectObject);

if (initType) {
QualifierUtil.addProperQualifier(target, argName, InitTypeQualifierAttribute, initType);
}
}

return function(target: any, propertyKey?: PropertyKey, parameterIndex?: number) {
Expand Down
4 changes: 4 additions & 0 deletions core/core-decorator/src/util/PrototypeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,8 @@ export class PrototypeUtil {
static getDesignType(clazz: EggProtoImplClass, propKey?: PropertyKey) {
return MetadataUtil.getMetaData('design:type', clazz, propKey);
}

static getDesignParamtypes(clazz: EggProtoImplClass, propKey?: PropertyKey) {
return MetadataUtil.getMetaData<unknown[]>('design:paramtypes', clazz, propKey);
}
}
41 changes: 38 additions & 3 deletions core/core-decorator/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SingletonCache from './fixtures/decators/SingletonCache';
import { PrototypeUtil, QualifierUtil } from '..';
import QualifierCacheService from './fixtures/decators/QualifierCacheService';
import { FOO_ATTRIBUTE, FooLogger } from './fixtures/decators/FooLogger';
import { ConstructorObject } from './fixtures/decators/ConstructorObject';
import { ConstructorObject, ConstructorQualifierObject } from './fixtures/decators/ConstructorObject';
import {
ChildDynamicMultiInstanceProto,
ChildSingletonProto,
Expand Down Expand Up @@ -90,8 +90,9 @@ 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 },
{ refIndex: 2, refName: 'otherCache', objName: 'cacheService' },
{ refIndex: 3, refName: 'optional1', objName: 'optional1', optional: true },
{ refIndex: 4, refName: 'optional2', objName: 'optional2', optional: true },
]);
});
});
Expand All @@ -107,6 +108,23 @@ describe('test/decorator.test.ts', () => {
QualifierUtil.getProperQualifier(QualifierCacheService, property, InitTypeQualifierAttribute) === ObjectInitType.SINGLETON,
);
});

it('should set default initType in inject', () => {
const properties = [
{ property: 'interfaceService', expected: undefined },
{ property: 'testContextService', expected: ObjectInitType.CONTEXT },
{ property: 'testSingletonService', expected: ObjectInitType.SINGLETON },
{ property: 'customNameService', expected: undefined },
{ property: 'customQualifierService1', expected: ObjectInitType.CONTEXT },
{ property: 'customQualifierService2', expected: ObjectInitType.CONTEXT },
];

for (const { property, expected } of properties) {
const qualifier = QualifierUtil.getProperQualifier(QualifierCacheService, property, InitTypeQualifierAttribute);
assert.strictEqual(qualifier, expected, `expect initType for ${property} to be ${expected}`);
}
});

it('should work use Symbol.for', () => {
assert(PrototypeUtil.isEggPrototype(QualifierCacheService));
const property = 'cache';
Expand All @@ -117,6 +135,7 @@ describe('test/decorator.test.ts', () => {
QualifierUtil.getProperQualifier(QualifierCacheService, property, Symbol.for('Qualifier.InitType')) === ObjectInitType.SINGLETON,
);
});

it('constructor should work', () => {
const constructorQualifiers = QualifierUtil.getProperQualifiers(ConstructorObject, 'xCache');
const constructorQualifiers2 = QualifierUtil.getProperQualifiers(ConstructorObject, 'cache');
Expand All @@ -126,6 +145,22 @@ describe('test/decorator.test.ts', () => {
]);
assert.deepStrictEqual(constructorQualifiers2, []);
});

it('should set default initType in constructor inject', () => {
const properties = [
{ property: 'xCache', expected: undefined },
{ property: 'cache', expected: ObjectInitType.SINGLETON },
{ property: 'ContextCache', expected: ObjectInitType.CONTEXT },
{ property: 'customNameCache', expected: undefined },
{ property: 'customQualifierCache1', expected: ObjectInitType.CONTEXT },
{ property: 'customQualifierCache2', expected: ObjectInitType.CONTEXT },
];

for (const { property, expected } of properties) {
const qualifier = QualifierUtil.getProperQualifier(ConstructorQualifierObject, property, InitTypeQualifierAttribute);
assert.strictEqual(qualifier, expected, `expect initType for ${property} to be ${expected}`);
}
});
});

describe('MultiInstanceProto', () => {
Expand Down
27 changes: 23 additions & 4 deletions core/core-decorator/test/fixtures/decators/ConstructorObject.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { ObjectInitType } from '@eggjs/tegg-types';
import { SingletonProto } from '../../../src/decorator/SingletonProto';
import { ICache } from './ICache';
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';
import { ContextProto } from '../../../src/decorator/ContextProto';
import { ICache } from './ICache';

@SingletonProto()
export class CacheService {}

@ContextProto()
export class CacheContextService {}

@SingletonProto()
export class ConstructorObject {
Expand All @@ -12,8 +19,20 @@ export class ConstructorObject {
@ModuleQualifier('foo')
@Inject({ name: 'fooCache'}) readonly xCache: ICache,
@Inject() readonly cache: ICache,
@Inject() readonly otherCache: CacheService,
@Inject({ optional: true }) readonly optional1?: ICache,
@InjectOptional() readonly optional2?: ICache,
) {
}
) {}
}

@SingletonProto()
export class ConstructorQualifierObject {
constructor(
@Inject() readonly xCache: ICache,
@Inject() readonly cache: CacheService,
@Inject() readonly ContextCache: CacheContextService,
@Inject('cacheService') readonly customNameCache: CacheService,
@InitTypeQualifier(ObjectInitType.CONTEXT) @Inject() readonly customQualifierCache1: CacheService,
@Inject() @InitTypeQualifier(ObjectInitType.CONTEXT) readonly customQualifierCache2: CacheService,
) {}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { ObjectInitType } from '@eggjs/tegg-types';
import { ContextProto, InitTypeQualifier, Inject, ModuleQualifier } from '../../..';
import { ContextProto, InitTypeQualifier, Inject, ModuleQualifier, SingletonProto } from '../../..';
import { ICache } from './ICache';

@ContextProto()
export class TestContextService {}

@SingletonProto()
export class TestSingletonService {}

@ContextProto()
export default class CacheService {
@Inject({
Expand All @@ -10,4 +16,24 @@ export default class CacheService {
@InitTypeQualifier(ObjectInitType.SINGLETON)
@ModuleQualifier('foo')
cache: ICache;

@Inject()
interfaceService: ICache;

@Inject()
testContextService: TestContextService;

@Inject()
testSingletonService: TestSingletonService;

@Inject('testSingletonService')
customNameService: TestSingletonService;

@InitTypeQualifier(ObjectInitType.CONTEXT)
@Inject()
customQualifierService1: TestSingletonService;

@Inject()
@InitTypeQualifier(ObjectInitType.CONTEXT)
customQualifierService2: TestSingletonService;
}
32 changes: 21 additions & 11 deletions core/metadata/src/factory/EggPrototypeCreatorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,23 @@ export class EggPrototypeCreatorFactory {

static async createProto(clazz: EggProtoImplClass, loadUnit: LoadUnit): Promise<EggPrototype[]> {
let properties: EggPrototypeInfo[] = [];
const defaultQualifier = [{
attribute: InitTypeQualifierAttribute,
value: PrototypeUtil.getInitType(clazz, {
unitPath: loadUnit.unitPath,
moduleName: loadUnit.name,
})!,
}, {
attribute: LoadUnitNameQualifierAttribute,
value: loadUnit.name,
}];

if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
const multiInstanceProtoInfo = PrototypeUtil.getMultiInstanceProperty(clazz, {
unitPath: loadUnit.unitPath,
moduleName: loadUnit.name,
})!;
for (const obj of multiInstanceProtoInfo.objects) {
const defaultQualifier = [{
attribute: InitTypeQualifierAttribute,
value: PrototypeUtil.getInitType(clazz, {
unitPath: loadUnit.unitPath,
moduleName: loadUnit.name,
})!,
}, {
attribute: LoadUnitNameQualifierAttribute,
value: loadUnit.name,
}];
defaultQualifier.forEach(qualifier => {
if (!obj.qualifiers.find(t => t.attribute === qualifier.attribute)) {
obj.qualifiers.push(qualifier);
Expand All @@ -56,7 +57,16 @@ export class EggPrototypeCreatorFactory {
});
}
} else {
properties = [ PrototypeUtil.getProperty(clazz)! ];
const property = PrototypeUtil.getProperty(clazz)!;
if (!property.qualifiers) {
property.qualifiers = [];
}
defaultQualifier.forEach(qualifier => {
if (!property.qualifiers!.find(t => t.attribute === qualifier.attribute)) {
property.qualifiers!.push(qualifier);
}
});
properties = [ property ];
}
const protos: EggPrototype[] = [];
for (const property of properties) {
Expand Down
46 changes: 46 additions & 0 deletions plugin/tegg/test/Inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import path from 'path';
import assert from 'assert';
import { BarService } from './fixtures/apps/optional-inject/app/modules/module-a/BarService';
import { FooService } from './fixtures/apps/optional-inject/app/modules/module-a/FooService';
import { BarService1 } from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarService1';
import { BarService2 } from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarService2';
import {
BarConstructorService1,
} from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarConstructorService1';
import {
BarConstructorService2,
} from './fixtures/apps/same-name-singleton-and-context-proto/app/modules/module-bar/BarConstructorService2';

describe('plugin/tegg/test/Inject.test.ts', () => {
let app;
Expand Down Expand Up @@ -47,6 +55,44 @@ describe('plugin/tegg/test/Inject.test.ts', () => {
});
});

describe('default initType qualifier', async () => {
beforeEach(async () => {
app = mm.app({
baseDir: path.join(__dirname, 'fixtures/apps/same-name-singleton-and-context-proto'),
framework: require.resolve('egg'),
});
await app.ready();
});

it('should work with singletonProto', async () => {
await app.mockModuleContextScope(async () => {
const barService1: BarService1 = await app.getEggObject(BarService1);
assert.strictEqual(barService1.type(), 'singleton');
});
});

it('should work with contextProto', async () => {
await app.mockModuleContextScope(async () => {
const barService2: BarService2 = await app.getEggObject(BarService2);
assert.strictEqual(barService2.type(), 'context');
});
});

it('should work with singletonProto', async () => {
await app.mockModuleContextScope(async () => {
const barService1: BarConstructorService1 = await app.getEggObject(BarConstructorService1);
assert.strictEqual(barService1.type(), 'singleton');
});
});

it('should work with contextProto', async () => {
await app.mockModuleContextScope(async () => {
const barService2: BarConstructorService2 = await app.getEggObject(BarConstructorService2);
assert.strictEqual(barService2.type(), 'context');
});
});
});

it('should throw error if no proto found', async () => {
app = mm.app({
baseDir: path.join(__dirname, 'fixtures/apps/invalid-inject'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Inject, SingletonProto } from '@eggjs/tegg';
import { FooService } from '../module-foo/FooService';

@SingletonProto()
export class BarConstructorService1 {
constructor(
@Inject() readonly fooService: FooService,
) {}

type() {
return this.fooService.type;
}
}
Loading

0 comments on commit 538ae80

Please sign in to comment.