diff --git a/core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts b/core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts index da1783c1..927bcdca 100644 --- a/core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts +++ b/core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts @@ -1,25 +1,25 @@ import { PrototypeUtil } from '@eggjs/core-decorator'; import type { EggProtoImplClass, LifecycleHook, LoadUnit, LoadUnitLifecycleContext } from '@eggjs/tegg-types'; -import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory'; -import { EggPrototypeFactory } from '../factory/EggPrototypeFactory'; +// import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory'; +// import { EggPrototypeFactory } from '../factory/EggPrototypeFactory'; export class LoadUnitMultiInstanceProtoHook implements LifecycleHook { - multiInstanceClazzSet: Set = new Set(); + static multiInstanceClazzSet: Set = new Set(); - async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise { - const clazzList = ctx.loader.load(); - const multiInstanceClazzList = Array.from(this.multiInstanceClazzSet); + static setAllClassList(clazzList: readonly EggProtoImplClass[]) { for (const clazz of clazzList) { if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) { this.multiInstanceClazzSet.add(clazz); } } - for (const clazz of multiInstanceClazzList) { - const protos = await EggPrototypeCreatorFactory.createProto(clazz, loadUnit); - for (const proto of protos) { - EggPrototypeFactory.instance.registerPrototype(proto, loadUnit); - } - } + } + + static clear() { + this.multiInstanceClazzSet.clear(); + } + + async preCreate(): Promise { + // ... } } diff --git a/core/metadata/src/impl/ModuleLoadUnit.ts b/core/metadata/src/impl/ModuleLoadUnit.ts index 4b724ea6..e3d33439 100644 --- a/core/metadata/src/impl/ModuleLoadUnit.ts +++ b/core/metadata/src/impl/ModuleLoadUnit.ts @@ -24,6 +24,7 @@ import { EggPrototypeFactory } from '../factory/EggPrototypeFactory'; import { LoadUnitFactory } from '../factory/LoadUnitFactory'; import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory'; import { MultiPrototypeFound } from '../errors'; +import { LoadUnitMultiInstanceProtoHook } from './LoadUnitMultiInstanceProtoHook'; let id = 0; @@ -64,21 +65,77 @@ class ProtoNode implements GraphNodeObj { } } +class MultiInstanceProtoNode implements GraphNodeObj { + readonly clazz: EggProtoImplClass; + readonly name: EggPrototypeName; + readonly id: string; + readonly qualifiers: QualifierInfo[]; + readonly initType: ObjectInitTypeLike; + readonly unitPath: string; + readonly moduleName: string; + + constructor(clazz: EggProtoImplClass, objName: EggPrototypeName, unitPath: string, moduleName: string) { + this.name = objName; + this.id = '' + (id++); + this.clazz = clazz; + this.qualifiers = QualifierUtil.getProtoQualifiers(clazz); + this.initType = PrototypeUtil.getInitType(clazz, { + unitPath, + moduleName, + })!; + this.unitPath = unitPath; + this.moduleName = moduleName; + } + + verifyQualifiers(qualifiers: QualifierInfo[]): boolean { + const property = PrototypeUtil.getMultiInstanceProperty(this.clazz, { + unitPath: this.unitPath, + moduleName: this.moduleName, + }); + if (!property) { + return false; + } + for (const obj of property.objects) { + const selfQualifiers = [ + ...this.qualifiers, + ...obj.qualifiers, + ]; + if (this.verifyInstanceQualifiers(selfQualifiers, qualifiers)) { + return true; + } + } + return false; + } + + verifyInstanceQualifiers(selfQualifiers: QualifierInfo[], qualifiers: QualifierInfo[]): boolean { + for (const qualifier of qualifiers) { + if (!selfQualifiers.find(t => t.attribute === qualifier.attribute && t.value === qualifier.value)) { + return false; + } + } + return true; + } + + toString(): string { + return `${this.clazz.name}@${PrototypeUtil.getFilePath(this.clazz)}`; + } +} + export class ModuleGraph { - private graph: Graph; + private graph: Graph; clazzList: EggProtoImplClass[]; readonly unitPath: string; readonly name: string; constructor(clazzList: EggProtoImplClass[], unitPath: string, name: string) { this.clazzList = clazzList; - this.graph = new Graph(); + this.graph = new Graph(); this.unitPath = unitPath; this.name = name; this.build(); } - private findInjectNode(objName: EggPrototypeName, qualifiers: QualifierInfo[], parentInitTye: ObjectInitTypeLike): GraphNode | undefined { + private findInjectNode(objName: EggPrototypeName, qualifiers: QualifierInfo[], parentInitTye: ObjectInitTypeLike): GraphNode | undefined { let nodes = Array.from(this.graph.nodes.values()) .filter(t => t.val.name === objName) .filter(t => t.val.verifyQualifiers(qualifiers)); @@ -94,7 +151,16 @@ export class ModuleGraph { value: parentInitTye, }; - nodes = nodes.filter(t => t.val.verifyQualifier(initTypeQualifier)); + nodes = nodes.filter(t => t.val.verifyQualifiers([ initTypeQualifier ])); + if (nodes.length === 1) { + return nodes[0]; + } + + const temp: Map> = new Map(); + for (const node of nodes) { + temp.set(node.val.clazz, node); + } + nodes = Array.from(temp.values()); if (nodes.length === 1) { return nodes[0]; } @@ -104,14 +170,18 @@ export class ModuleGraph { } private build() { - const protoGraphNodes: GraphNode[] = []; + const protoGraphNodes: GraphNode[] = []; for (const clazz of this.clazzList) { const objNames = PrototypeUtil.getObjNames(clazz, { unitPath: this.unitPath, moduleName: this.name, }); for (const objName of objNames) { - protoGraphNodes.push(new GraphNode(new ProtoNode(clazz, objName, this.unitPath, this.name))); + if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) { + protoGraphNodes.push(new GraphNode(new MultiInstanceProtoNode(clazz, objName, this.unitPath, this.name))); + } else { + protoGraphNodes.push(new GraphNode(new ProtoNode(clazz, objName, this.unitPath, this.name))); + } } } for (const node of protoGraphNodes) { @@ -120,13 +190,34 @@ export class ModuleGraph { } } for (const node of protoGraphNodes) { - const injectObjects = PrototypeUtil.getInjectObjects(node.val.clazz); - for (const injectObject of injectObjects) { - const qualifiers = QualifierUtil.getProperQualifiers(node.val.clazz, injectObject.refName); - const injectNode = this.findInjectNode(injectObject.objName, qualifiers, node.val.initType); - // If not found maybe in other module - if (injectNode) { - this.graph.addEdge(node, injectNode); + if (PrototypeUtil.isEggMultiInstancePrototype(node.val.clazz)) { + const property = PrototypeUtil.getMultiInstanceProperty(node.val.clazz, { + moduleName: this.name, + unitPath: this.unitPath, + }); + for (const objectInfo of property?.objects || []) { + const injectObjects = PrototypeUtil.getInjectObjects(node.val.clazz); + for (const injectObject of injectObjects) { + const qualifiers = [ + ...QualifierUtil.getProperQualifiers(node.val.clazz, injectObject.refName), + ...objectInfo.properQualifiers?.[injectObject.refName] ?? [], + ]; + const injectNode = this.findInjectNode(injectObject.objName, qualifiers, node.val.initType); + // If not found maybe in other module + if (injectNode) { + this.graph.addEdge(node, injectNode); + } + } + } + } else { + const injectObjects = PrototypeUtil.getInjectObjects(node.val.clazz); + for (const injectObject of injectObjects) { + const qualifiers = QualifierUtil.getProperQualifiers(node.val.clazz, injectObject.refName); + const injectNode = this.findInjectNode(injectObject.objName, qualifiers, node.val.initType); + // If not found maybe in other module + if (injectNode) { + this.graph.addEdge(node, injectNode); + } } } } @@ -162,8 +253,9 @@ export class ModuleLoadUnit implements LoadUnit { this.loader = loader; } - private loadClazz(): EggProtoImplClass[] { + private doLoadClazz(): EggProtoImplClass[] { const clazzList = this.loader.load(); + const result = clazzList.slice(); for (const clazz of clazzList) { const defaultQualifier = [{ attribute: InitTypeQualifierAttribute, @@ -179,12 +271,30 @@ export class ModuleLoadUnit implements LoadUnit { QualifierUtil.addProtoQualifier(clazz, qualifier.attribute, qualifier.value); }); } - return clazzList; + for (const clazz of LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet) { + const instanceProperty = PrototypeUtil.getMultiInstanceProperty(clazz, { + moduleName: this.name, + unitPath: this.unitPath, + }); + if (instanceProperty) { + result.push(clazz); + } + } + return result; + } + + private loadClazz() { + if (!this.clazzList) { + const clazzList = this.doLoadClazz(); + const protoGraph = new ModuleGraph(clazzList, this.unitPath, this.name); + protoGraph.sort(); + this.clazzList = protoGraph.clazzList; + } } async preLoad() { - const clazzList = this.loader.load(); - for (const protoClass of clazzList) { + this.loadClazz(); + for (const protoClass of this.clazzList) { const fnName = LifecycleUtil.getStaticLifecycleHook('preLoad', protoClass); if (fnName) { await protoClass[fnName]?.(); @@ -193,10 +303,7 @@ export class ModuleLoadUnit implements LoadUnit { } async init() { - const clazzList = this.loadClazz(); - const protoGraph = new ModuleGraph(clazzList, this.unitPath, this.name); - protoGraph.sort(); - this.clazzList = protoGraph.clazzList; + this.loadClazz(); for (const clazz of this.clazzList) { const protos = await EggPrototypeCreatorFactory.createProto(clazz, this); for (const proto of protos) { diff --git a/core/metadata/src/model/AppGraph.ts b/core/metadata/src/model/AppGraph.ts index 720bde01..89cfade9 100644 --- a/core/metadata/src/model/AppGraph.ts +++ b/core/metadata/src/model/AppGraph.ts @@ -8,6 +8,7 @@ import type { EggProtoImplClass, EggPrototypeName, GraphNodeObj, ModuleReference export interface InstanceClazzMeta { name: PropertyKey; qualifiers: QualifierInfo[]; + properQualifiers: Record; accessLevel: AccessLevel, instanceModule: GraphNode; ownerModule: GraphNode; @@ -65,6 +66,7 @@ export class ClazzMap { ...qualifiers, ...info.qualifiers, ], + properQualifiers: info.properQualifiers || {}, instanceModule: instanceNode, ownerModule: ownerNode, }); @@ -81,6 +83,7 @@ export class ClazzMap { moduleName: ownerNode.val.moduleConfig.name, }) as AccessLevel, qualifiers, + properQualifiers: {}, ownerModule: ownerNode, instanceModule: ownerNode, }); @@ -153,7 +156,7 @@ export class ClazzMap { for (const obj of mayObjs) { result.add(obj.instanceModule); - result.add(obj.ownerModule); + // result.add(obj.ownerModule); } return Array.from(result); } @@ -215,6 +218,16 @@ export class AppGraph { } } + getClazzList(): readonly EggProtoImplClass[] { + const clazzSet = new Set(); + for (const node of this.graph.nodes.values()) { + for (const clazz of node.val.getClazzList()) { + clazzSet.add(clazz); + } + } + return Array.from(clazzSet); + } + build() { this.clazzMap = new ClazzMap(this.graph); @@ -225,13 +238,38 @@ export class AppGraph { const injectObjects = PrototypeUtil.getInjectObjects(clazz); // 3. iterate all inject objects for (const injectObject of injectObjects) { - const properQualifiers = QualifierUtil.getProperQualifiers(clazz, injectObject.refName); - // 4. find dependency module - const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node); - for (const moduleNode of dependencyModules) { - // 5. add edge - if (node !== moduleNode) { - this.graph.addEdge(node, moduleNode); + if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) { + for (const instanceNode of this.graph.nodes.values()) { + const property = PrototypeUtil.getMultiInstanceProperty(clazz, { + unitPath: instanceNode.val.moduleConfig.path, + moduleName: instanceNode.val.moduleConfig.name, + }); + for (const info of property?.objects || []) { + const properQualifiers = [ + ...QualifierUtil.getProperQualifiers(clazz, injectObject.refName), + ...info.properQualifiers?.[injectObject.refName] ?? [], + ]; + // 4. find dependency module + const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node); + for (const moduleNode of dependencyModules) { + // 5. add edge + if (instanceNode !== moduleNode) { + this.graph.addEdge(instanceNode, moduleNode); + } + } + } + } + } else { + const properQualifiers = [ + ...QualifierUtil.getProperQualifiers(clazz, injectObject.refName), + ]; + // 4. find dependency module + const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node); + for (const moduleNode of dependencyModules) { + // 5. add edge + if (node !== moduleNode) { + this.graph.addEdge(node, moduleNode); + } } } } diff --git a/core/metadata/test/AppGraph.test.ts b/core/metadata/test/AppGraph.test.ts index a8afe406..ca785c4c 100644 --- a/core/metadata/test/AppGraph.test.ts +++ b/core/metadata/test/AppGraph.test.ts @@ -4,6 +4,10 @@ import { AppGraph, ModuleNode } from '../src/model/AppGraph'; import { RootProto } from './fixtures/modules/app-graph-modules/root/Root'; import { UsedProto } from './fixtures/modules/app-graph-modules/used/Used'; import { UnusedProto } from './fixtures/modules/app-graph-modules/unused/Unused'; +import { App } from './fixtures/modules/app-multi-inject-multi/app/modules/app/App'; +import { BizManager } from './fixtures/modules/app-multi-inject-multi/app/modules/bar/BizManager'; +import { Secret } from './fixtures/modules/app-multi-inject-multi/app/modules/foo/Secret'; +import { App2 } from './fixtures/modules/app-multi-inject-multi/app/modules/app2/App'; describe('test/LoadUnit/AppGraph.test.ts', () => { it('optional module dep should work', () => { @@ -32,4 +36,42 @@ describe('test/LoadUnit/AppGraph.test.ts', () => { graph.sort(); assert(graph.moduleConfigList.length === 2); }); + + it('multi instance inject multi instance should work', () => { + const graph = new AppGraph(); + const appModuleNode = new ModuleNode({ + name: 'app', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/app'), + }); + appModuleNode.addClazz(App); + graph.addNode(appModuleNode); + + const app2ModuleNode = new ModuleNode({ + name: 'app2', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/app2'), + }); + app2ModuleNode.addClazz(App2); + graph.addNode(app2ModuleNode); + + const barOptionalModuleNode = new ModuleNode({ + name: 'bar', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/bar'), + }); + barOptionalModuleNode.addClazz(BizManager); + graph.addNode(barOptionalModuleNode); + const fooOptionalModuleNode = new ModuleNode({ + name: 'foo', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/foo'), + }); + fooOptionalModuleNode.addClazz(Secret); + graph.addNode(fooOptionalModuleNode); + graph.build(); + graph.sort(); + assert.deepStrictEqual(graph.moduleConfigList.map(t => t.name), [ + 'app', + 'app2', + 'bar', + 'foo', + ]); + }); }); diff --git a/core/metadata/test/LoadUnit.test.ts b/core/metadata/test/LoadUnit.test.ts index d1f8ef42..c67e884a 100644 --- a/core/metadata/test/LoadUnit.test.ts +++ b/core/metadata/test/LoadUnit.test.ts @@ -1,9 +1,13 @@ import path from 'path'; import assert from 'assert'; -import { EggLoadUnitType, LoadUnitFactory } from '..'; +import { AppGraph, EggLoadUnitType, LoadUnitFactory, LoadUnitLifecycleUtil, LoadUnitMultiInstanceProtoHook, ModuleNode } from '..'; import { InitTypeQualifierAttribute, ObjectInitType } from '@eggjs/core-decorator'; import { TestLoader } from './fixtures/TestLoader'; import { FOO_ATTRIBUTE } from './fixtures/modules/multi-instance-module/MultiInstance'; +import { App } from './fixtures/modules/app-multi-inject-multi/app/modules/app/App'; +import { App2 } from './fixtures/modules/app-multi-inject-multi/app/modules/app2/App'; +import { BizManager } from './fixtures/modules/app-multi-inject-multi/app/modules/bar/BizManager'; +import { Secret } from './fixtures/modules/app-multi-inject-multi/app/modules/foo/Secret'; describe('test/LoadUnit/LoadUnit.test.ts', () => { @@ -104,6 +108,16 @@ describe('test/LoadUnit/LoadUnit.test.ts', () => { }); describe('MultiInstance proto', () => { + let loadUnitMultiInstanceProtoHook: LoadUnitMultiInstanceProtoHook; + beforeEach(() => { + loadUnitMultiInstanceProtoHook = new LoadUnitMultiInstanceProtoHook(); + LoadUnitLifecycleUtil.registerLifecycle(loadUnitMultiInstanceProtoHook); + }); + + afterEach(() => { + LoadUnitLifecycleUtil.deleteLifecycle(loadUnitMultiInstanceProtoHook); + }); + it('should load static work', async () => { const multiInstanceModule = path.join(__dirname, './fixtures/modules/multi-instance-module'); const loader = new TestLoader(multiInstanceModule); @@ -134,5 +148,57 @@ describe('test/LoadUnit/LoadUnit.test.ts', () => { assert(foo2Prototype.length === 1); await LoadUnitFactory.destroyLoadUnit(loadUnit); }); + + it('should multi instance inject multi instance work', async () => { + const graph = new AppGraph(); + const appModuleNode = new ModuleNode({ + name: 'app', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/app'), + }); + appModuleNode.addClazz(App); + graph.addNode(appModuleNode); + + const app2ModuleNode = new ModuleNode({ + name: 'app2', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/app2'), + }); + app2ModuleNode.addClazz(App2); + graph.addNode(app2ModuleNode); + + const barOptionalModuleNode = new ModuleNode({ + name: 'bar', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/bar'), + }); + barOptionalModuleNode.addClazz(BizManager); + graph.addNode(barOptionalModuleNode); + const fooOptionalModuleNode = new ModuleNode({ + name: 'foo', + path: path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/foo'), + }); + fooOptionalModuleNode.addClazz(Secret); + graph.addNode(fooOptionalModuleNode); + graph.build(); + graph.sort(); + + LoadUnitMultiInstanceProtoHook.setAllClassList(graph.getClazzList()); + + const appInstanceModule = path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/app'); + const app2InstanceModule = path.join(__dirname, './fixtures/modules/app-multi-inject-multi/app/modules/app2'); + + const loader = new TestLoader(appInstanceModule); + const loadUnit = await LoadUnitFactory.createLoadUnit(appInstanceModule, EggLoadUnitType.MODULE, loader); + const loader2 = new TestLoader(app2InstanceModule); + const loadUnit2 = await LoadUnitFactory.createLoadUnit(app2InstanceModule, EggLoadUnitType.MODULE, loader2); + + const app1Prototype = loadUnit.getEggPrototype('app', []); + const app2Prototype = loadUnit.getEggPrototype('app2', []); + + assert(app1Prototype); + assert(app2Prototype); + + await LoadUnitFactory.destroyLoadUnit(loadUnit); + await LoadUnitFactory.destroyLoadUnit(loadUnit2); + LoadUnitMultiInstanceProtoHook.clear(); + }); }); }); diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/App.ts b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/App.ts new file mode 100644 index 00000000..1ea819b7 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/App.ts @@ -0,0 +1,9 @@ +import { Inject, SingletonProto } from '@eggjs/core-decorator'; +import { BizManager, BizManagerQualifier } from '../bar/BizManager'; + +@SingletonProto() +export class App { + @Inject() + @BizManagerQualifier('foo') + bizManager: BizManager; +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/module.yml b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/module.yml new file mode 100644 index 00000000..34e00088 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/module.yml @@ -0,0 +1,9 @@ +BizManager: + clients: + foo: {} + bar: {} + +secret: + keys: + - '1' + - '2' diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/package.json b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/package.json new file mode 100644 index 00000000..21a0b239 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app/package.json @@ -0,0 +1,6 @@ +{ + "name": "app", + "eggModule": { + "name": "app" + } +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/App.ts b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/App.ts new file mode 100644 index 00000000..06c1e1c6 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/App.ts @@ -0,0 +1,9 @@ +import { Inject, SingletonProto } from '@eggjs/core-decorator'; +import { Secret, SecretQualifier } from '../foo/Secret'; + +@SingletonProto() +export class App2 { + @Inject() + @SecretQualifier('app2') + secret: Secret; +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/module.yml b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/module.yml new file mode 100644 index 00000000..0b760277 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/module.yml @@ -0,0 +1,4 @@ +secret: + keys: + - '1' + - '2' diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/package.json b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/package.json new file mode 100644 index 00000000..22879d25 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/app2/package.json @@ -0,0 +1,6 @@ +{ + "name": "app2", + "eggModule": { + "name": "app2" + } +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/bar/BizManager.ts b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/bar/BizManager.ts new file mode 100644 index 00000000..82f9bc5c --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/bar/BizManager.ts @@ -0,0 +1,62 @@ +import { + MultiInstanceProto, + AccessLevel, + Inject, + ObjectInitType, + ObjectInfo, + MultiInstancePrototypeGetObjectsContext, + MultiInstanceInfo, +} from '@eggjs/tegg'; +import { ModuleConfigUtil } from '@eggjs/tegg-common-util'; +import { EggProtoImplClass, QualifierUtil } from '@eggjs/core-decorator'; +import { Secret, SecretQualifierAttribute } from '../foo/Secret'; + +export const BizManagerQualifierAttribute = Symbol.for('Qualifier.ChatModel'); +export const BizManagerInjectName = 'bizManager'; + +export function BizManagerQualifier(chatModelName: string) { + return function(target: any, propertyKey: PropertyKey) { + QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, + propertyKey, BizManagerQualifierAttribute, chatModelName); + }; +} + + +@MultiInstanceProto({ + accessLevel: AccessLevel.PUBLIC, + initType: ObjectInitType.SINGLETON, + // 从 module.yml 中动态获取配置来决定需要初始化几个对象 + getObjects(ctx: MultiInstancePrototypeGetObjectsContext) { + const config = ModuleConfigUtil.loadModuleConfigSync(ctx.unitPath) as any; + const name = ModuleConfigUtil.readModuleNameSync(ctx.unitPath); + const clients = config?.BizManager?.clients; + if (!clients) return []; + return Object.keys(clients).map((clientName: string) => { + return { + name: BizManagerInjectName, + qualifiers: [{ + attribute: BizManagerQualifierAttribute, + value: clientName, + }], + properQualifiers: { + secret: [{ + attribute: SecretQualifierAttribute, + value: name, + }], + }, + }; + }); + }, +}) +export class BizManager { + readonly name: string; + readonly secret: string; + + constructor( + @Inject() secret: Secret, + @MultiInstanceInfo([ BizManagerQualifierAttribute ]) objInfo: ObjectInfo, + ) { + this.name = objInfo.name as string; + this.secret = secret.getSecret(this.name); + } +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/bar/package.json b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/bar/package.json new file mode 100644 index 00000000..acfa2b48 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/bar/package.json @@ -0,0 +1,6 @@ +{ + "name": "bar", + "eggModule": { + "name": "bar" + } +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/foo/Secret.ts b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/foo/Secret.ts new file mode 100644 index 00000000..8dfa7d1c --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/foo/Secret.ts @@ -0,0 +1,42 @@ +import { + MultiInstanceProto, + MultiInstancePrototypeGetObjectsContext, + ObjectInitType, + AccessLevel, + QualifierUtil, +} from '@eggjs/tegg'; +import { ModuleConfigUtil } from '@eggjs/tegg/helper'; +import { EggProtoImplClass } from '@eggjs/tegg-types'; + +export const SecretQualifierAttribute = Symbol.for('Qualifier.Secret'); +export const SecretInjectName = 'secret'; + +export function SecretQualifier(chatModelName: string) { + return function(target: any, propertyKey: PropertyKey) { + QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, + propertyKey, SecretQualifierAttribute, chatModelName); + }; +} + +@MultiInstanceProto({ + accessLevel: AccessLevel.PUBLIC, + initType: ObjectInitType.SINGLETON, + getObjects(ctx: MultiInstancePrototypeGetObjectsContext) { + const config = ModuleConfigUtil.loadModuleConfigSync(ctx.unitPath) as any; + const keys = config?.secret?.keys; + if (!keys || keys.length === 0) return []; + const name = ModuleConfigUtil.readModuleNameSync(ctx.unitPath); + return [{ + name: SecretInjectName, + qualifiers: [{ + attribute: SecretQualifierAttribute, + value: name, + }], + }]; + }, +}) +export class Secret { + getSecret(key: string): string { + return key + '233'; + } +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/foo/package.json b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/foo/package.json new file mode 100644 index 00000000..7f30a320 --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/app/modules/foo/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "eggModule": { + "name": "foo" + } +} diff --git a/core/metadata/test/fixtures/modules/app-multi-inject-multi/package.json b/core/metadata/test/fixtures/modules/app-multi-inject-multi/package.json new file mode 100644 index 00000000..cedcd87c --- /dev/null +++ b/core/metadata/test/fixtures/modules/app-multi-inject-multi/package.json @@ -0,0 +1,3 @@ +{ + "name": "app-multi-inject-multi" +} diff --git a/core/types/core-decorator/model/EggMultiInstancePrototypeInfo.ts b/core/types/core-decorator/model/EggMultiInstancePrototypeInfo.ts index 7422bde3..8b959966 100644 --- a/core/types/core-decorator/model/EggMultiInstancePrototypeInfo.ts +++ b/core/types/core-decorator/model/EggMultiInstancePrototypeInfo.ts @@ -6,6 +6,7 @@ import { QualifierInfo } from './QualifierInfo'; export interface ObjectInfo { name: EggPrototypeName; qualifiers: QualifierInfo[]; + properQualifiers?: Record; } export interface MultiInstancePrototypeGetObjectsContext { diff --git a/core/types/core-decorator/model/EggPrototypeInfo.ts b/core/types/core-decorator/model/EggPrototypeInfo.ts index f612897f..a886a773 100644 --- a/core/types/core-decorator/model/EggPrototypeInfo.ts +++ b/core/types/core-decorator/model/EggPrototypeInfo.ts @@ -30,4 +30,8 @@ export interface EggPrototypeInfo { * EggPrototype qualifiers */ qualifiers?: QualifierInfo[]; + /** + * EggPrototype properties qualifiers + */ + properQualifiers?: QualifierInfo[]; } diff --git a/plugin/dal/lib/DataSource.ts b/plugin/dal/lib/DataSource.ts index 1a6eb34c..ba8eeeea 100644 --- a/plugin/dal/lib/DataSource.ts +++ b/plugin/dal/lib/DataSource.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { - AccessLevel, + AccessLevel, Inject, LifecycleInit, MultiInstanceProto, MultiInstancePrototypeGetObjectsContext, @@ -26,6 +26,7 @@ import { DataSource } from '@eggjs/dal-runtime'; import { TableModelManager } from './TableModelManager'; import { MysqlDataSourceManager } from './MysqlDataSourceManager'; import { SqlMapManager } from './SqlMapManager'; +import { TransactionalAOP } from './TransactionalAOP'; @MultiInstanceProto({ accessLevel: AccessLevel.PUBLIC, @@ -62,6 +63,14 @@ import { SqlMapManager } from './SqlMapManager'; export class DataSourceDelegate implements IDataSource { private dataSource: DataSource; + // register aop here let module dependent teggDal + @Inject({ + name: 'transactionalAOP', + }) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + private transactionalAOP: TransactionalAOP; + @LifecycleInit() async init(_: EggObjectLifeCycleContext, obj: EggObject) { const dataSourceQualifierValue = obj.proto.getQualifier(DataSourceQualifierAttribute); diff --git a/plugin/tegg/app.ts b/plugin/tegg/app.ts index 95136afe..61645be7 100644 --- a/plugin/tegg/app.ts +++ b/plugin/tegg/app.ts @@ -67,5 +67,6 @@ export default class App { if (this.loadUnitMultiInstanceProtoHook) { this.app.loadUnitLifecycleUtil.deleteLifecycle(this.loadUnitMultiInstanceProtoHook); } + LoadUnitMultiInstanceProtoHook.clear(); } } diff --git a/plugin/tegg/lib/EggModuleLoader.ts b/plugin/tegg/lib/EggModuleLoader.ts index 619e2116..5904a88b 100644 --- a/plugin/tegg/lib/EggModuleLoader.ts +++ b/plugin/tegg/lib/EggModuleLoader.ts @@ -1,4 +1,11 @@ -import { EggLoadUnitType, Loader, LoadUnitFactory, AppGraph, ModuleNode } from '@eggjs/tegg-metadata'; +import { + EggLoadUnitType, + Loader, + LoadUnitFactory, + AppGraph, + ModuleNode, + LoadUnitMultiInstanceProtoHook, +} from '@eggjs/tegg-metadata'; import { LoaderFactory } from '@eggjs/tegg-loader'; import { EggAppLoader } from './EggAppLoader'; import { Application } from 'egg'; @@ -44,6 +51,7 @@ export class EggModuleLoader { const loaderCache = new Map(); const appGraph = this.buildAppGraph(loaderCache); appGraph.sort(); + LoadUnitMultiInstanceProtoHook.setAllClassList(appGraph.getClazzList()); const moduleConfigList = appGraph.moduleConfigList; for (const moduleConfig of moduleConfigList) { const modulePath = moduleConfig.path; diff --git a/plugin/tegg/test/MultiInstanceInjectMultiInstance.test.ts b/plugin/tegg/test/MultiInstanceInjectMultiInstance.test.ts new file mode 100644 index 00000000..a4099f6f --- /dev/null +++ b/plugin/tegg/test/MultiInstanceInjectMultiInstance.test.ts @@ -0,0 +1,41 @@ +import mm from 'egg-mock'; +import path from 'path'; +import assert from 'assert'; +import { App2 } from './fixtures/apps/app-multi-inject-multi/app/modules/app2/App'; +import { App } from './fixtures/apps/app-multi-inject-multi/app/modules/app/App'; + +describe('plugin/tegg/test/MultiInstanceInjectMultiInstance.test.ts', () => { + let app; + + after(async () => { + await app.close(); + }); + + afterEach(() => { + mm.restore(); + }); + + before(async () => { + mm(process.env, 'EGG_TYPESCRIPT', true); + mm(process, 'cwd', () => { + return path.join(__dirname, '..'); + }); + app = mm.app({ + baseDir: path.join(__dirname, 'fixtures/apps/app-multi-inject-multi'), + framework: require.resolve('egg'), + }); + await app.ready(); + }); + + it('dynamic inject should work', async () => { + const app2Instance: App2 = await app.getEggObject(App2); + const appInstance: App = await app.getEggObject(App); + const app2Secret = app2Instance.secret.getSecret('mock'); + const appName = appInstance.bizManager.name; + const appSecret = appInstance.bizManager.secret; + + assert.equal(app2Secret, 'mock233'); + assert.equal(appName, 'foo'); + assert.equal(appSecret, 'foo233'); + }); +}); diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/App.ts b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/App.ts new file mode 100644 index 00000000..1ea819b7 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/App.ts @@ -0,0 +1,9 @@ +import { Inject, SingletonProto } from '@eggjs/core-decorator'; +import { BizManager, BizManagerQualifier } from '../bar/BizManager'; + +@SingletonProto() +export class App { + @Inject() + @BizManagerQualifier('foo') + bizManager: BizManager; +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/module.yml b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/module.yml new file mode 100644 index 00000000..34e00088 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/module.yml @@ -0,0 +1,9 @@ +BizManager: + clients: + foo: {} + bar: {} + +secret: + keys: + - '1' + - '2' diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/package.json b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/package.json new file mode 100644 index 00000000..21a0b239 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app/package.json @@ -0,0 +1,6 @@ +{ + "name": "app", + "eggModule": { + "name": "app" + } +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/App.ts b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/App.ts new file mode 100644 index 00000000..06c1e1c6 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/App.ts @@ -0,0 +1,9 @@ +import { Inject, SingletonProto } from '@eggjs/core-decorator'; +import { Secret, SecretQualifier } from '../foo/Secret'; + +@SingletonProto() +export class App2 { + @Inject() + @SecretQualifier('app2') + secret: Secret; +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/module.yml b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/module.yml new file mode 100644 index 00000000..0b760277 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/module.yml @@ -0,0 +1,4 @@ +secret: + keys: + - '1' + - '2' diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/package.json b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/package.json new file mode 100644 index 00000000..22879d25 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/app2/package.json @@ -0,0 +1,6 @@ +{ + "name": "app2", + "eggModule": { + "name": "app2" + } +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/bar/BizManager.ts b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/bar/BizManager.ts new file mode 100644 index 00000000..317454ed --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/bar/BizManager.ts @@ -0,0 +1,62 @@ +import { + MultiInstanceProto, + AccessLevel, + Inject, + ObjectInitType, + ObjectInfo, + MultiInstancePrototypeGetObjectsContext, + MultiInstanceInfo, +} from '@eggjs/tegg'; +import { ModuleConfigUtil } from '@eggjs/tegg-common-util'; +import { EggProtoImplClass, QualifierUtil } from '@eggjs/core-decorator'; +import { Secret, SecretQualifierAttribute } from '../foo/Secret'; + +export const BizManagerQualifierAttribute = Symbol.for('Qualifier.ChatModel'); +export const BizManagerInjectName = 'bizManager'; + +export function BizManagerQualifier(chatModelName: string) { + return function(target: any, propertyKey: PropertyKey) { + QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, + propertyKey, BizManagerQualifierAttribute, chatModelName); + }; +} + + +@MultiInstanceProto({ + accessLevel: AccessLevel.PUBLIC, + initType: ObjectInitType.SINGLETON, + // 从 module.yml 中动态获取配置来决定需要初始化几个对象 + getObjects(ctx: MultiInstancePrototypeGetObjectsContext) { + const config = ModuleConfigUtil.loadModuleConfigSync(ctx.unitPath) as any; + const name = ModuleConfigUtil.readModuleNameSync(ctx.unitPath); + const clients = config?.BizManager?.clients; + if (!clients) return []; + return Object.keys(clients).map((clientName: string) => { + return { + name: BizManagerInjectName, + qualifiers: [{ + attribute: BizManagerQualifierAttribute, + value: clientName, + }], + properQualifiers: { + secret: [{ + attribute: SecretQualifierAttribute, + value: name, + }], + }, + }; + }); + }, +}) +export class BizManager { + readonly name: string; + readonly secret: string; + + constructor( + @Inject() secret: Secret, + @MultiInstanceInfo([ BizManagerQualifierAttribute ]) objInfo: ObjectInfo, + ) { + this.name = objInfo.qualifiers.find(t => t.attribute === BizManagerQualifierAttribute)!.value as string; + this.secret = secret.getSecret(this.name); + } +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/bar/package.json b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/bar/package.json new file mode 100644 index 00000000..acfa2b48 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/bar/package.json @@ -0,0 +1,6 @@ +{ + "name": "bar", + "eggModule": { + "name": "bar" + } +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/foo/Secret.ts b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/foo/Secret.ts new file mode 100644 index 00000000..8dfa7d1c --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/foo/Secret.ts @@ -0,0 +1,42 @@ +import { + MultiInstanceProto, + MultiInstancePrototypeGetObjectsContext, + ObjectInitType, + AccessLevel, + QualifierUtil, +} from '@eggjs/tegg'; +import { ModuleConfigUtil } from '@eggjs/tegg/helper'; +import { EggProtoImplClass } from '@eggjs/tegg-types'; + +export const SecretQualifierAttribute = Symbol.for('Qualifier.Secret'); +export const SecretInjectName = 'secret'; + +export function SecretQualifier(chatModelName: string) { + return function(target: any, propertyKey: PropertyKey) { + QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, + propertyKey, SecretQualifierAttribute, chatModelName); + }; +} + +@MultiInstanceProto({ + accessLevel: AccessLevel.PUBLIC, + initType: ObjectInitType.SINGLETON, + getObjects(ctx: MultiInstancePrototypeGetObjectsContext) { + const config = ModuleConfigUtil.loadModuleConfigSync(ctx.unitPath) as any; + const keys = config?.secret?.keys; + if (!keys || keys.length === 0) return []; + const name = ModuleConfigUtil.readModuleNameSync(ctx.unitPath); + return [{ + name: SecretInjectName, + qualifiers: [{ + attribute: SecretQualifierAttribute, + value: name, + }], + }]; + }, +}) +export class Secret { + getSecret(key: string): string { + return key + '233'; + } +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/foo/package.json b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/foo/package.json new file mode 100644 index 00000000..7f30a320 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/app/modules/foo/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "eggModule": { + "name": "foo" + } +} diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/config/config.default.js b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/config/config.default.js new file mode 100644 index 00000000..b674459f --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/config/config.default.js @@ -0,0 +1,15 @@ +'use strict'; + +const path = require('path'); + +module.exports = function(appInfo) { + const config = { + keys: 'test key', + security: { + csrf: { + ignoreJSON: false, + }, + }, + }; + return config; +}; diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/config/plugin.js b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/config/plugin.js new file mode 100644 index 00000000..10d5c293 --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/config/plugin.js @@ -0,0 +1,13 @@ +'use strict'; + +exports.tracer = { + package: 'egg-tracer', + enable: true, +}; + +exports.teggConfig = { + package: '@eggjs/tegg-config', + enable: true, +}; + +exports.watcher = false; diff --git a/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/package.json b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/package.json new file mode 100644 index 00000000..cedcd87c --- /dev/null +++ b/plugin/tegg/test/fixtures/apps/app-multi-inject-multi/package.json @@ -0,0 +1,3 @@ +{ + "name": "app-multi-inject-multi" +} diff --git a/standalone/standalone/src/EggModuleLoader.ts b/standalone/standalone/src/EggModuleLoader.ts index 7b181afb..4b4b980e 100644 --- a/standalone/standalone/src/EggModuleLoader.ts +++ b/standalone/standalone/src/EggModuleLoader.ts @@ -1,4 +1,10 @@ -import { EggLoadUnitType, Loader, LoadUnit, LoadUnitFactory } from '@eggjs/tegg-metadata'; +import { + EggLoadUnitType, + Loader, + LoadUnit, + LoadUnitFactory, + LoadUnitMultiInstanceProtoHook, +} from '@eggjs/tegg-metadata'; import { LoaderFactory } from '@eggjs/tegg-loader'; import { AppGraph, ModuleNode } from '@eggjs/tegg/helper'; import { ModuleReference } from '@eggjs/tegg-common-util'; @@ -32,6 +38,7 @@ export class EggModuleLoader { const loaderCache = new Map(); const appGraph = EggModuleLoader.generateAppGraph(loaderCache, this.moduleReferences); appGraph.sort(); + LoadUnitMultiInstanceProtoHook.setAllClassList(appGraph.getClazzList()); const moduleConfigList = appGraph.moduleConfigList; for (const moduleConfig of moduleConfigList) { const modulePath = moduleConfig.path;