refactor(api): emit events logic

This commit is contained in:
yassinedorbozgithub 2024-10-04 17:42:10 +01:00
parent ac9c10c576
commit 90256a350b
11 changed files with 67 additions and 281 deletions

View File

@ -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<Attachment, never> {
constructor(
@InjectModel(Attachment.name) readonly model: Model<Attachment>,
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<void> {
this.eventEmitter.emit('hook:chatbot:attachment:upload', created);
super.setEventEmitter(eventEmitter);
}
}

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

@ -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<Conversation>,
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<void> {
this.eventEmitter.emit('hook:chatbot:conversation:start', created);
super.setEventEmitter(eventEmitter);
}
/**

View File

@ -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<Subscriber>,
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<void> {
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<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 +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<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

@ -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<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

@ -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<Translation> {
constructor(
@InjectModel(Translation.name) readonly model: Model<Translation>,
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<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.setEventEmitter(eventEmitter);
}
}

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,
@ -28,59 +28,10 @@ export class PermissionRepository extends BaseRepository<
> {
constructor(
@InjectModel(Permission.name) readonly model: Model<Permission>,
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<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.setEventEmitter(eventEmitter);
}
/**

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';
@ -32,60 +32,10 @@ export class RoleRepository extends BaseRepository<
@InjectModel(Role.name) readonly model: Model<Role>,
@InjectModel(Permission.name)
private readonly permissionModel: Model<Permission>,
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<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.setEventEmitter(eventEmitter);
}
/**

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,
@ -39,6 +49,8 @@ export abstract class BaseRepository<
private readonly leanOpts = { virtuals: true, defaults: true, getters: true };
private emitter: EventEmitter2;
constructor(
readonly model: Model<T>,
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<T>) {
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<DeleteResult, D, unknown, T, 'deleteMany'>;
await repository.postDelete(query, result);
});
@ -100,6 +128,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 +141,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,