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: add default inject init type qualifier #255

Merged
merged 2 commits into from
Oct 30, 2024
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
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;
}
}
}

gxkl marked this conversation as resolved.
Show resolved Hide resolved
return {
objName,
initType,
};
}

gxkl marked this conversation as resolved.
Show resolved Hide resolved
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}`);
}
});
gxkl marked this conversation as resolved.
Show resolved Hide resolved

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,
) {}
gxkl marked this conversation as resolved.
Show resolved Hide resolved
}
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,
}];
gxkl marked this conversation as resolved.
Show resolved Hide resolved

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 ];
gxkl marked this conversation as resolved.
Show resolved Hide resolved
}
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');
});
});
gxkl marked this conversation as resolved.
Show resolved Hide resolved

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
Loading