From ced2ce2134964dcb410410c0192a34f77507c42d Mon Sep 17 00:00:00 2001 From: killa Date: Wed, 20 Jul 2022 22:36:34 +0800 Subject: [PATCH] feat: impl Inject Model (#43) --- core/orm-decorator/README.md | 6 +- core/orm-decorator/src/decorator/Model.ts | 4 ++ .../src/util/ModelMetadataUtil.ts | 4 +- plugin/orm/app.ts | 6 ++ plugin/orm/lib/ContextModeObject.ts | 70 +++++++++++++++++++ plugin/orm/lib/ContextModelProto.ts | 59 ++++++++++++++++ plugin/orm/lib/LeoricRegister.ts | 9 ++- plugin/orm/lib/ModelProtoHook.ts | 2 +- plugin/orm/package.json | 1 + plugin/orm/test/fixtures/apps/orm-app/app.ts | 23 ++++++ .../orm-app/modules/orm-module/AppService.ts | 6 +- .../orm-app/modules/orm-module/PkgService.ts | 6 +- plugin/orm/test/index.test.ts | 8 +++ plugin/orm/typings/index.d.ts | 10 +++ 14 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 plugin/orm/lib/ContextModeObject.ts create mode 100644 plugin/orm/lib/ContextModelProto.ts create mode 100644 plugin/orm/test/fixtures/apps/orm-app/app.ts diff --git a/core/orm-decorator/README.md b/core/orm-decorator/README.md index 20a5cb14..f773ca24 100644 --- a/core/orm-decorator/README.md +++ b/core/orm-decorator/README.md @@ -24,13 +24,13 @@ export class App extends Bone { ## Use Model ```ts -import { ContextProto } from '@eggjs/tegg'; +import { ContextProto, Inject } from '@eggjs/tegg'; import { App } from './model/App'; @ContextProto() export class AppService { - // TODO impl inject Bone for context - App: typeof App = App; + @Inject() + App: typeof App; async createApp(data: { name: string; diff --git a/core/orm-decorator/src/decorator/Model.ts b/core/orm-decorator/src/decorator/Model.ts index 7e30e80f..3fecf9dc 100644 --- a/core/orm-decorator/src/decorator/Model.ts +++ b/core/orm-decorator/src/decorator/Model.ts @@ -5,11 +5,15 @@ export interface ModelParams { tableName?: string; } +export const MODEL_PROTO_IMPL_TYPE = 'MODEL_PROTO'; + export function Model(param?: ModelParams) { return function(clazz: EggProtoImplClass) { ModelInfoUtil.setIsModel(true, clazz); const func = ContextProto({ + name: clazz.name, accessLevel: AccessLevel.PUBLIC, + protoImplType: MODEL_PROTO_IMPL_TYPE, }); if (param?.tableName) { ModelInfoUtil.setTableName(param.tableName, clazz); diff --git a/core/orm-decorator/src/util/ModelMetadataUtil.ts b/core/orm-decorator/src/util/ModelMetadataUtil.ts index 131cce1d..a6c6bb47 100644 --- a/core/orm-decorator/src/util/ModelMetadataUtil.ts +++ b/core/orm-decorator/src/util/ModelMetadataUtil.ts @@ -4,11 +4,11 @@ import { ModelMetadata } from '../model/ModelMetadata'; export const MODEL_METADATA = Symbol.for('EggPrototype#model#metadata'); export class ModelMetadataUtil { - static setControllerMetadata(clazz: EggProtoImplClass, metaData: ModelMetadata) { + static setModelMetadata(clazz: EggProtoImplClass, metaData: ModelMetadata) { MetadataUtil.defineMetaData(MODEL_METADATA, metaData, clazz); } - static getControllerMetadata(clazz): ModelMetadata | undefined { + static getModelMetadata(clazz): ModelMetadata | undefined { return MetadataUtil.getOwnMetaData(MODEL_METADATA, clazz); } } diff --git a/plugin/orm/app.ts b/plugin/orm/app.ts index 9de4d7c8..c0adf767 100644 --- a/plugin/orm/app.ts +++ b/plugin/orm/app.ts @@ -3,6 +3,9 @@ import { DataSourceManager } from './lib/DataSourceManager'; import { LeoricRegister } from './lib/LeoricRegister'; import { ModelProtoManager } from './lib/ModelProtoManager'; import { ModelProtoHook } from './lib/ModelProtoHook'; +import { MODEL_PROTO_IMPL_TYPE } from '@eggjs/tegg-orm-decorator'; +import ContextModelProto from './lib/ContextModelProto'; +import { ContextModeObject } from './lib/ContextModeObject'; export default class OrmAppBootHook { private readonly app: Application; @@ -17,10 +20,13 @@ export default class OrmAppBootHook { this.modelProtoManager = new ModelProtoManager(); this.leoricRegister = new LeoricRegister(this.modelProtoManager, this.dataSourceManager); this.modelProtoHook = new ModelProtoHook(this.modelProtoManager); + this.app.eggPrototypeCreatorFactory.registerPrototypeCreator(MODEL_PROTO_IMPL_TYPE, ContextModelProto.createProto); + this.app.leoricRegister = this.leoricRegister; } configWillLoad() { this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.modelProtoHook); + this.app.eggObjectFactory.registerEggObjectCreateMethod(ContextModelProto, ContextModeObject.createObject); } configDidLoad() { diff --git a/plugin/orm/lib/ContextModeObject.ts b/plugin/orm/lib/ContextModeObject.ts new file mode 100644 index 00000000..e2e1877a --- /dev/null +++ b/plugin/orm/lib/ContextModeObject.ts @@ -0,0 +1,70 @@ +import assert from 'assert'; +import { + EggContext, + EggObject, + EggObjectLifeCycleContext, + EggObjectStatus, +} from '@eggjs/tegg-runtime'; +import { EggPrototype } from '@eggjs/tegg-metadata'; +import { EggPrototypeName, EggObjectName } from '@eggjs/tegg'; +import { Id, IdenticalUtil } from '@eggjs/tegg-lifecycle'; +import { Bone } from 'leoric'; +import ContextModelProto from './ContextModelProto'; +import { EGG_CONTEXT } from '@eggjs/egg-module-common'; + +export class ContextModeObject implements EggObject { + private status: EggObjectStatus = EggObjectStatus.PENDING; + id: Id; + readonly name: EggPrototypeName; + private _obj: typeof Bone; + readonly proto: ContextModelProto; + readonly ctx: EggContext; + + constructor(name: EggObjectName, proto: ContextModelProto, ctx: EggContext) { + this.name = name; + this.proto = proto; + this.ctx = ctx; + this.id = IdenticalUtil.createObjectId(this.proto.id, this.ctx.id); + } + + async init() { + const ctx = this.ctx; + const clazz = class ContextModelClass extends this.proto.model { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + static get name() { + return super.name; + } + + static get ctx() { + return ctx.get(EGG_CONTEXT); + } + + // custom setter always execute before define [CTX] when new Instance(super(opts) calling), if custom setter requires ctx, it should not be undefined + get ctx() { + return ctx.get(EGG_CONTEXT); + } + }; + this._obj = clazz; + this.status = EggObjectStatus.READY; + } + + injectProperty() { + throw new Error('never call ModelObject#injectProperty'); + } + + get isReady() { + return this.status === EggObjectStatus.READY; + } + + get obj() { + return this._obj; + } + + static async createObject(name: EggObjectName, proto: EggPrototype, _: EggObjectLifeCycleContext, ctx?: EggContext): Promise { + assert(ctx, 'ctx must be defined for ContextModelObject'); + const modelObject = new ContextModeObject(name, proto as ContextModelProto, ctx); + await modelObject.init(); + return modelObject; + } +} diff --git a/plugin/orm/lib/ContextModelProto.ts b/plugin/orm/lib/ContextModelProto.ts new file mode 100644 index 00000000..a989b0d1 --- /dev/null +++ b/plugin/orm/lib/ContextModelProto.ts @@ -0,0 +1,59 @@ +import { EggPrototype, LoadUnit, EggPrototypeLifecycleContext } from '@eggjs/tegg-metadata'; +import { + AccessLevel, + EggPrototypeName, + ObjectInitType, + QualifierInfo, + QualifierUtil, + MetadataUtil, + MetaDataKey, +} from '@eggjs/tegg'; +import { Id, IdenticalUtil } from '@eggjs/tegg-lifecycle'; +import { Bone } from 'leoric'; + +export default class ContextModelProto implements EggPrototype { + private readonly qualifiers: QualifierInfo[]; + readonly accessLevel = AccessLevel.PUBLIC; + id: Id; + readonly initType = ObjectInitType.CONTEXT; + readonly injectObjects = []; + readonly loadUnitId: string; + readonly moduleName: string; + readonly name: EggPrototypeName; + readonly model: typeof Bone; + + constructor(loadUnit: LoadUnit, model: typeof Bone) { + this.model = model; + this.id = IdenticalUtil.createProtoId(loadUnit.id, `leoric:${model.name}`); + this.loadUnitId = loadUnit.id; + this.moduleName = loadUnit.name; + this.name = model.name; + this.qualifiers = QualifierUtil.getProtoQualifiers(model); + } + + constructEggObject(): object { + return {}; + } + + getMetaData(metadataKey: MetaDataKey): T | undefined { + return MetadataUtil.getMetaData(metadataKey, this.model); + } + + verifyQualifier(qualifier: QualifierInfo): boolean { + const selfQualifiers = this.qualifiers.find(t => t.attribute === qualifier.attribute); + return selfQualifiers?.value === qualifier.value; + } + + verifyQualifiers(qualifiers: QualifierInfo[]): boolean { + for (const qualifier of qualifiers) { + if (!this.verifyQualifier(qualifier)) { + return false; + } + } + return true; + } + + static createProto(ctx: EggPrototypeLifecycleContext): ContextModelProto { + return new ContextModelProto(ctx.loadUnit, ctx.clazz as typeof Bone); + } +} diff --git a/plugin/orm/lib/LeoricRegister.ts b/plugin/orm/lib/LeoricRegister.ts index 716bfd4d..680407a5 100644 --- a/plugin/orm/lib/LeoricRegister.ts +++ b/plugin/orm/lib/LeoricRegister.ts @@ -1,15 +1,17 @@ +import Base from 'sdk-base'; import { ModelProtoManager } from './ModelProtoManager'; import { DataSourceManager, OrmConfig } from './DataSourceManager'; import Realm from 'leoric'; import { hookNames } from 'leoric/src/setup_hooks'; import { ModelMetadata, ModelMetadataUtil } from '@eggjs/tegg-orm-decorator'; -export class LeoricRegister { +export class LeoricRegister extends Base { private readonly modelProtoManager: ModelProtoManager; private readonly dataSourceManager: DataSourceManager; - private readonly realmMap: Map; + readonly realmMap: Map; constructor(modelProtoManager: ModelProtoManager, dataSourceManager: DataSourceManager) { + super(); this.modelProtoManager = modelProtoManager; this.dataSourceManager = dataSourceManager; this.realmMap = new Map(); @@ -51,7 +53,7 @@ export class LeoricRegister { async register() { for (const { proto, clazz } of this.modelProtoManager.getProtos()) { - const metadata = ModelMetadataUtil.getControllerMetadata(clazz); + const metadata = ModelMetadataUtil.getModelMetadata(clazz); if (!metadata) throw new Error(`not found metadata for model ${proto.id}`); const realm = this.getOrCreateRealm(metadata.dataSource); realm.models[clazz.name] = clazz; @@ -71,5 +73,6 @@ export class LeoricRegister { } await Promise.all(Array.from(this.realmMap.values()) .map(realm => realm.connect())); + this.ready(true); } } diff --git a/plugin/orm/lib/ModelProtoHook.ts b/plugin/orm/lib/ModelProtoHook.ts index fc89ddb1..6dc9d2c0 100644 --- a/plugin/orm/lib/ModelProtoHook.ts +++ b/plugin/orm/lib/ModelProtoHook.ts @@ -16,7 +16,7 @@ export class ModelProtoHook implements LifecycleHook { assert(findModel.name === 'egg_before_create_hook'); assert(findModel.desc === 'the framework'); }); + + it('ctx should inject with Model', async () => { + ctx = await app.mockModuleContext(); + const appService = await ctx.getEggObject(AppService); + app.mockLog(); + await appService.findApp('egg'); + app.expectLog(/sql: SELECT \* FROM `apps` WHERE `name` = 'egg' LIMIT 1 path: \//); + }); }); diff --git a/plugin/orm/typings/index.d.ts b/plugin/orm/typings/index.d.ts index c1a426a0..001a6f91 100644 --- a/plugin/orm/typings/index.d.ts +++ b/plugin/orm/typings/index.d.ts @@ -3,7 +3,17 @@ import '@eggjs/tegg-plugin'; import { DataType } from 'leoric'; import { AttributeOptions } from '@eggjs/tegg-orm-decorator'; +import { LeoricRegister } from '../lib/LeoricRegister'; declare module '@eggjs/tegg-orm-decorator' { export declare function Attribute(dataType: DataType, options?: AttributeOptions): (target: any, propertyKey: PropertyKey) => void; } + +declare module 'egg' { + export interface TeggOrmApplication { + leoricRegister: LeoricRegister; + } + + interface Application extends TeggOrmApplication { + } +}