diff --git a/api/src/attachment/repositories/attachment.repository.ts b/api/src/attachment/repositories/attachment.repository.ts index a52f6a32..ccb7a92f 100644 --- a/api/src/attachment/repositories/attachment.repository.ts +++ b/api/src/attachment/repositories/attachment.repository.ts @@ -13,23 +13,15 @@ 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 { constructor( @InjectModel(Attachment.name) readonly model: Model, - private readonly eventEmitter: EventEmitter2, + readonly eventEmitter: EventEmitter2, ) { super(model, Attachment); - } - - /** - * Handles post-creation operations for an attachment. - * - * @param created - The created attachment document. - */ - async postCreate(created: AttachmentDocument): Promise { - this.eventEmitter.emit('hook:chatbot:attachment:upload', created); + super.setEventEmitter(eventEmitter); } } diff --git a/api/src/attachment/schemas/attachment.schema.ts b/api/src/attachment/schemas/attachment.schema.ts index 8929fe93..5a201f21 100644 --- a/api/src/attachment/schemas/attachment.schema.ts +++ b/api/src/attachment/schemas/attachment.schema.ts @@ -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; -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) diff --git a/api/src/chat/repositories/conversation.repository.ts b/api/src/chat/repositories/conversation.repository.ts index 197c7c25..aea3117a 100644 --- a/api/src/chat/repositories/conversation.repository.ts +++ b/api/src/chat/repositories/conversation.repository.ts @@ -16,7 +16,6 @@ import { BaseRepository } from '@/utils/generics/base-repository'; import { Conversation, CONVERSATION_POPULATE, - ConversationDocument, ConversationFull, ConversationPopulate, } from '../schemas/conversation.schema'; @@ -29,19 +28,10 @@ export class ConversationRepository extends BaseRepository< > { constructor( @InjectModel(Conversation.name) readonly model: Model, - private readonly eventEmitter: EventEmitter2, + 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 { - this.eventEmitter.emit('hook:chatbot:conversation:start', created); + super.setEventEmitter(eventEmitter); } /** diff --git a/api/src/chat/repositories/subscriber.repository.ts b/api/src/chat/repositories/subscriber.repository.ts index ce9160ad..2222ec04 100644 --- a/api/src/chat/repositories/subscriber.repository.ts +++ b/api/src/chat/repositories/subscriber.repository.ts @@ -24,7 +24,6 @@ import { SubscriberUpdateDto } from '../dto/subscriber.dto'; import { Subscriber, SUBSCRIBER_POPULATE, - SubscriberDocument, SubscriberFull, SubscriberPopulate, } from '../schemas/subscriber.schema'; @@ -37,24 +36,10 @@ export class SubscriberRepository extends BaseRepository< > { constructor( @InjectModel(Subscriber.name) readonly model: Model, - private readonly eventEmitter: EventEmitter2, + readonly eventEmitter: EventEmitter2, ) { super(model, Subscriber, SUBSCRIBER_POPULATE, SubscriberFull); - } - - /** - * Emits events related to the creation of a new subscriber. - * - * @param created - The newly created subscriber document. - */ - async postCreate(created: SubscriberDocument): Promise { - this.eventEmitter.emit( - 'hook:stats:entry', - 'new_users', - 'New users', - created, - ); - this.eventEmitter.emit('hook:subscriber:create', created); + super.setEventEmitter(eventEmitter); } /** @@ -80,12 +65,6 @@ export class SubscriberRepository extends BaseRepository< ): Promise { 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 +84,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, - Document, - unknown, - Subscriber, - 'findOneAndUpdate' - >, - updated: Subscriber, - ) { - this.eventEmitter.emit('hook:subscriber:update:after', updated); - } - /** * Constructs a query to find a subscriber by their foreign ID. * diff --git a/api/src/chat/services/chat.service.ts b/api/src/chat/services/chat.service.ts index e13f49d3..48eaec9d 100644 --- a/api/src/chat/services/chat.service.ts +++ b/api/src/chat/services/chat.service.ts @@ -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); } diff --git a/api/src/cms/repositories/menu.repository.ts b/api/src/cms/repositories/menu.repository.ts index a85d25f4..bc9ad11e 100644 --- a/api/src/cms/repositories/menu.repository.ts +++ b/api/src/cms/repositories/menu.repository.ts @@ -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, @@ -33,6 +33,7 @@ export class MenuRepository extends BaseRepository< private readonly eventEmitter: EventEmitter2, ) { super(model, Menu, MENU_POPULATE, MenuFull); + super.setEventEmitter(eventEmitter); } /** @@ -68,52 +69,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 { - 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, - Document, - unknown, - Menu, - 'findOneAndUpdate' - >, - updated: Menu, - ): Promise { - 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, - unknown, - Menu, - 'deleteOne' | 'deleteMany' - >, - result: DeleteResult, - ): Promise { - this.eventEmitter.emit('hook:menu:delete', result); - } } diff --git a/api/src/i18n/repositories/translation.repository.ts b/api/src/i18n/repositories/translation.repository.ts index 4581ee90..beaa73ea 100644 --- a/api/src/i18n/repositories/translation.repository.ts +++ b/api/src/i18n/repositories/translation.repository.ts @@ -9,9 +9,9 @@ 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'; @@ -19,58 +19,9 @@ import { Translation } from '../../i18n/schemas/translation.schema'; export class TranslationRepository extends BaseRepository { constructor( @InjectModel(Translation.name) readonly model: Model, - private readonly eventEmitter: EventEmitter2, + 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, - Document, - 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 & - 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, - unknown, - Translation, - 'deleteOne' | 'deleteMany' - >, - _result: DeleteResult, - ) { - this.eventEmitter.emit('hook:translation:delete'); + super.setEventEmitter(eventEmitter); } } diff --git a/api/src/user/repositories/permission.repository.ts b/api/src/user/repositories/permission.repository.ts index d5b44a13..ef761665 100644 --- a/api/src/user/repositories/permission.repository.ts +++ b/api/src/user/repositories/permission.repository.ts @@ -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, @@ -28,59 +28,10 @@ export class PermissionRepository extends BaseRepository< > { constructor( @InjectModel(Permission.name) readonly model: Model, - private readonly eventEmitter: EventEmitter2, + 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 & - Permission & { _id: Types.ObjectId }, - ): Promise { - 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, - Document, - unknown, - Permission, - 'findOneAndUpdate' - >, - updated: Permission, - ): Promise { - 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, - unknown, - Permission, - 'deleteOne' | 'deleteMany' - >, - result: DeleteResult, - ): Promise { - this.eventEmitter.emit('hook:access:permission:delete', result); + super.setEventEmitter(eventEmitter); } /** diff --git a/api/src/user/repositories/role.repository.ts b/api/src/user/repositories/role.repository.ts index 8549d9cc..8e9f92b1 100644 --- a/api/src/user/repositories/role.repository.ts +++ b/api/src/user/repositories/role.repository.ts @@ -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'; @@ -32,60 +32,10 @@ export class RoleRepository extends BaseRepository< @InjectModel(Role.name) readonly model: Model, @InjectModel(Permission.name) private readonly permissionModel: Model, - private readonly eventEmitter: EventEmitter2, + 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 & Role & { _id: Types.ObjectId }, - ): Promise { - 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, - Document, - unknown, - Role, - 'findOneAndUpdate' - >, - updated: Role, - ): Promise { - 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, - unknown, - Role, - 'deleteOne' | 'deleteMany' - >, - result: DeleteResult, - ): Promise { - this.eventEmitter.emit('hook:access:role:delete', result); + super.setEventEmitter(eventEmitter); } /** diff --git a/api/src/user/services/permission.service.ts b/api/src/user/services/permission.service.ts index 18e0b4d5..244c203f 100644 --- a/api/src/user/services/permission.service.ts +++ b/api/src/user/services/permission.service.ts @@ -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); } diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index c67cf973..0ff42803 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -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, P extends string = never, @@ -39,6 +49,8 @@ export abstract class BaseRepository< private readonly leanOpts = { virtuals: true, defaults: true, getters: true }; + private emitter: EventEmitter2; + constructor( readonly model: Model, private readonly cls: new () => T, @@ -52,6 +64,14 @@ export abstract class BaseRepository< return this.populate; } + getEventName(suffix: EHook) { + return `hook:${this.cls.name.toLocaleLowerCase()}:${suffix.toLocaleLowerCase()}`; + } + + setEventEmitter(eventEmitter: EventEmitter2) { + this.emitter = eventEmitter; + } + private registerLifeCycleHooks() { const repository = this; const hooks = LifecycleHookManager.getHooks(this.cls.name); @@ -71,6 +91,10 @@ export abstract class BaseRepository< }); hooks?.save.post.execute(async function (created: HydratedDocument) { + repository.emitter?.emit( + repository.getEventName(EHook.postCreate), + created, + ); await repository.postCreate(created); }); @@ -92,6 +116,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; await repository.postDelete(query, result); }); @@ -100,6 +128,12 @@ export abstract class BaseRepository< const query = this as Query; 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 +141,10 @@ export abstract class BaseRepository< updated: HydratedDocument, ) { if (updated) { + repository.emitter?.emit( + repository.getEventName(EHook.postUpdate), + updated, + ); const query = this as Query; await repository.postUpdate( query,