Skip to content
This repository has been archived by the owner on Mar 23, 2024. It is now read-only.

Commit

Permalink
fix: previous issues with notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
nmashchenko committed Dec 17, 2023
1 parent c275abc commit 61eff0c
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateUser1702763328738 implements MigrationInterface {
name = 'CreateUser1702763328738'
export class CreateUser1702842748127 implements MigrationInterface {
name = 'CreateUser1702842748127'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "file" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "path" character varying NOT NULL, CONSTRAINT "PK_36b46d232307066b3a2c9ea3a1d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "role" ("id" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "status" ("id" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_e12743a7086ec826733f54e1d95" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "file" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "path" character varying NOT NULL, CONSTRAINT "PK_36b46d232307066b3a2c9ea3a1d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "universities" ("id" SERIAL NOT NULL, "university" character varying NOT NULL, "degree" character varying NOT NULL, "major" character varying NOT NULL, "admissionDate" date NOT NULL, "graduationDate" date, "userId" integer, CONSTRAINT "PK_8da52f2cee6b407559fdbabf59e" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "jobs" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "company" character varying NOT NULL, "startDate" date NOT NULL, "endDate" date, "userId" integer, CONSTRAINT "PK_cf0a6c42b72fcc7f7c237def345" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "projects" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "link" character varying NOT NULL, "userId" integer, CONSTRAINT "PK_6271df0a7aed1d6c0691ce6ac50" PRIMARY KEY ("id"))`);
Expand Down Expand Up @@ -58,9 +58,9 @@ export class CreateUser1702763328738 implements MigrationInterface {
await queryRunner.query(`DROP TABLE "projects"`);
await queryRunner.query(`DROP TABLE "jobs"`);
await queryRunner.query(`DROP TABLE "universities"`);
await queryRunner.query(`DROP TABLE "file"`);
await queryRunner.query(`DROP TABLE "status"`);
await queryRunner.query(`DROP TABLE "role"`);
await queryRunner.query(`DROP TABLE "file"`);
}

}
60 changes: 60 additions & 0 deletions server/src/modules/notifications/dto/query-notification.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
import { plainToInstance, Transform, Type } from 'class-transformer';
export class SortNotificationDto {
@ApiProperty()
@IsString()
orderBy: keyof Notification;

@ApiProperty()
@IsString()
order: string;
}

export class FilterNotificationDto {
@ApiProperty()
@IsOptional()
@IsNotEmpty()
type?: string;

@ApiProperty()
@IsOptional()
@IsNotEmpty()
read?: string;
}

export class QueryNotificationDto {
@ApiProperty({
required: false,
})
@Transform(({ value }) => (value ? Number(value) : 1))
@IsNumber()
@IsOptional()
page: number;

@ApiProperty({
required: false,
})
@Transform(({ value }) => (value ? Number(value) : 10))
@IsNumber()
@IsOptional()
limit: number;

@ApiProperty({ type: String, required: false })
@IsOptional()
@Transform(({ value }) =>
value ? plainToInstance(FilterNotificationDto, JSON.parse(value)) : undefined
)
@ValidateNested()
@Type(() => FilterNotificationDto)
filters: FilterNotificationDto;

@ApiProperty({ type: String, required: false })
@IsOptional()
@Transform(({ value }) => {
return value ? plainToInstance(SortNotificationDto, JSON.parse(value)) : undefined;
})
@ValidateNested({ each: true })
@Type(() => SortNotificationDto)
sort?: SortNotificationDto[] | null;
}
50 changes: 42 additions & 8 deletions server/src/modules/notifications/notifications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
Param,
Patch,
Post,
Query,
SerializeOptions,
Request,
UseGuards,
} from '@nestjs/common';
import { RoleEnum } from '../../libs/database/metadata/roles/roles.enum';
Expand All @@ -18,8 +20,10 @@ import { AuthGuard } from '@nestjs/passport';
import { NotificationsService } from './notifications.service';
import { CreateNotificationDto } from './dto/create-notification.dto';
import { NullableType } from '../../utils/types/nullable.type';
import { ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Notification } from './entities/notification.entity';
import { infinityPagination } from '../../utils/infinity-pagination';
import { QueryNotificationDto } from './dto/query-notification.dto';

