diff --git a/package.json b/package.json index ed1906f..7d6606a 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,6 @@ "version": "1.0.0", "description": "NestJs project generated using startease CLI tool", "license": "MIT", - "engines": { - "node": "20" - }, "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", diff --git a/src/app.module.ts b/src/app.module.ts index 4b729e5..428381e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { RepositoryModule } from './module/v1/repository/repository.module'; import { SeederModule } from './module/v1/seeder/seeder.module'; import { ProjectModule } from './module/v1/project/project.module'; import { TechnologyModule } from './module/v1/technology/technology.module'; +import { RatingModule } from './module/v1/rating/rating.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { TechnologyModule } from './module/v1/technology/technology.module'; SeederModule, ProjectModule, TechnologyModule, + RatingModule, ], controllers: [], providers: [], diff --git a/src/common/enums/meeting.enum.ts b/src/common/enums/meeting.enum.ts deleted file mode 100644 index 83b0422..0000000 --- a/src/common/enums/meeting.enum.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum MeetingStatusEnum { - CANCELLED = 'CANCELLED', - PENDING = 'PENDING', - PENDING_APPROVAL = 'PENDING_APPROVAL', - STARTED = 'STARTED', - ENDED = 'ENDED', - UPCOMING = 'UPCOMING', - REJECTED = 'REJECTED', -} diff --git a/src/module/v1/project/project.module.ts b/src/module/v1/project/project.module.ts index bd6a5a2..bfd8dad 100644 --- a/src/module/v1/project/project.module.ts +++ b/src/module/v1/project/project.module.ts @@ -17,5 +17,6 @@ import { RepositoryModule } from 'src/module/v1/repository/repository.module'; ], controllers: [ProjectController], providers: [ProjectService], + exports: [ProjectService], }) export class ProjectModule {} diff --git a/src/module/v1/project/project.service.ts b/src/module/v1/project/project.service.ts index 19c0755..fd9af64 100644 --- a/src/module/v1/project/project.service.ts +++ b/src/module/v1/project/project.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; +import { FilterQuery, Model, UpdateQuery } from 'mongoose'; import { uploadSingleFile } from 'src/common/utils/aws.util'; import { BaseHelper } from 'src/common/utils/helper.util'; import { CreateProjectDto, UpdateProjectDto } from 'src/module/v1/project/dto/project.dto'; @@ -92,4 +92,8 @@ export class ProjectService { return deletedProject; } + + async updateQuery(query: FilterQuery, updatePayload: UpdateQuery) { + return await this.projectModel.findOneAndUpdate(query, updatePayload, { new: true }); + } } diff --git a/src/module/v1/rating/dto/rating.dto.ts b/src/module/v1/rating/dto/rating.dto.ts new file mode 100644 index 0000000..47803bd --- /dev/null +++ b/src/module/v1/rating/dto/rating.dto.ts @@ -0,0 +1,23 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { IsMongoId, IsNotEmpty, IsNumber, IsOptional, Min } from 'class-validator'; + +export class CrateRatingDto { + @IsNotEmpty() + @IsNumber() + @Min(1) + rating: number; + + @IsNotEmpty() + @IsMongoId({ message: 'Project id is not valid' }) + projectId: string; + + @IsNotEmpty() + @IsOptional() + comment: string; +} + +export class UpdateRatingDto extends PartialType(CrateRatingDto) { + @IsNotEmpty() + @IsMongoId({ message: 'Rating id is not valid' }) + ratingId: string; +} diff --git a/src/module/v1/rating/rating.controller.ts b/src/module/v1/rating/rating.controller.ts new file mode 100644 index 0000000..6223a7e --- /dev/null +++ b/src/module/v1/rating/rating.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { RatingService } from './rating.service'; +import { LoggedInUserDecorator } from 'src/common/decorators/logged_in_user.decorator'; +import { UserDocument } from 'src/module/v1/user/schemas/user.schema'; +import { CrateRatingDto, UpdateRatingDto } from 'src/module/v1/rating/dto/rating.dto'; +import { PaginationDto } from 'src/module/v1/repository/dto/repository.dto'; + +@Controller('rating') +export class RatingController { + constructor(private readonly ratingService: RatingService) {} + + @Post() + async create(@Body() payload: CrateRatingDto, @LoggedInUserDecorator() user: UserDocument) { + return await this.ratingService.create(user, payload); + } + + @Patch() + async update(@Body() payload: UpdateRatingDto, @LoggedInUserDecorator() user: UserDocument) { + return await this.ratingService.updateRating(user, payload); + } + + @Get('project/:projectId') + async getRatingByProject(@Param('projectId') projectId: string, @Query() query: PaginationDto) { + return await this.ratingService.getRatingByProject(projectId, query); + } + + @Get(':id') + async getRatingById(@Param('id') id: string) { + return await this.ratingService.getRatingById(id); + } + + @Delete(':ratingId') + async deleteRating(@Param('ratingId') ratingId: string, @LoggedInUserDecorator() user: UserDocument) { + return await this.ratingService.deleteRating(ratingId, user); + } +} diff --git a/src/module/v1/rating/rating.module.ts b/src/module/v1/rating/rating.module.ts new file mode 100644 index 0000000..e873051 --- /dev/null +++ b/src/module/v1/rating/rating.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { RatingService } from './rating.service'; +import { RatingController } from './rating.controller'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Rating, RatingSchema } from 'src/module/v1/rating/schema/rating.schema'; +import { ProjectModule } from 'src/module/v1/project/project.module'; +import { RepositoryModule } from 'src/module/v1/repository/repository.module'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: Rating.name, + schema: RatingSchema, + }, + ]), + RepositoryModule, + ProjectModule, + ], + controllers: [RatingController], + providers: [RatingService], +}) +export class RatingModule {} diff --git a/src/module/v1/rating/rating.service.ts b/src/module/v1/rating/rating.service.ts new file mode 100644 index 0000000..c567a6e --- /dev/null +++ b/src/module/v1/rating/rating.service.ts @@ -0,0 +1,130 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import mongoose, { Model } from 'mongoose'; +import { ProjectService } from 'src/module/v1/project/project.service'; +import { CrateRatingDto, UpdateRatingDto } from 'src/module/v1/rating/dto/rating.dto'; +import { Rating, RatingDocument } from 'src/module/v1/rating/schema/rating.schema'; +import { PaginationDto } from 'src/module/v1/repository/dto/repository.dto'; +import { RepositoryService } from 'src/module/v1/repository/repository.service'; +import { UserDocument } from 'src/module/v1/user/schemas/user.schema'; + +@Injectable() +export class RatingService { + constructor( + @InjectModel(Rating.name) private ratingModel: Model, + private repositoryService: RepositoryService, + private projectService: ProjectService, + ) {} + + async create(user: UserDocument, payload: CrateRatingDto) { + const { projectId, rating, comment } = payload; + const project = await this.projectService.getProjectById(projectId); + + if (!project) { + throw new NotFoundException('Project not found'); + } + + const ratingData = await this.ratingModel.create({ + user: user._id, + project: project._id, + rating, + comment, + }); + + if (!ratingData) { + throw new NotFoundException('Unable to rate project, please try again'); + } + + const averageRating = await this.getAverageRating(projectId); + if (averageRating > 0) { + await this.projectService.updateQuery({ _id: project._id }, { averageRating }); + } + + return ratingData; + } + + async getAverageRating(projectId: string) { + const averageRating = await this.ratingModel.aggregate([ + { + $match: { + project: new mongoose.Types.ObjectId(projectId), + isDeleted: false, + }, + }, + { + $group: { + _id: '$project', + averageRating: { $avg: '$rating' }, + }, + }, + { + $project: { + _id: 0, + averageRating: 1, + }, + }, + ]); + + return averageRating[0]?.averageRating || 0; + } + + async updateRating(user: UserDocument, payload: UpdateRatingDto) { + const { ratingId } = payload; + + const updateRating = await this.ratingModel.findOneAndUpdate( + { + _id: ratingId, + user: user._id, + }, + { + ...payload, + }, + { new: true }, + ); + + if (!updateRating) { + throw new NotFoundException('Rating not found'); + } + + return updateRating; + } + + async getRatingByProject(projectId: string, query: PaginationDto) { + const [averageRating, rating] = await Promise.all([ + this.getAverageRating(projectId), + this.repositoryService.paginate(this.ratingModel, query, { project: projectId }, 'user project'), + ]); + + return { + rating, + averageRating, + }; + } + + async deleteRating(ratingId: string, user: UserDocument) { + const deletedRating = await this.ratingModel.findOneAndUpdate( + { + _id: ratingId, + user: user._id, + }, + { + isDeleted: true, + }, + ); + + if (!deletedRating) { + throw new NotFoundException('Rating not found'); + } + + // update project average rating + const project = await this.projectService.getProjectById(deletedRating.project); + if (project) { + const averageRating = await this.getAverageRating(deletedRating.project); + await this.projectService.updateQuery({ _id: project._id }, { averageRating }); + } + } + + async getRatingById(id: string) { + return await this.ratingModel.findById(id); + } +} diff --git a/src/module/v1/rating/schema/rating.schema.ts b/src/module/v1/rating/schema/rating.schema.ts new file mode 100644 index 0000000..213e093 --- /dev/null +++ b/src/module/v1/rating/schema/rating.schema.ts @@ -0,0 +1,35 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import mongoose, { Document } from 'mongoose'; +import { Project } from 'src/module/v1/project/schema/project.schema'; +import { User } from 'src/module/v1/user/schemas/user.schema'; + +export type RatingDocument = Rating & Document; + +@Schema({ timestamps: true }) +export class Rating { + @Prop({ type: mongoose.Schema.Types.ObjectId, ref: User.name }) + user: string; + + @Prop({ type: mongoose.Schema.Types.ObjectId, ref: Project.name }) + project: string; + + @Prop({ enum: [1, 2, 3, 4, 5], default: 0 }) + rating: number; + + @Prop() + comment: string; + + @Prop({ default: false }) + isDeleted: boolean; +} + +export const RatingSchema = SchemaFactory.createForClass(Rating); + +RatingSchema.index({ project: 1, user: 1 }, { unique: true }); + +RatingSchema.pre('find', function (next) { + // remove deleted ratings + this.where({ isDeleted: false }); + + next(); +});