import { type EntityData, LockMode, type RequiredEntityData } from '@mikro-orm/core'; import type { EntityManager, EntityRepository, FindOneOptions, MikroORM, QueryBuilder } from '@mikro-orm/postgresql'; import { Logger } from '@nestjs/common'; import { Errors, type MaybeArray } from '@wener/utils'; import { Contexts, getCurrentTenantId, getCurrentUserId } from '../../app'; import { Features } from '../../Feature'; import { EntityAuditAction, writeEntityAuditLog } from '../audit'; import { EntityFeature } from '../enum'; import type { HasEntityRefEntity } from '../mixins'; import { resolveEntityRef2 } from '../resolveEntityRef'; import type { StandardBaseEntity } from '../StandardBaseEntity'; import { setData } from '../setData'; import { setOwnerRef } from '../setOwnerRef'; import type { AnyStandardEntity } from '../types'; import { resolveEntitySearch } from './applySearch'; import { createQueryBuilder } from './createQueryBuilder'; import type { EntityClass } from './EntityClass'; import { type FindAllEntityOptions, type FindAllEntityResult, findAllEntity } from './findAllEntity'; import { hasEntityFeature } from './hasEntityFeature'; import { type ResolveEntityOptions, type ResolveEntityResult, resolveEntity } from './resolveEntity'; import type { AssignEntityOwnerOptions, BindEntityOptions, ClaimEntityOwnerOptions, CreateEntityOptions, EntityResult, EntityService2, HasEntityRefService, HasOwnerEntityService, HasStatusEntityService, ReleaseEntityOwnerOptions, ResolveSearchOptions, SetEntityNotesOptions, SetEntityStatusOptions, UpdateEntityOptions, } from './services'; export interface EntityServiceOptions { softDelete?: boolean; } export class BaseEntityService implements EntityService2, HasStatusEntityService, HasEntityRefService, HasOwnerEntityService { readonly log = new Logger(this.constructor.name); protected readonly options: EntityServiceOptions = { softDelete: false }; constructor( protected readonly orm: MikroORM, readonly Entity: EntityClass, readonly em: EntityManager = orm.em, readonly repo: EntityRepository = em.getRepository(Entity), ) { // if (EntityBaseService.#services.has(Entity)) { // let last = EntityBaseService.#services.get(Entity); // this.log.error( // `ResourceEntityService for ${Entity.name} already exists: ${last?.constructor.name} <-> ${this.constructor.name} `, // ); // } else { // EntityBaseService.#services.set(Entity, this); // EntityBaseService.#services.set(Entity.name, this); // } } applySearch({ builder, search }: { builder: QueryBuilder; search: string }) { const { and, or } = this.resolveSearch({ search }); and?.length && builder.andWhere(and as any); or?.length && builder.andWhere({ $or: or } as any); } resolveSearch(opts: ResolveSearchOptions) { return resolveEntitySearch({ ...opts, Entity: this.Entity, hasFeature: this.hasFeature }); } async claimEntityOwner(ent: ResolveEntityOptions, _opts: ClaimEntityOwnerOptions): Promise> { const userId = Contexts.userId.require(); const { entity } = await this.requireEntity(ent); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasOwnerRef), '资源不支持所有权'); Errors.BadRequest.check(userId, '未能获取到当前用户信息'); Errors.Forbidden.check(!entity.ownerId, '资源已经被分配'); setOwnerRef(entity, userId); await this.em.persist(entity).flush(); return { entity }; } async assignEntityOwner(ent: ResolveEntityOptions, opts: AssignEntityOwnerOptions): Promise> { const { entity } = await this.requireEntity(ent); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasOwnerRef), '资源不支持所有权'); setOwnerRef(entity, opts.ownerId); await this.em.persist(entity).flush(); return { entity }; } async releaseEntityOwner(ent: ResolveEntityOptions, _opts: ReleaseEntityOwnerOptions): Promise> { const { entity } = await this.requireEntity(ent); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasOwnerRef), '资源不支持所有权'); entity.ownerId = undefined; entity.ownerType = undefined; await this.em.persist(entity).flush(); return { entity }; } async createQueryBuilder({ em = this.em }: { em?: EntityManager } = {}): Promise<{ builder: QueryBuilder }> { return createQueryBuilder(em, this.Entity) as any; } async requireEntity(req: ResolveEntityOptions, ext?: FindOneOptions): Promise> { let { entity } = await this.resolveEntity(req, ext); Errors.NotFound.check(entity, 'entity not found'); return { entity }; } async resolveEntity(req: ResolveEntityOptions, ext?: FindOneOptions): Promise> { return resolveEntity(req, this, ext); } async findAllEntity(req: FindAllEntityOptions): Promise> { return findAllEntity(req, this); } async deleteEntity(req: ResolveEntityOptions): Promise<{ entity?: E }> { const { em } = this; const { entity } = await this.resolveEntity(req); if (!entity) { return {}; } entity.deletedAt = new Date(); if (hasEntityFeature(entity, EntityFeature.HasAuditorRef)) { entity.deletedById = getCurrentUserId(); } if (this.options.softDelete) { em.persist(entity); } else { em.remove(entity); } writeEntityAuditLog({ entity, action: EntityAuditAction.Delete, em }); await em.flush(); return { entity }; } undeleteEntity(req: ResolveEntityOptions): Promise<{ entity?: E }> { return this.em.transactional(async (em) => { const { entity } = await this.resolveEntity(req); if (!entity) { return {}; } entity.deletedAt = undefined; if (hasEntityFeature(entity, EntityFeature.HasAuditorRef)) { entity.deletedById = undefined; } writeEntityAuditLog({ entity, action: EntityAuditAction.Undelete, em }); await em.persist(entity).flush(); return { entity }; }); } purgeEntity(req: ResolveEntityOptions): Promise<{ entity?: E }> { return this.em.transactional(async (em) => { const { entity } = await this.resolveEntity(req); if (!entity) { return {}; } writeEntityAuditLog({ entity, action: EntityAuditAction.Purge, em, before: entity.toPOJO() }); await em.remove(entity).flush(); return { entity }; }); } hasFeature = (code: MaybeArray) => { return Features.hasFeature(this.Entity, code); }; async createEntity(opts: CreateEntityOptions>) { const { repo, em } = this; let entity: E; const { data, upsert } = opts; (data as any).tid = getCurrentTenantId(); if (upsert) { const { fields: onConflictFields, exclude: onConflictExcludeFields = [], merge: onConflictMergeFields, action: onConflictAction, } = opts.onConflict || {}; try { entity = await this.em.upsert(this.Entity, data as any, { onConflictFields, onConflictMergeFields, onConflictExcludeFields: [...onConflictExcludeFields, 'id', 'uid', 'tid', 'createdAt', 'deletedAt'], onConflictAction, }); } catch (e) { this.log.error(`[${getCurrentTenantId()}] upsert ${this.Entity.name} ${JSON.stringify(opts)} ${e}`); throw e; } writeEntityAuditLog({ entity, action: EntityAuditAction.Upsert, before: data, em }); } else { entity = repo.create(data as any); await em.persist(entity).flush(); writeEntityAuditLog({ entity, action: EntityAuditAction.Create, after: entity.toPOJO() }); await em.flush(); } return { entity }; } async updateEntity(ent: ResolveEntityOptions, opts: UpdateEntityOptions): Promise> { return this.em.transactional(async (em) => { const { data } = opts; const { entity } = await this.requireEntity(ent, { lockMode: LockMode.PESSIMISTIC_WRITE }); const before = entity.toPOJO(); { const { attributes, properties, extensions, ...rest } = data as EntityData; entity.assign(trimUndefined(rest)); entity.attributes = setData(entity.attributes, { data: attributes, partial: true }); entity.properties = setData(entity.properties, { data: properties, partial: true }); entity.extensions = setData(entity.extensions, { data: extensions, partial: true }); } writeEntityAuditLog({ entity, action: EntityAuditAction.Update, em, before, after: entity.toPOJO() }); await em.persist(entity).flush(); return { entity }; }); } async patchEntity(ent: ResolveEntityOptions, opts: UpdateEntityOptions): Promise> { return this.em.transactional(async (em) => { const { data } = opts; const { entity } = await this.requireEntity(ent, { lockMode: LockMode.PESSIMISTIC_WRITE }); const before = entity.toPOJO(); { const { attributes, properties, extensions, metadata, ...rest } = data as EntityData; entity.assign(trimUndefined(rest)); entity.attributes = setData(entity.attributes, { data: attributes, partial: true }); entity.properties = setData(entity.properties, { data: properties, partial: true }); entity.extensions = setData(entity.extensions, { data: extensions, partial: true }); if (hasEntityFeature(entity, EntityFeature.HasMetadata)) { entity.metadata = setData(entity.metadata, { data: metadata, partial: true }); } } writeEntityAuditLog({ entity, action: EntityAuditAction.Patch, em, before, after: entity.toPOJO() }); await em.persist(entity).flush(); return { entity }; }); } async bindEntity(ent: ResolveEntityOptions, ref: BindEntityOptions) { const { entity } = await this.requireEntity(ent); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasEntityRef), '资源不支持绑定'); const { entityId, entityType, entity: entityRef } = await resolveEntityRef2(ref); entity.entityId = entityId; entity.entityType = entityType; // todo check entity type valid return { entity, ref: entityRef }; } async unbindEntity(req: ResolveEntityOptions) { const { entity } = await this.requireEntity(req); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasEntityRef), '资源不支持解绑'); const ent = entity as HasEntityRefEntity; ent.entityId = undefined; ent.entityType = undefined; return { entity }; } async setEntityStatus(ent: ResolveEntityOptions, opts: SetEntityStatusOptions) { const { entity } = await this.requireEntity(ent); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasStateStatus), '资源不支持状态'); entity.status = opts.status; entity.state = opts.state || entity.state; await this.em.persist(entity).flush(); return { entity }; } async setEntityNotes(ent: ResolveEntityOptions, opts: SetEntityNotesOptions) { let { entity } = await this.requireEntity(ent); Errors.BadRequest.check(hasEntityFeature(entity, EntityFeature.HasNotes), '资源不支持备注'); entity.notes = opts.notes; await this.em.persist(entity).flush(); return { entity }; } } function trimUndefined(o: any) { for (const k in o) { if (o[k] === undefined) { delete o[k]; } } return o; }