Merge pull request #155 from Hexastack/150-issue-optimize-api-emitted-events

refactor(api): emit events logic
This commit is contained in:
Mohamed Marrouchi 2024-10-06 20:10:21 +01:00 committed by GitHub
commit fa491584ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 153 additions and 310 deletions

View File

@ -6,6 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule, getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Model } from 'mongoose';
@ -37,7 +38,7 @@ describe('BotStatsRepository', () => {
rootMongooseTestModule(installBotStatsFixtures),
MongooseModule.forFeature([BotStatsModel]),
],
providers: [LoggerService, BotStatsRepository],
providers: [LoggerService, BotStatsRepository, EventEmitter2],
}).compile();
botStatsRepository = module.get<BotStatsRepository>(BotStatsRepository);
botStatsModel = module.get<Model<BotStats>>(getModelToken('BotStats'));

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ -16,8 +17,11 @@ import { BotStats, BotStatsType } from '../schemas/bot-stats.schema';
@Injectable()
export class BotStatsRepository extends BaseRepository<BotStats> {
constructor(@InjectModel(BotStats.name) readonly model: Model<BotStats>) {
super(model, BotStats);
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(BotStats.name) readonly model: Model<BotStats>,
) {
super(eventEmitter, model, BotStats);
}
/**

View File

@ -13,23 +13,14 @@ import { Model } from 'mongoose';
import { BaseRepository } from '@/utils/generics/base-repository';
import { Attachment, AttachmentDocument } from '../schemas/attachment.schema';
import { Attachment } from '../schemas/attachment.schema';
@Injectable()
export class AttachmentRepository extends BaseRepository<Attachment, never> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Attachment.name) readonly model: Model<Attachment>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Attachment);
}
/**
* Handles post-creation operations for an attachment.
*
* @param created - The created attachment document.
*/
async postCreate(created: AttachmentDocument): Promise<void> {
this.eventEmitter.emit('hook:chatbot:attachment:upload', created);
super(eventEmitter, model, Attachment);
}
}

View File

@ -12,6 +12,7 @@ import { THydratedDocument } from 'mongoose';
import { FileType } from '@/chat/schemas/types/attachment';
import { config } from '@/config';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { buildURL } from '@/utils/helpers/URL';
import { MIME_REGEX } from '../utilities';
@ -115,10 +116,10 @@ export class Attachment extends BaseSchema {
export type AttachmentDocument = THydratedDocument<Attachment>;
export const AttachmentModel: ModelDefinition = {
export const AttachmentModel: ModelDefinition = LifecycleHookManager.attach({
name: Attachment.name,
schema: SchemaFactory.createForClass(Attachment),
};
});
AttachmentModel.schema.virtual('url').get(function () {
if (this._id && this.name)

View File

@ -7,6 +7,7 @@
*/
import { NotFoundException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
@ -44,7 +45,12 @@ describe('ContextVarController', () => {
rootMongooseTestModule(installContextVarFixtures),
MongooseModule.forFeature([ContextVarModel]),
],
providers: [LoggerService, ContextVarService, ContextVarRepository],
providers: [
LoggerService,
ContextVarService,
ContextVarRepository,
EventEmitter2,
],
}).compile();
contextVarController =
module.get<ContextVarController>(ContextVarController);

View File

@ -6,6 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule, getModelToken } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
import { Model } from 'mongoose';
@ -38,7 +39,7 @@ describe('BlockRepository', () => {
rootMongooseTestModule(installBlockFixtures),
MongooseModule.forFeature([BlockModel, CategoryModel, LabelModel]),
],
providers: [BlockRepository, CategoryRepository],
providers: [BlockRepository, CategoryRepository, EventEmitter2],
}).compile();
blockRepository = module.get<BlockRepository>(BlockRepository);
categoryRepository = module.get<CategoryRepository>(CategoryRepository);

View File

@ -7,6 +7,7 @@
*/
import { Injectable, Optional } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import {
Document,
@ -36,10 +37,11 @@ export class BlockRepository extends BaseRepository<
BlockFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Block.name) readonly model: Model<Block>,
@Optional() private readonly logger?: LoggerService,
) {
super(model, Block, BLOCK_POPULATE, BlockFull);
super(eventEmitter, model, Block, BLOCK_POPULATE, BlockFull);
}
/**

View File

@ -7,6 +7,7 @@
*/
import { ForbiddenException, Injectable, Optional } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
@ -20,10 +21,11 @@ export class CategoryRepository extends BaseRepository<Category> {
private readonly blockService: BlockService;
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Category.name) readonly model: Model<Category>,
@Optional() blockService?: BlockService,
) {
super(model, Category);
super(eventEmitter, model, Category);
this.blockService = blockService;
}

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ -16,7 +17,10 @@ import { ContextVar } from '../schemas/context-var.schema';
@Injectable()
export class ContextVarRepository extends BaseRepository<ContextVar> {
constructor(@InjectModel(ContextVar.name) readonly model: Model<ContextVar>) {
super(model, ContextVar);
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(ContextVar.name) readonly model: Model<ContextVar>,
) {
super(eventEmitter, model, ContextVar);
}
}

