chore: add new route
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { IsEnum, IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
|
||||
export class CreateNotificationDto {
|
||||
@IsNotEmpty({ message: NotificationMessage.TITLE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.TITLE_STRING })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.MESSAGE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.MESSAGE_STRING })
|
||||
message: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.USERID_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.USERID_IS_STRING })
|
||||
@IsUUID("4", { message: NotificationMessage.USERID_IS_UUID })
|
||||
recipientId: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.TYPE_IS_REQUIRED })
|
||||
@IsEnum(NotifType)
|
||||
type: NotifType;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, IsOptional } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export class SearchNotificationQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsIn([0, 1])
|
||||
@ApiPropertyOptional({ description: "Notification status", example: 1 })
|
||||
isRead?: number;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const NOTIFICATION = Object.freeze({
|
||||
QUEUE_NAME: "notification",
|
||||
SEND_NOTIFICATION_JOB_NAME: "sendNotification",
|
||||
SEND_LOGIN_OTP_JOB_NAME: "sendLoginOtp",
|
||||
|
||||
SEND_NOTIFICATION_JOB_DELAY: 1000, // delay after 1 second
|
||||
SEND_NOTIFICATION_JOB_PRIORITY: 1, // high priority
|
||||
SEND_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times
|
||||
SEND_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 seconds
|
||||
SEND_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds
|
||||
|
||||
SEND_NOTIFICATION_JOB_CONCURRENCY: 2,
|
||||
SEND_NOTIFICATION_JOB_LOCK_DURATION: 30000,
|
||||
SEND_NOTIFICATION_JOB_STALLED_INTERVAL: 30000,
|
||||
SEND_NOTIFICATION_JOB_MAX_STALLED_COUNT: 2,
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Entity({ repository: () => NotificationRepository })
|
||||
export class Notification extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 250 })
|
||||
title!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
message!: string;
|
||||
|
||||
@Enum({ items: () => NotifType, nativeEnumName: "notif_type" })
|
||||
type!: NotifType;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isRead!: boolean & Opt;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade" })
|
||||
recipient!: User;
|
||||
|
||||
[EntityRepositoryType]?: NotificationRepository;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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> {
|
||||
await this.notificationsService.createNewEmailNotification(recipientId, data, em);
|
||||
}
|
||||
|
||||
protected getHandlerName(): string {
|
||||
return "NewEmail";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { IBaseNotificationData, IChangePasswordNotificationData, INewEmailNotificationData } from "./ISendNotificationData";
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
|
||||
export type NotificationJobData = {
|
||||
type: NotifType;
|
||||
recipientId: string;
|
||||
data: IBaseNotificationData | IChangePasswordNotificationData | INewEmailNotificationData;
|
||||
};
|
||||
|
||||
export type OtpJobData = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface IBaseNotificationData {
|
||||
userPhone?: string;
|
||||
userEmail?: string;
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
export interface IChangePasswordNotificationData extends IBaseNotificationData {
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface IGenericNotificationData extends IBaseNotificationData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface INewEmailNotificationData extends IBaseNotificationData {
|
||||
newEmail: string;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { Controller, Get, Param, Patch, Query } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { SearchNotificationQueryDto } from "./DTO/search-notification-query.dto";
|
||||
import { NotificationsService } from "./services/notifications.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
|
||||
@Controller("notifications")
|
||||
@AuthGuards()
|
||||
export class NotificationController {
|
||||
constructor(private readonly notificationService: NotificationsService) {}
|
||||
|
||||
//********************* */
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "all notifications by user" })
|
||||
getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) {
|
||||
return this.notificationService.getAllNotifications(queryDto, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@Patch(":id/read")
|
||||
@ApiOperation({ summary: "mark user notification as read" })
|
||||
markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.notificationService.markAsRead(paramDto.id, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@Patch("read-all")
|
||||
@ApiOperation({ summary: "mark user all notification as read" })
|
||||
markAllAsRead(@UserDec("id") userId: string) {
|
||||
return this.notificationService.markAllAsRead(userId);
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
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 { NotificationController } from "./notifications.controller";
|
||||
import { NotificationProcessor } from "./queue/notification.processor";
|
||||
import { NotificationQueue } from "./queue/notification.queue";
|
||||
import { NotificationsService } from "./services/notifications.service";
|
||||
import { NotificationSetting } from "../settings/entities/notification-setting.entity";
|
||||
import { SettingModule } from "../settings/settings.module";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { UserLoginHandler } from "./handlers/user-login.handler";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
MikroOrmModule.forFeature([Notification, NotificationSetting]),
|
||||
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,
|
||||
},
|
||||
}),
|
||||
UtilsModule,
|
||||
SettingModule,
|
||||
],
|
||||
providers: [
|
||||
NotificationsService,
|
||||
NotificationQueue,
|
||||
NotificationProcessor,
|
||||
// notification handlers
|
||||
NotificationHandlerFactory,
|
||||
UserLoginHandler,
|
||||
NewEmailHandler,
|
||||
ChangePasswordHandler,
|
||||
],
|
||||
controllers: [NotificationController],
|
||||
exports: [NotificationsService, NotificationQueue],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Processor } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
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";
|
||||
|
||||
@Processor(NOTIFICATION.QUEUE_NAME, {
|
||||
concurrency: NOTIFICATION.SEND_NOTIFICATION_JOB_CONCURRENCY,
|
||||
lockDuration: NOTIFICATION.SEND_NOTIFICATION_JOB_LOCK_DURATION,
|
||||
stalledInterval: NOTIFICATION.SEND_NOTIFICATION_JOB_STALLED_INTERVAL,
|
||||
maxStalledCount: NOTIFICATION.SEND_NOTIFICATION_JOB_MAX_STALLED_COUNT,
|
||||
})
|
||||
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,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<NotificationJobData | OtpJobData>, token?: string) {
|
||||
this.logger.log(`Processing notification job: ${job.name} ${token ? `with token: ${token}` : ""}`);
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`Unknown job name: ${job.name}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async processWithTransaction(type: NotifType, recipientId: string, data: IBaseNotificationData): Promise<void> {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
|
||||
await this.notificationHandlerFactory.processNotification(type, recipientId, data, em);
|
||||
|
||||
await em.commit();
|
||||
} catch (error) {
|
||||
await em.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Queue } from "bullmq";
|
||||
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { IBaseNotificationData, IChangePasswordNotificationData, INewEmailNotificationData } from "../interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationQueue {
|
||||
constructor(@InjectQueue(NOTIFICATION.QUEUE_NAME) private readonly notificationsQueue: Queue) {}
|
||||
|
||||
async addNotificationJob(type: NotifType, recipientId: string, data: IBaseNotificationData) {
|
||||
await this.notificationsQueue.add(
|
||||
NOTIFICATION.SEND_NOTIFICATION_JOB_NAME,
|
||||
{
|
||||
type,
|
||||
recipientId,
|
||||
data,
|
||||
},
|
||||
{
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
priority: NOTIFICATION.SEND_NOTIFICATION_JOB_PRIORITY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async addLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
await this.addNotificationJob(NotifType.USER_LOGIN, recipientId, data);
|
||||
}
|
||||
|
||||
async addNewEmailNotification(recipientId: string, data: INewEmailNotificationData) {
|
||||
await this.addNotificationJob(NotifType.NEW_EMAIL, recipientId, data);
|
||||
}
|
||||
|
||||
async addChangePasswordNotification(recipientId: string, data: IChangePasswordNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CHANGE_PASSWORD, recipientId, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { EntityRepository, FilterQuery } from "@mikro-orm/postgresql";
|
||||
|
||||
import { PaginationUtils } from "../../utils/services/pagination.utils";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
|
||||
export class NotificationRepository extends EntityRepository<Notification> {
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
const { skip, limit } = PaginationUtils(queryDto);
|
||||
|
||||
const whereConditions: FilterQuery<Notification> = { recipient: { id: recipientId } };
|
||||
|
||||
if (queryDto.isRead !== undefined) {
|
||||
whereConditions.isRead = queryDto.isRead === 1;
|
||||
}
|
||||
|
||||
return this.findAndCount(whereConditions, {
|
||||
orderBy: { createdAt: "DESC" },
|
||||
offset: skip,
|
||||
limit: limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CommonMessage, NotificationMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
import { IBaseNotificationData, IChangePasswordNotificationData, INewEmailNotificationData } from "../interfaces/ISendNotificationData";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name);
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly notificationRepository: NotificationRepository,
|
||||
private readonly userSettingsService: UserSettingsService,
|
||||
) {}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createNotification(createNotificationDto: CreateNotificationDto, em?: EntityManager) {
|
||||
if (!em) {
|
||||
const localEm = 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);
|
||||
|
||||
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 });
|
||||
if (!user) throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const notification = em.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: user,
|
||||
});
|
||||
//
|
||||
await em.persistAndFlush(notification);
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAsRead(notificationId: string, userId: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId }, deletedAt: null });
|
||||
|
||||
if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
|
||||
notification.isRead = true;
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
const [notifications, count] = await this.notificationRepository.getAllNotifications(queryDto, recipientId);
|
||||
const notificationCount = await this.countUserNotifications(recipientId);
|
||||
return { notifications, notificationCount, count, paginate: true };
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getNotificationById(id: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id, deletedAt: null });
|
||||
if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
return { notification };
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async countUserNotifications(userId: string) {
|
||||
const notificationCount = await this.notificationRepository.count({ recipient: { id: userId }, isRead: false, deletedAt: null });
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAllAsRead(userId: string) {
|
||||
await this.notificationRepository.nativeUpdate({ recipient: { id: userId }, deletedAt: null }, { isRead: true });
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createLoginNotification(recipientId: string, _data: IBaseNotificationData) {
|
||||
const loginDate = new Date().toLocaleString("fa-IR");
|
||||
const message = NotificationMessage.LOGIN_MESSAGE.replace("[loginDate]", loginDate);
|
||||
//sent notification based on the userSettings
|
||||
const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.USER_LOGIN, recipientId);
|
||||
if (isActive) {
|
||||
return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId });
|
||||
}
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createNewEmailNotification(recipientId: string, data: INewEmailNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.NEW_EMAIL_MESSAGE.replace("[subject]", data.newEmail);
|
||||
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);
|
||||
}
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async changePasswordNotification(recipientId: string, _data: IChangePasswordNotificationData, em: EntityManager) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user