fix: notification service
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
export abstract class BaseNotificationHandler<TData = unknown> {
|
||||
protected abstract readonly logger: Logger;
|
||||
|
||||
/**
|
||||
* Process the notification with automatic transaction management
|
||||
*/
|
||||
async processWithTransaction(recipientId: string, data: TData, em: EntityManager): Promise<void> {
|
||||
try {
|
||||
await this.handle(recipientId, data, em);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process ${this.getHandlerName()} notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the specific notification logic - to be implemented by each handler
|
||||
*/
|
||||
protected abstract handle(recipientId: string, data: TData, em: EntityManager): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the handler name for logging purposes
|
||||
*/
|
||||
protected abstract getHandlerName(): string;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { BaseNotificationHandler } from "./base-notification.handler";
|
||||
import { IChangePasswordNotificationData } from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../services/notifications.service";
|
||||
|
||||
@Injectable()
|
||||
export class ChangePasswordHandler extends BaseNotificationHandler<IChangePasswordNotificationData> {
|
||||
protected readonly logger = new Logger(ChangePasswordHandler.name);
|
||||
|
||||
constructor(private readonly notificationsService: NotificationsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async handle(recipientId: string, data: IChangePasswordNotificationData, em: EntityManager): Promise<void> {
|
||||
await this.notificationsService.changePasswordNotification(recipientId, data, em);
|
||||
}
|
||||
|
||||
protected getHandlerName(): string {
|
||||
return "ChangePassword";
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { BaseNotificationHandler } from "./base-notification.handler";
|
||||
import { INewEmailNotificationData } from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../services/notifications.service";
|
||||
|
||||
@Injectable()
|
||||
export class NewEmailHandler extends BaseNotificationHandler<INewEmailNotificationData> {
|
||||
protected readonly logger = new Logger(NewEmailHandler.name);
|
||||
|
||||
constructor(private readonly notificationsService: NotificationsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async handle(recipientId: string, data: INewEmailNotificationData, em: EntityManager): Promise<void> {
|
||||
try {
|
||||
await this.notificationsService.createNewEmailNotification(recipientId, data, em);
|
||||
this.logger.log(`New email notifications (in-app + push) processed for user: ${recipientId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing new email notifications for user ${recipientId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected getHandlerName(): string {
|
||||
return "NewEmail";
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { BaseNotificationHandler } from "./base-notification.handler";
|
||||
import { ChangePasswordHandler } from "./change-password.handler";
|
||||
import { NewEmailHandler } from "./new-email.handler";
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationHandlerFactory {
|
||||
private readonly logger = new Logger(NotificationHandlerFactory.name);
|
||||
private readonly handlers: Map<NotifType, BaseNotificationHandler> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly newEmailHandler: NewEmailHandler,
|
||||
private readonly changePasswordHandler: ChangePasswordHandler,
|
||||
) {
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// User notifications
|
||||
this.handlers.set(NotifType.NEW_EMAIL, this.newEmailHandler);
|
||||
this.handlers.set(NotifType.CHANGE_PASSWORD, this.changePasswordHandler);
|
||||
|
||||
this.logger.log(`Registered ${this.handlers.size} notification handlers`);
|
||||
}
|
||||
|
||||
async processNotification(type: NotifType, recipientId: string, data: unknown, em: EntityManager): Promise<void> {
|
||||
const handler = this.handlers.get(type);
|
||||
|
||||
if (!handler) {
|
||||
this.logger.warn(`No handler found for notification type: ${type}`);
|
||||
throw new Error(`No handler registered for notification type: ${type}`);
|
||||
}
|
||||
|
||||
await handler.processWithTransaction(recipientId, data, em);
|
||||
}
|
||||
|
||||
getRegisteredTypes(): NotifType[] {
|
||||
return Array.from(this.handlers.keys());
|
||||
}
|
||||
|
||||
isTypeSupported(type: NotifType): boolean {
|
||||
return this.handlers.has(type);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { IBaseNotificationData } from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../services/notifications.service";
|
||||
|
||||
@Injectable()
|
||||
export class UserLoginHandler {
|
||||
private readonly logger = new Logger(UserLoginHandler.name);
|
||||
|
||||
constructor(private readonly notificationsService: NotificationsService) {}
|
||||
|
||||
async processUserLogin(recipientId: string, data: IBaseNotificationData): Promise<void> {
|
||||
try {
|
||||
await this.notificationsService.createLoginNotification(recipientId, data);
|
||||
this.logger.log(`User login notification sent successfully to ${recipientId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to send user login notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,6 @@ import { Module } from "@nestjs/common";
|
||||
|
||||
import { NOTIFICATION } from "./constants";
|
||||
import { Notification } from "./entities/notification.entity";
|
||||
import { ChangePasswordHandler } from "./handlers/change-password.handler";
|
||||
import { NewEmailHandler } from "./handlers/new-email.handler";
|
||||
import { NotificationHandlerFactory } from "./handlers/notification-handler.factory";
|
||||
import { UserLoginHandler } from "./handlers/user-login.handler";
|
||||
import { NotificationController } from "./notifications.controller";
|
||||
import { NotificationProcessor } from "./queue/notification.processor";
|
||||
import { NotificationQueue } from "./queue/notification.queue";
|
||||
@@ -24,33 +20,11 @@ import { UtilsModule } from "../utils/utils.module";
|
||||
imports: [
|
||||
HttpModule,
|
||||
MikroOrmModule.forFeature([Notification, NotificationSetting, User]),
|
||||
BullModule.registerQueue({
|
||||
name: NOTIFICATION.QUEUE_NAME,
|
||||
defaultJobOptions: {
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
},
|
||||
}),
|
||||
BullModule.registerQueue({ name: NOTIFICATION.QUEUE_NAME }),
|
||||
UtilsModule,
|
||||
SettingModule,
|
||||
],
|
||||
providers: [
|
||||
NotificationsService,
|
||||
NajvaPushService,
|
||||
NotificationQueue,
|
||||
NotificationProcessor,
|
||||
NotificationHandlerFactory,
|
||||
UserLoginHandler,
|
||||
NewEmailHandler,
|
||||
ChangePasswordHandler,
|
||||
najvaConfigProvider,
|
||||
],
|
||||
providers: [NotificationsService, NajvaPushService, NotificationQueue, NotificationProcessor, najvaConfigProvider],
|
||||
controllers: [NotificationController],
|
||||
exports: [NotificationsService, NotificationQueue, NajvaPushService],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Processor } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
@@ -6,10 +5,9 @@ import { Job } from "bullmq";
|
||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotificationHandlerFactory } from "../handlers/notification-handler.factory";
|
||||
import { UserLoginHandler } from "../handlers/user-login.handler";
|
||||
import { NotificationJobData, OtpJobData } from "../interfaces/INotification-job-data";
|
||||
import { IBaseNotificationData } from "../interfaces/ISendNotificationData";
|
||||
import { IBaseNotificationData, IChangePasswordNotificationData, INewEmailNotificationData } from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../services/notifications.service";
|
||||
|
||||
@Processor(NOTIFICATION.QUEUE_NAME, {
|
||||
concurrency: NOTIFICATION.SEND_NOTIFICATION_JOB_CONCURRENCY,
|
||||
@@ -20,11 +18,7 @@ import { IBaseNotificationData } from "../interfaces/ISendNotificationData";
|
||||
export class NotificationProcessor extends WorkerProcessor {
|
||||
protected readonly logger = new Logger(NotificationProcessor.name);
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly notificationHandlerFactory: NotificationHandlerFactory,
|
||||
private readonly userLoginHandler: UserLoginHandler,
|
||||
) {
|
||||
constructor(private readonly notificationsService: NotificationsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -34,12 +28,7 @@ export class NotificationProcessor extends WorkerProcessor {
|
||||
try {
|
||||
if (job.name === NOTIFICATION.SEND_NOTIFICATION_JOB_NAME) {
|
||||
const { type, recipientId, data } = job.data as NotificationJobData;
|
||||
|
||||
if (type === NotifType.USER_LOGIN) {
|
||||
await this.userLoginHandler.processUserLogin(recipientId, data as IBaseNotificationData);
|
||||
} else {
|
||||
await this.processWithTransaction(type, recipientId, data);
|
||||
}
|
||||
await this.processNotification(type, recipientId, data);
|
||||
} else {
|
||||
this.logger.warn(`Unknown job name: ${job.name}`);
|
||||
}
|
||||
@@ -54,17 +43,32 @@ export class NotificationProcessor extends WorkerProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private async processWithTransaction(type: NotifType, recipientId: string, data: IBaseNotificationData): Promise<void> {
|
||||
const em = this.em.fork();
|
||||
//*********************************** */
|
||||
|
||||
private async processNotification(type: NotifType, recipientId: string, data: IBaseNotificationData): Promise<void> {
|
||||
try {
|
||||
await em.begin();
|
||||
this.logger.log(`Processing ${type} notification for user: ${recipientId}`);
|
||||
|
||||
await this.notificationHandlerFactory.processNotification(type, recipientId, data, em);
|
||||
switch (type) {
|
||||
case NotifType.NEW_EMAIL:
|
||||
await this.notificationsService.createNewEmailNotification(recipientId, data as INewEmailNotificationData);
|
||||
break;
|
||||
|
||||
await em.commit();
|
||||
case NotifType.CHANGE_PASSWORD:
|
||||
await this.notificationsService.changePasswordNotification(recipientId, data as IChangePasswordNotificationData);
|
||||
break;
|
||||
|
||||
case NotifType.USER_LOGIN:
|
||||
await this.notificationsService.createLoginNotification(recipientId, data);
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported notification type: ${type}`);
|
||||
}
|
||||
|
||||
this.logger.log(`Successfully processed ${type} notification for user: ${recipientId}`);
|
||||
} catch (error) {
|
||||
await em.rollback();
|
||||
this.logger.error(`Failed to process ${type} notification for user ${recipientId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,53 +29,16 @@ export class NotificationsService {
|
||||
|
||||
//************************ */
|
||||
|
||||
async createNotification(createNotificationDto: CreateNotificationDto, em?: EntityManager) {
|
||||
if (!em) {
|
||||
const localEm = this.em.fork();
|
||||
async createNotification(createNotificationDto: CreateNotificationDto) {
|
||||
const em = this.em.fork();
|
||||
|
||||
await localEm.begin();
|
||||
try {
|
||||
//
|
||||
const user = await localEm.findOne(User, { id: createNotificationDto.recipientId });
|
||||
if (!user) throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||
const notification = localEm.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: user,
|
||||
});
|
||||
await localEm.persistAndFlush(notification);
|
||||
|
||||
// Send push notification to all user devices if user has push tokens
|
||||
if (user.pushTokens && user.pushTokens.length > 0) {
|
||||
await this.sendPushNotificationToUserDevices(user.id, user.pushTokens, {
|
||||
title: createNotificationDto.title,
|
||||
body: createNotificationDto.message,
|
||||
notification_click: {
|
||||
click_url: `${this.configService.getOrThrow("FRONTEND_URL")}/notifications`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await localEm.commit();
|
||||
|
||||
return notification;
|
||||
//
|
||||
} catch (error) {
|
||||
this.logger.error("error in createNotification", error);
|
||||
await localEm.rollback();
|
||||
throw error;
|
||||
//
|
||||
}
|
||||
//
|
||||
} else {
|
||||
//
|
||||
const user = await em.findOne(User, { id: createNotificationDto.recipientId, deletedAt: null });
|
||||
try {
|
||||
const user = await em.findOne(User, { id: createNotificationDto.recipientId });
|
||||
if (!user) throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const notification = em.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: user,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(notification);
|
||||
|
||||
// Send push notification to all user devices if user has push tokens
|
||||
@@ -89,24 +52,18 @@ export class NotificationsService {
|
||||
});
|
||||
}
|
||||
|
||||
await em.commit();
|
||||
|
||||
return notification;
|
||||
} catch (error) {
|
||||
this.logger.error("error in createNotification", error);
|
||||
await em.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//===============================================
|
||||
|
||||
async sendPushNotificationToUser(userToken: string, message: INajvaNotificationMessage) {
|
||||
return this.najvaPushService.sendNotificationToUser(userToken, message);
|
||||
}
|
||||
|
||||
//===============================================
|
||||
|
||||
async sendPushNotificationToMultipleUsers(userTokens: string[], message: INajvaNotificationMessage) {
|
||||
return this.najvaPushService.sendNotificationToMultipleUsers(userTokens, message);
|
||||
}
|
||||
|
||||
//===============================================
|
||||
|
||||
async sendPushNotificationToUserDevices(userId: string, userTokens: string[], message: INajvaNotificationMessage) {
|
||||
try {
|
||||
const result = await this.najvaPushService.sendNotificationToMultipleUsers(userTokens, message);
|
||||
@@ -184,20 +141,20 @@ export class NotificationsService {
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async createNewEmailNotification(recipientId: string, data: INewEmailNotificationData, em: EntityManager) {
|
||||
async createNewEmailNotification(recipientId: string, data: INewEmailNotificationData) {
|
||||
const message = NotificationMessage.NEW_EMAIL_MESSAGE.replace("[subject]", data.subject);
|
||||
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.NEW_EMAIL, recipientId);
|
||||
if (isActive) {
|
||||
return this.createNotification({ title: NotificationMessage.NEW_EMAIL, type: NotifType.NEW_EMAIL, message, recipientId }, em);
|
||||
return this.createNotification({ title: NotificationMessage.NEW_EMAIL, type: NotifType.NEW_EMAIL, message, recipientId });
|
||||
}
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async changePasswordNotification(recipientId: string, _data: IChangePasswordNotificationData, em: EntityManager) {
|
||||
async changePasswordNotification(recipientId: string, _data: IChangePasswordNotificationData) {
|
||||
const message = NotificationMessage.CHANGE_PASSWORD_MESSAGE;
|
||||
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.CHANGE_PASSWORD, recipientId);
|
||||
if (isActive) {
|
||||
return this.createNotification({ title: NotificationMessage.CHANGE_PASSWORD, type: NotifType.CHANGE_PASSWORD, message, recipientId }, em);
|
||||
return this.createNotification({ title: NotificationMessage.CHANGE_PASSWORD, type: NotifType.CHANGE_PASSWORD, message, recipientId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user