View File

@ -16,7 +16,6 @@ import { BaseRepository } from '@/utils/generics/base-repository';
import {
Conversation,
CONVERSATION_POPULATE,
ConversationDocument,
ConversationFull,
ConversationPopulate,
} from '../schemas/conversation.schema';
@ -28,20 +27,16 @@ export class ConversationRepository extends BaseRepository<
ConversationFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Conversation.name) readonly model: Model<Conversation>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Conversation, CONVERSATION_POPULATE, ConversationFull);
}
/**
* Called after a new conversation is created. This method emits the event
* with the newly created conversation document.
*
* @param created - The newly created conversation document.
*/
async postCreate(created: ConversationDocument): Promise<void> {
this.eventEmitter.emit('hook:chatbot:conversation:start', created);
super(
eventEmitter,
model,
Conversation,
CONVERSATION_POPULATE,
ConversationFull,
);
}
/**

View File

@ -28,10 +28,10 @@ export class LabelRepository extends BaseRepository<
LabelFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Label.name) readonly model: Model<Label>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Label, LABEL_POPULATE, LabelFull);
super(eventEmitter, model, Label, LABEL_POPULATE, LabelFull);
}
/**

View File

@ -7,6 +7,7 @@
*/
import { Injectable, Optional } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ -39,12 +40,14 @@ export class MessageRepository extends BaseRepository<
private readonly languageService: LanguageService;
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Message.name) readonly model: Model<AnyMessage>,
@Optional() nlpSampleService?: NlpSampleService,
@Optional() logger?: LoggerService,
@Optional() languageService?: LanguageService,
) {
super(
eventEmitter,
model,
Message as new () => AnyMessage,
MESSAGE_POPULATE,

View File

@ -36,10 +36,10 @@ export class SubscriberRepository extends BaseRepository<
SubscriberFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Subscriber.name) readonly model: Model<Subscriber>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Subscriber, SUBSCRIBER_POPULATE, SubscriberFull);
super(eventEmitter, model, Subscriber, SUBSCRIBER_POPULATE, SubscriberFull);
}
/**
@ -54,7 +54,6 @@ export class SubscriberRepository extends BaseRepository<
'New users',
created,
);
this.eventEmitter.emit('hook:subscriber:create', created);
}
/**
@ -80,12 +79,6 @@ export class SubscriberRepository extends BaseRepository<
): Promise<void> {
const subscriberUpdates: SubscriberUpdateDto = updates?.['$set'];
this.eventEmitter.emit(
'hook:subscriber:update:before',
criteria,
subscriberUpdates,
);
const oldSubscriber = await this.findOne(criteria);
if (subscriberUpdates.assignedTo !== oldSubscriber?.assignedTo) {
@ -105,26 +98,6 @@ export class SubscriberRepository extends BaseRepository<
}
}
/**
* Emits an event after successfully updating a subscriber.
* Triggers the event with the updated subscriber data.
*
* @param _query - The Mongoose query object for finding and updating a subscriber.
* @param updated - The updated subscriber entity.
*/
async postUpdate(
_query: Query<
Document<Subscriber, any, any>,
Document<Subscriber, any, any>,
unknown,
Subscriber,
'findOneAndUpdate'
>,
updated: Subscriber,
) {
this.eventEmitter.emit('hook:subscriber:update:after', updated);
}
/**
* Constructs a query to find a subscriber by their foreign ID.
*

View File

@ -287,7 +287,7 @@ export class ChatService {
*
* @param subscriber - The end user (subscriber)
*/
@OnEvent('hook:subscriber:create')
@OnEvent('hook:subscriber:postCreate')
onSubscriberCreate(subscriber: Subscriber) {
this.websocketGateway.broadcastSubscriberNew(subscriber);
}
@ -297,7 +297,7 @@ export class ChatService {
*
* @param subscriber - The end user (subscriber)
*/
@OnEvent('hook:subscriber:update:after')
@OnEvent('hook:subscriber:postUpdate')
onSubscriberUpdate(subscriber: Subscriber) {
this.websocketGateway.broadcastSubscriberUpdate(subscriber);
}

View File

@ -11,7 +11,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { LoggerModule } from '@/logger/logger.module';
import { LoggerService } from '@/logger/logger.service';
import {
installMenuFixtures,
@ -39,7 +38,6 @@ describe('MenuController', () => {
imports: [
rootMongooseTestModule(installMenuFixtures),
MongooseModule.forFeature([MenuModel]),
LoggerModule,
],
providers: [
MenuRepository,

View File

@ -6,6 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule, getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Model } from 'mongoose';
@ -52,6 +53,7 @@ describe('ContentTypeRepository', () => {
findOne: jest.fn(),
},
},
EventEmitter2,
],
}).compile();
blockService = module.get<BlockService>(BlockService);

View File

@ -7,6 +7,7 @@
*/
import { ForbiddenException, Injectable, Optional } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
@ -19,11 +20,12 @@ import { Content } from '../schemas/content.schema';
@Injectable()
export class ContentTypeRepository extends BaseRepository<ContentType> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(ContentType.name) readonly model: Model<ContentType>,
@InjectModel(Content.name) private readonly contentModel: Model<Content>,
@Optional() private readonly blockService?: BlockService,
) {
super(model, ContentType);
super(eventEmitter, model, ContentType);
}
/**

View File

@ -6,6 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule, getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Model } from 'mongoose';
@ -40,7 +41,7 @@ describe('ContentRepository', () => {
rootMongooseTestModule(installContentFixtures),
MongooseModule.forFeature([ContentTypeModel, ContentModel]),
],
providers: [LoggerService, ContentRepository],
providers: [LoggerService, ContentRepository, EventEmitter2],
}).compile();
contentRepository = module.get<ContentRepository>(ContentRepository);
contentModel = module.get<Model<Content>>(getModelToken('Content'));

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import {
Document,
@ -33,8 +34,11 @@ export class ContentRepository extends BaseRepository<
ContentPopulate,
ContentFull
> {
constructor(@InjectModel(Content.name) readonly model: Model<Content>) {
super(model, Content, CONTENT_POPULATE, ContentFull);
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Content.name) readonly model: Model<Content>,
) {
super(eventEmitter, model, Content, CONTENT_POPULATE, ContentFull);
}
/**

View File

@ -10,7 +10,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { LoggerModule } from '@/logger/logger.module';
import {
installMenuFixtures,
rootMenuFixtures,
@ -31,7 +30,6 @@ describe('MenuRepository', () => {
imports: [
rootMongooseTestModule(installMenuFixtures),
MongooseModule.forFeature([MenuModel]),
LoggerModule,
],
providers: [MenuRepository, EventEmitter2],
}).compile();

View File

@ -9,9 +9,9 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query } from 'mongoose';
import { Model } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { BaseRepository } from '@/utils/generics/base-repository';
import {
Menu,
@ -29,10 +29,10 @@ export class MenuRepository extends BaseRepository<
MenuFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Menu.name) readonly model: Model<Menu>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Menu, MENU_POPULATE, MenuFull);
super(eventEmitter, model, Menu, MENU_POPULATE, MenuFull);
}
/**
@ -68,52 +68,4 @@ export class MenuRepository extends BaseRepository<
}
}
}
/**
* Post-create hook that triggers the event after a `Menu` document has been successfully created.
*
* @param created - The newly created `MenuDocument`.
*/
async postCreate(created: MenuDocument): Promise<void> {
this.eventEmitter.emit('hook:menu:create', created);
}
/**
* @description
* Post-update hook that triggers the event after a `Menu` document has been successfully updated.
*
* @param query - The query used to update the `Menu` document.
* @param updated - The updated `Menu` document.
*/
async postUpdate(
_query: Query<
Document<Menu, any, any>,
Document<Menu, any, any>,
unknown,
Menu,
'findOneAndUpdate'
>,
updated: Menu,
): Promise<void> {
this.eventEmitter.emit('hook:menu:update', updated);
}
/**
* Post-delete hook that triggers the event after a `Menu` document has been successfully deleted.
*
* @param query - The query used to delete the `Menu` document.
* @param result - The result of the deletion.
*/
async postDelete(
_query: Query<
DeleteResult,
Document<Menu, any, any>,
unknown,
Menu,
'deleteOne' | 'deleteMany'
>,
result: DeleteResult,
): Promise<void> {
this.eventEmitter.emit('hook:menu:delete', result);
}
}