@ApiTags('Notifications')
@Controller({
Expand All @@ -37,6 +41,35 @@ export class NotificationsController {
return await this.notificationService.createNotification(dto);
}

@SerializeOptions({
groups: ['user'],
})
@ApiBearerAuth()
@Roles(RoleEnum.user)
@UseGuards(AuthGuard('jwt'))
@Get('')
async findAll(@Request() request, @Query() query: QueryNotificationDto) {
const page = query?.page ?? 1;
let limit = query?.limit ?? 10;

if (limit > 10) {
limit = 10;
}

return infinityPagination(
await this.notificationService.findManyWithPagination({
userJwtPayload: request.user,
filterOptions: query?.filters,
sortOptions: query?.sort,
paginationOptions: {
page,
limit,
},
}),
{ page, limit }
);
}

@SerializeOptions({
groups: ['user'],
})
Expand All @@ -46,19 +79,20 @@ export class NotificationsController {
return this.notificationService.findOne({ id: +id });
}

@Get('user/:userid')
async findNotificationByReceiver(@Param('userid') userid: number) {
return await this.notificationService.findByReceiver(userid);
}

// add guard / httpcode
@Patch(':id')
@ApiBearerAuth()
@Roles(RoleEnum.user)
@UseGuards(AuthGuard('jwt'))
@HttpCode(HttpStatus.OK)
async readUnreadNotification(@Param('id') id: number) {
await this.notificationService.readNotification(id);
}

// add guard / httpcode
@Delete(':id')
@ApiBearerAuth()
@Roles(RoleEnum.user)
@UseGuards(AuthGuard('jwt'))
@HttpCode(HttpStatus.NO_CONTENT)
async deleteNotification(@Param('id') id: number) {
await this.notificationService.deleteNotification(id);
}
Expand Down
134 changes: 100 additions & 34 deletions server/src/modules/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { Notification } from './entities/notification.entity';
import { UsersService } from '../users/users.service';
import { User } from '../users/entities/user.entity';
import { CreateNotificationDto, SystemNotificationDataDto } from './dto/create-notification.dto';
import { NotificationTypesEnum } from './types/notification.type';
import { EntityCondition } from '../../utils/types/entity-condition.type';
import { NullableType } from '../../utils/types/nullable.type';
import { FilterNotificationDto, SortNotificationDto } from './dto/query-notification.dto';
import { IPaginationOptions } from '../../utils/types/pagination-options';
import { JwtPayloadType } from '../auth/base/strategies/types/jwt-payload.type';

@Injectable()
export class NotificationsService {
Expand All @@ -20,48 +23,99 @@ export class NotificationsService {
public async readNotification(id: number) {
const notification = await this.findOne({ id: id });

if (notification) {
notification.read = !notification.read;
await this.notificationRepository.save(notification);
if (!notification) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
user: `notification with id: ${id} was not found`,
},
},
HttpStatus.UNPROCESSABLE_ENTITY
);
}

notification.read = !notification.read;
await this.notificationRepository.save(notification);
}

// see user service and how it's done there
public async deleteNotification(id: number) {
const notification = await this.findOne({ id: id });

if (notification) {
await this.notificationRepository.remove(notification);
if (!notification) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
user: `notification with id: ${id} was not found`,
},
},
HttpStatus.UNPROCESSABLE_ENTITY
);
}

await this.notificationRepository.remove(notification);
}

// NB: this is ideal example of how it should be used
public async findOne(fields: EntityCondition<Notification>): Promise<NullableType<Notification>> {
return this.notificationRepository.findOne({
where: fields,
});
}

async findByFromUser(userId: number) {
return await this.notificationRepository
.createQueryBuilder('notification')
.leftJoinAndSelect('notification.receiver', 'receiver')
.where("notification.data -> 'fromUser' -> 'id' = :userId", {
userId: userId,
})
.getMany();
}
async findManyWithPagination({
userJwtPayload,
filterOptions,
sortOptions,
paginationOptions,
}: {
userJwtPayload: JwtPayloadType;
filterOptions: FilterNotificationDto;
sortOptions?: SortNotificationDto[] | null;
paginationOptions: IPaginationOptions;
}): Promise<Notification[]> {
const where: FindOptionsWhere<Notification> = {};

where.receiver = { id: userJwtPayload.id };

if (filterOptions?.type) {
switch (filterOptions.type) {
case NotificationTypesEnum.system:
where.type = NotificationTypesEnum.system;
break;
case NotificationTypesEnum.team_invitation:
where.type = NotificationTypesEnum.team_invitation;
break;
default:
// Handle the default case or leave it empty if not needed
break;
}
}

async findByReceiver(receiverId: number) {
return await this.notificationRepository.find({
where: { receiver: { id: receiverId } },
if (filterOptions?.read) {
where.read = filterOptions.read.toLowerCase() == 'true';
}

return this.notificationRepository.find({
skip: (paginationOptions.page - 1) * paginationOptions.limit,
take: paginationOptions.limit,
where: where,
order: sortOptions?.reduce(
(accumulator, sort) => ({
...accumulator,
[sort.orderBy]: sort.order,
}),
{}
),
});
}

// improve code here
async createNotification(dto: CreateNotificationDto) {
const receiver = await this.checkAndGetReceiver(dto.receiver);
const receiver = await this.getUser(dto.receiver);

const data = await this.getDataByType(dto);

await this.notificationRepository.save(
this.notificationRepository.create({
receiver: receiver,
Expand All @@ -72,22 +126,34 @@ export class NotificationsService {
}

private async getDataByType(dto: CreateNotificationDto) {
//TODO: rewrite for switch to add more cases
if (dto.type == 'system') {
const data = dto.data as SystemNotificationDataDto;
return {
system_message: data.system_message,
};
switch (dto.type) {
case 'system':
const data = dto.data as SystemNotificationDataDto;
return {
system_message: data.system_message,
};
// Add more cases here as needed
default:
// Handle the default case or leave it empty if not needed
break;
}
}

// improve code here, doesn;t have to error out
private async checkAndGetReceiver(receiver: string): Promise<User> {
const user = await this.usersService.findOne({ username: receiver });
private async getUser(username: string): Promise<User> {
const user = await this.usersService.findOne({ username });

if (!user) {
throw new BadRequestException('Receiver not exist');
} else {
return user;
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
user: `user ${username} was not found`,
},
},
HttpStatus.UNPROCESSABLE_ENTITY
);
}

return user;
}
}

0 comments on commit 61eff0c

Please sign in to comment.