View File

@ -18,10 +18,10 @@ import { Language } from '../schemas/language.schema';
@Injectable()
export class LanguageRepository extends BaseRepository<Language> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Language.name) readonly model: Model<Language>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Language);
super(eventEmitter, model, Language);
}
/**

View File

@ -9,68 +9,18 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, Types } from 'mongoose';
import { Model } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { BaseRepository } from '@/utils/generics/base-repository';
import { Translation } from '../../i18n/schemas/translation.schema';
@Injectable()
export class TranslationRepository extends BaseRepository<Translation> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Translation.name) readonly model: Model<Translation>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Translation);
}
/**
* Emits an event after a translation document is updated.
*
* @param query - The query object representing the update operation.
* @param updated - The updated translation document.
*/
async postUpdate(
_query: Query<
Document<Translation, any, any>,
Document<Translation, any, any>,
unknown,
Translation,
'findOneAndUpdate'
>,
_updated: Translation,
) {
this.eventEmitter.emit('hook:translation:update');
}
/**
* Emits an event after a new translation document is created.
*
* @param created - The newly created translation document.
*/
async postCreate(
_created: Document<unknown, unknown, Translation> &
Translation & { _id: Types.ObjectId },
) {
this.eventEmitter.emit('hook:translation:create');
}
/**
* Emits an event after a translation document is deleted.
*
* @param query - The query object representing the delete operation.
* @param result - The result of the delete operation.
*/
async postDelete(
_query: Query<
DeleteResult,
Document<Translation, any, any>,
unknown,
Translation,
'deleteOne' | 'deleteMany'
>,
_result: DeleteResult,
) {
this.eventEmitter.emit('hook:translation:delete');
super(eventEmitter, model, Translation);
}
}

View File

@ -29,12 +29,12 @@ export class NlpEntityRepository extends BaseRepository<
NlpEntityFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(NlpEntity.name) readonly model: Model<NlpEntity>,
private readonly nlpValueRepository: NlpValueRepository,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
private readonly eventEmitter: EventEmitter2,
) {
super(model, NlpEntity, NLP_ENTITY_POPULATE, NlpEntityFull);
super(eventEmitter, model, NlpEntity, NLP_ENTITY_POPULATE, NlpEntityFull);
}
/**

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ -26,9 +27,11 @@ export class NlpSampleEntityRepository extends BaseRepository<
NlpSampleEntityFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(NlpSampleEntity.name) readonly model: Model<NlpSampleEntity>,
) {
super(
eventEmitter,
model,
NlpSampleEntity,
NLP_SAMPLE_ENTITY_POPULATE,

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery } from 'mongoose';
@ -27,10 +28,11 @@ export class NlpSampleRepository extends BaseRepository<
NlpSampleFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(NlpSample.name) readonly model: Model<NlpSample>,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
) {
super(model, NlpSample, NLP_SAMPLE_POPULATE, NlpSampleFull);
super(eventEmitter, model, NlpSample, NLP_SAMPLE_POPULATE, NlpSampleFull);
}
/**

View File

@ -29,11 +29,11 @@ export class NlpValueRepository extends BaseRepository<
NlpValueFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(NlpValue.name) readonly model: Model<NlpValue>,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
private readonly eventEmitter: EventEmitter2,
) {
super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull);
super(eventEmitter, model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull);
}
/**

View File

@ -19,11 +19,11 @@ import { Setting } from '../schemas/setting.schema';
@Injectable()
export class SettingRepository extends BaseRepository<Setting> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Setting.name) readonly model: Model<Setting>,
private readonly eventEmitter: EventEmitter2,
private readonly i18n: I18nService,
) {
super(model, Setting);
super(eventEmitter, model, Setting);
}
/**

View File

@ -6,6 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model, TFilterQuery } from 'mongoose';
@ -25,8 +26,11 @@ export class InvitationRepository extends BaseRepository<
InvitationPopulate,
InvitationFull
> {
constructor(@InjectModel(Invitation.name) readonly model: Model<Invitation>) {
super(model, Invitation, INVITATION_POPULATE, InvitationFull);
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Invitation.name) readonly model: Model<Invitation>,
) {
super(eventEmitter, model, Invitation, INVITATION_POPULATE, InvitationFull);
}
/**

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model as MongooseModel } from 'mongoose';
@ -27,11 +28,12 @@ export class ModelRepository extends BaseRepository<
ModelFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Model.name) readonly model: MongooseModel<Model>,
@InjectModel(Permission.name)
private readonly permissionModel: MongooseModel<Permission>,
) {
super(model, Model, MODEL_POPULATE, ModelFull);
super(eventEmitter, model, Model, MODEL_POPULATE, ModelFull);
}
/**

View File

@ -9,9 +9,9 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery, Types } from 'mongoose';
import { Model, TFilterQuery } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { BaseRepository } from '@/utils/generics/base-repository';
import {
Permission,
@ -27,60 +27,10 @@ export class PermissionRepository extends BaseRepository<
PermissionFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Permission.name) readonly model: Model<Permission>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Permission, PERMISSION_POPULATE, PermissionFull);
}
/**
* Emits an event after a permission is created.
*
* @param created - The created permission document.
*/
async postCreate(
created: Document<unknown, unknown, Permission> &
Permission & { _id: Types.ObjectId },
): Promise<void> {
this.eventEmitter.emit('hook:access:permission:create', created);
}
/**
* Emits an event after a permission is updated.
*
* @param _query - The query used for updating the permission.
* @param updated - The updated permission entity.
*/
async postUpdate(
_query: Query<
Document<Permission, any, any>,
Document<Permission, any, any>,
unknown,
Permission,
'findOneAndUpdate'
>,
updated: Permission,
): Promise<void> {
this.eventEmitter.emit('hook:access:permission:update', updated);
}
/**
* Emits an event after a permission is deleted.
*
* @param _query - The query used for deleting the permission.
* @param result - The result of the delete operation.
*/
async postDelete(
_query: Query<
DeleteResult,
Document<Permission, any, any>,
unknown,
Permission,
'deleteOne' | 'deleteMany'
>,
result: DeleteResult,
): Promise<void> {
this.eventEmitter.emit('hook:access:permission:delete', result);
super(eventEmitter, model, Permission, PERMISSION_POPULATE, PermissionFull);
}
/**

View File

@ -9,9 +9,9 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, TFilterQuery, Types } from 'mongoose';
import { Model, TFilterQuery } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { Permission } from '../schemas/permission.schema';
@ -29,63 +29,12 @@ export class RoleRepository extends BaseRepository<
RoleFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Role.name) readonly model: Model<Role>,
@InjectModel(Permission.name)
private readonly permissionModel: Model<Permission>,
private readonly eventEmitter: EventEmitter2,
) {
super(model, Role, ROLE_POPULATE, RoleFull);
}
/**
* Emits a hook event after a role is successfully created.
*
* @param created The created Role document.
*/
async preCreate(
created: Document<unknown, object, Role> & Role & { _id: Types.ObjectId },
): Promise<void> {
if (created) {
this.eventEmitter.emit('hook:access:role:create', created);
}
}
/**
* Emits a hook event after a role is successfully updated.
*
* @param _query The query used to update the Role.
* @param updated The updated Role entity.
*/
async postUpdate(
_query: Query<
Document<Role, any, any>,
Document<Role, any, any>,
unknown,
Role,
'findOneAndUpdate'
>,
updated: Role,
): Promise<void> {
this.eventEmitter.emit('hook:access:role:update', updated);
}
/**
* Emits a hook event after a role is successfully deleted.
*
* @param _query The query used to delete the Role.
* @param result The result of the deletion operation.
*/
async postDelete(
_query: Query<
DeleteResult,
Document<Role, any, any>,
unknown,
Role,
'deleteOne' | 'deleteMany'
>,
result: DeleteResult,
): Promise<void> {
this.eventEmitter.emit('hook:access:role:delete', result);
super(eventEmitter, model, Role, ROLE_POPULATE, RoleFull);
}
/**

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import {
Document,
@ -35,8 +36,11 @@ export class UserRepository extends BaseRepository<
UserPopulate,
UserFull
> {
constructor(@InjectModel(User.name) readonly model: Model<User>) {
super(model, User, USER_POPULATE, UserFull);
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(User.name) readonly model: Model<User>,
) {
super(eventEmitter, model, User, USER_POPULATE, UserFull);
}
/**

View File

@ -42,7 +42,8 @@ export class PermissionService extends BaseService<
* This method listens to events matching the pattern 'hook:access:*:*' and clears
* the permissions cache to ensure fresh data is used for subsequent requests.
*/
@OnEvent('hook:access:*:*')
@OnEvent('hook:role:*')
@OnEvent('hook:permission:*')
async handlePermissionUpdateEvent() {
await this.cacheManager.del(PERMISSION_CACHE_KEY);
}

View File

@ -6,6 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ClassTransformOptions, plainToClass } from 'class-transformer';
import {
Document,
@ -28,6 +29,15 @@ export type DeleteResult = {
deletedCount: number;
};
export enum EHook {
preCreate = 'preCreate',
preUpdate = 'preUpdate',
preDelete = 'preDelete',
postCreate = 'postCreate',
postUpdate = 'postUpdate',
postDelete = 'postDelete',
}
export abstract class BaseRepository<
T extends FlattenMaps<unknown>,
P extends string = never,
@ -40,6 +50,7 @@ export abstract class BaseRepository<
private readonly leanOpts = { virtuals: true, defaults: true, getters: true };
constructor(
private readonly emitter: EventEmitter2,
readonly model: Model<T>,
private readonly cls: new () => T,
protected readonly populate: P[] = [],
@ -52,6 +63,10 @@ export abstract class BaseRepository<
return this.populate;
}
getEventName(suffix: EHook) {
return `hook:${this.cls.name.toLocaleLowerCase()}:${suffix.toLocaleLowerCase()}`;
}
private registerLifeCycleHooks() {
const repository = this;
const hooks = LifecycleHookManager.getHooks(this.cls.name);
@ -71,6 +86,10 @@ export abstract class BaseRepository<
});
hooks?.save.post.execute(async function (created: HydratedDocument<T>) {
repository.emitter.emit(
repository.getEventName(EHook.postCreate),
created,
);
await repository.postCreate(created);
});
@ -92,6 +111,10 @@ export abstract class BaseRepository<
});
hooks?.deleteMany.post.execute(async function (result: DeleteResult) {
repository.emitter.emit(
repository.getEventName(EHook.postDelete),
result,
);
const query = this as Query<DeleteResult, D, unknown, T, 'deleteMany'>;
await repository.postDelete(query, result);
});
@ -100,6 +123,12 @@ export abstract class BaseRepository<
const query = this as Query<D, D, unknown, T, 'findOneAndUpdate'>;
const criteria = query.getFilter();
const updates = query.getUpdate();
repository.emitter.emit(
repository.getEventName(EHook.preUpdate),
criteria,
updates?.['$set'],
);
await repository.preUpdate(query, criteria, updates);
});
@ -107,6 +136,10 @@ export abstract class BaseRepository<
updated: HydratedDocument<T>,
) {
if (updated) {
repository.emitter.emit(
repository.getEventName(EHook.postUpdate),
updated,
);
const query = this as Query<D, D, unknown, T, 'findOneAndUpdate'>;
await repository.postUpdate(
query,

View File

@ -7,6 +7,7 @@
*/
import { Module } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { installDummyFixtures } from '@/utils/test/fixtures/dummy';
@ -21,6 +22,6 @@ import { DummyService } from './services/dummy.service';
rootMongooseTestModule(installDummyFixtures),
MongooseModule.forFeature([DummyModel]),
],
providers: [DummyRepository, DummyService],
providers: [DummyRepository, DummyService, EventEmitter2],
})
export class DummyModule {}

View File

@ -7,6 +7,7 @@
*/
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ -16,7 +17,10 @@ import { Dummy } from '../schemas/dummy.schema';
@Injectable()
export class DummyRepository extends BaseRepository<Dummy> {
constructor(@InjectModel(Dummy.name) readonly model: Model<Dummy>) {
super(model, Dummy);
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Dummy.name) readonly model: Model<Dummy>,
) {
super(eventEmitter, model, Dummy);
}
}