From 2b4edfbddba279858743a451855468d57dff4f2d Mon Sep 17 00:00:00 2001 From: Adelina Enache Date: Sun, 19 May 2024 10:45:59 +0300 Subject: [PATCH] feat: write-review-page (#71) * Add joined at date for searched user * Update user search query to respect respect http standards * Enable cors * minor fixes * Correct relations * Check if users are connected when doing an user search * fix db structure * fix namings --- src/main.ts | 1 + .../20240509195830_fix_namings/migration.sql | 29 ++++++++++ src/prisma/prisma.service.ts | 16 ------ src/prisma/schema.prisma | 31 +++++----- src/schemas/user.properties.ts | 7 +++ src/user/controller/user.controller.ts | 29 +++++++--- src/user/service/user.service.ts | 56 +++++++++++++------ src/user/user.e2e.spec.ts | 14 ++--- 8 files changed, 119 insertions(+), 64 deletions(-) create mode 100644 src/prisma/migrations/20240509195830_fix_namings/migration.sql diff --git a/src/main.ts b/src/main.ts index 9ccf174..76e2fb5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const globalPrefix = 'api'; app.setGlobalPrefix(globalPrefix); + app.enableCors(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, diff --git a/src/prisma/migrations/20240509195830_fix_namings/migration.sql b/src/prisma/migrations/20240509195830_fix_namings/migration.sql new file mode 100644 index 0000000..30285c1 --- /dev/null +++ b/src/prisma/migrations/20240509195830_fix_namings/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to drop the column `ratedUserId` on the `Rating` table. All the data in the column will be lost. + - You are about to drop the column `raterUserId` on the `Rating` table. All the data in the column will be lost. + - Added the required column `postedToId` to the `Rating` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + + +-- DropForeignKey +ALTER TABLE "Rating" DROP CONSTRAINT "Rating_ratedUserId_fkey"; + +-- DropForeignKey +ALTER TABLE "Rating" DROP CONSTRAINT "Rating_raterUserId_fkey"; + +-- AlterTable +ALTER TABLE "Rating" DROP COLUMN "ratedUserId", +DROP COLUMN "raterUserId", +ADD COLUMN "postedById" TEXT, +ADD COLUMN "postedToId" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "Rating" ADD CONSTRAINT "Rating_postedToId_fkey" FOREIGN KEY ("postedToId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Rating" ADD CONSTRAINT "Rating_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 4450538..359f950 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -6,20 +6,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); } - async searchUsers(searchTerm: string) { - return await this.user.findMany({ - where: { - OR: [ - { email: { contains: searchTerm } }, - { name: { contains: searchTerm } }, - ], - }, - select: { - id: true, - email: true, - name: true, - profilePictureUrl: true, - }, - }); - } } diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 4fc31cd..e496f7a 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -26,18 +26,19 @@ model User { isEmailVerified Boolean @default(false) headline String? jobTitle String? - connections Connection[] @relation("follower") - followedConnections Connection[] @relation("following") + followers Connection[] @relation("followsTheUser") + followings Connection[] @relation("followedByUser") LinkedSocialAccounts LinkedSocialAccount[] - ratings Rating[] @relation("ratedUser") - ratingsReceived Rating[] @relation("raterUser") + ratingsPosted Rating[] @relation("ratingsGivenToOtherUsers") + ratingsReceived Rating[] @relation("ratingsRecievedFromOtherUsers") + joinedAt DateTime @default(now()) } model Connection { id Int @id @default(autoincrement()) - follower User @relation("follower", fields: [followerId], references: [id]) + follower User @relation("followedByUser", fields: [followerId], references: [id]) followerId String - following User @relation("following", fields: [followingId], references: [id]) + following User @relation("followsTheUser", fields: [followingId], references: [id]) followingId String @@unique([followerId, followingId]) @@ -61,11 +62,11 @@ model LinkedSocialAccount { model Rating { id Int @id @default(autoincrement()) // Person who is being rated - ratedUser User @relation(fields: [ratedUserId], references: [id], onDelete: Cascade, onUpdate: Cascade, name: "ratedUser") - ratedUserId String + postedTo User @relation(fields: [postedToId], references: [id], onDelete: Cascade, onUpdate: Cascade, name: "ratingsRecievedFromOtherUsers") + postedToId String // Person who is rating - raterUser User? @relation(fields: [raterUserId], references: [id], onDelete: SetNull, onUpdate: Cascade, name: "raterUser") - raterUserId String? + postedBy User? @relation(fields: [postedById], references: [id], onDelete: SetNull, onUpdate: Cascade, name: "ratingsGivenToOtherUsers") + postedById String? professionalism Int @default(0) reliability Int @default(0) communication Int @default(0) @@ -75,12 +76,12 @@ model Rating { } model UserSocialAccount { - id String @id @default(cuid()) - provider String @unique - accessToken String + id String @id @default(cuid()) + provider String @unique + accessToken String - user User @relation(fields: [userId], references: [id]) - userId String + user User @relation(fields: [userId], references: [id]) + userId String } model VerificationCode { diff --git a/src/schemas/user.properties.ts b/src/schemas/user.properties.ts index c7983dd..3cc40c6 100644 --- a/src/schemas/user.properties.ts +++ b/src/schemas/user.properties.ts @@ -10,4 +10,11 @@ export const userProperties = { isEmailVerified: { type: 'boolean' }, headline: { type: 'string' }, jobTitle: { type: 'string' }, + joinedAt: { type: 'string', format: 'date-time' }, +}; + +export const userExtraProps = { + connectionsCount: { type: 'number' }, + ratingsCount: { type: 'number' }, + isConnection: { type: 'boolean' }, }; diff --git a/src/user/controller/user.controller.ts b/src/user/controller/user.controller.ts index 1bde7ca..e36ea4b 100644 --- a/src/user/controller/user.controller.ts +++ b/src/user/controller/user.controller.ts @@ -5,7 +5,9 @@ import { Param, Post, Put, + Query, UploadedFile, + UseGuards, UseInterceptors, } from '@nestjs/common'; import { CurrentUser } from '../../decorators/current-user.decorator'; @@ -25,9 +27,10 @@ import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, } from '@nestjs/swagger'; -import { userProperties } from '../../schemas/user.properties'; +import { userExtraProps, userProperties } from '../../schemas/user.properties'; import { reviewProperties, reviewPropertiesWithComment, @@ -35,6 +38,7 @@ import { // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Multer } from 'multer'; import { Public } from '../../decorators/public.decorator'; +import { AuthGuard } from '../../auth/guard/auth/auth.guard'; @Controller('user') @ApiBearerAuth() @@ -129,7 +133,7 @@ export class UserController { type: 'object', properties: { id: { type: 'number' }, - ratedUserId: { type: 'string' }, + postedToId: { type: 'string' }, raterUserId: { type: 'string' }, professionalism: { type: 'number' }, reliability: { type: 'number' }, @@ -141,10 +145,10 @@ export class UserController { }) async rateUser( @CurrentUser() user: User, - @Param('userId') ratedUserId: string, + @Param('userId') postedToId: string, @Body() rating: RatingDto, ) { - return this.userService.rateUser(user, ratedUserId, rating); + return this.userService.rateUser(user, postedToId, rating); } @Get('ratings/self') @@ -263,8 +267,7 @@ export class UserController { return this.userService.getAvgUserRatings(user, false, userId); } - @Public() - @Get('search/:query') + @Get('search') @ApiOperation({ summary: 'Search users', description: 'Search for users', @@ -275,11 +278,19 @@ export class UserController { type: 'array', items: { type: 'object', - properties: userProperties, + properties: { ...userExtraProps, ...userProperties }, }, }, }) - async searchUsers(@Param('query') query: string) { - return this.userService.searchUsers(query); + @ApiQuery({ + name: 'query', + type: 'string', + }) + @UseGuards(AuthGuard) + async searchUsers( + @CurrentUser() user: User, + @Query() { query }: { query: string }, + ) { + return this.userService.searchUsers(user.id, query); } } diff --git a/src/user/service/user.service.ts b/src/user/service/user.service.ts index 849824d..0238316 100644 --- a/src/user/service/user.service.ts +++ b/src/user/service/user.service.ts @@ -69,9 +69,9 @@ export class UserService { } } - async rateUser(user: User, ratedUserId: User['id'], ratingDto: RatingDto) { + async rateUser(user: User, postedToId: User['id'], ratingDto: RatingDto) { const ratedUser = await this.prisma.user.findUnique({ - where: { id: ratedUserId }, + where: { id: postedToId }, }); // Check if the user exists @@ -80,7 +80,7 @@ export class UserService { } // Check if the user is trying to rate himself - if (user.id === ratedUserId) { + if (user.id === postedToId) { throw new BadRequestException('You cannot rate yourself'); } @@ -89,8 +89,8 @@ export class UserService { // Rate the user const rating = await this.prisma.rating.create({ data: { - ratedUserId: ratedUserId, - raterUserId: ratingDto.anonymous ? null : user.id, + postedToId: postedToId, + postedById: ratingDto.anonymous ? null : user.id, professionalism: ratingDto.professionalism, reliability: ratingDto.reliability, communication: ratingDto.communication, @@ -100,8 +100,11 @@ export class UserService { }); // Update the cache - const avgRatings = await this.calculateAvgRating(ratedUserId); - await this.cache.set(`avg-ratings-${ratedUserId}`, avgRatings); + const avgRatings = await this.calculateAvgRating(postedToId); + await this.cache.set( + `avg-ratings-${postedToId}`, + JSON.stringify(avgRatings), + ); return rating; } @@ -110,15 +113,15 @@ export class UserService { if (self) revieweeUserId = user.id; const ratings = await this.prisma.rating.findMany({ - where: { ratedUserId: revieweeUserId }, + where: { postedToId: revieweeUserId }, include: { - raterUser: true, + postedBy: true, }, }); return ratings.map((review) => ({ - userName: review.raterUser ? review.raterUser.name : 'Anonymous', - profilePictureUrl: review.raterUser?.profilePictureUrl, + userName: review.postedBy ? review.postedBy.name : 'Anonymous', + profilePictureUrl: review.postedBy?.profilePictureUrl, professionalism: review.professionalism, reliability: review.reliability, communication: review.communication, @@ -127,7 +130,7 @@ export class UserService { })); } - async searchUsers(searchTerm?: string) { + async searchUsers(userId: User['id'], searchTerm?: string) { if (!searchTerm) { throw new BadRequestException('Search term is required'); } @@ -142,10 +145,29 @@ export class UserService { id: true, email: true, name: true, + joinedAt: true, + isEmailVerified: true, + jobTitle: true, profilePictureUrl: true, + followings: { + where: { + followerId: userId, + }, + }, + _count: { + select: { + followings: true, + ratingsReceived: true, + }, + }, }, }); - return users; + return users.map(({ _count, followings, ...user }) => ({ + connectionsCount: _count.followings, + ratingsCount: _count.ratingsReceived, + isConnection: followings.length == 0, + ...user, + })); } async linkSocialAccount( @@ -194,7 +216,7 @@ export class UserService { const avgRatings = await this.calculateAvgRating(userId); // Cache the ratings for 24 hours - await this.cache.set(`avg-ratings-${userId}`, avgRatings); + await this.cache.set(`avg-ratings-${userId}`, JSON.stringify(avgRatings)); return avgRatings; } @@ -213,7 +235,7 @@ export class UserService { private async calculateAvgRating(userId: User['id']) { const result = await this.prisma.rating.aggregate({ - where: { ratedUserId: userId }, + where: { postedToId: userId }, _avg: { professionalism: true, reliability: true, @@ -221,7 +243,7 @@ export class UserService { }, }); - return JSON.stringify({ + return { professionalism: result._avg.professionalism ?? 0, reliability: result._avg.reliability ?? 0, communication: result._avg.communication ?? 0, @@ -230,6 +252,6 @@ export class UserService { result._avg.reliability + result._avg.communication) / 3, - }); + }; } } diff --git a/src/user/user.e2e.spec.ts b/src/user/user.e2e.spec.ts index 190e7cd..dceadc2 100644 --- a/src/user/user.e2e.spec.ts +++ b/src/user/user.e2e.spec.ts @@ -134,7 +134,7 @@ describe('User Controller Tests', () => { await prisma.rating.createMany({ data: [ { - ratedUserId: '2', + postedToId: '2', raterUserId: '1', professionalism: 5, reliability: 5, @@ -142,7 +142,7 @@ describe('User Controller Tests', () => { comment: 'Something', }, { - ratedUserId: '2', + postedToId: '2', raterUserId: '1', professionalism: 5, reliability: 5, @@ -153,7 +153,7 @@ describe('User Controller Tests', () => { await prisma.rating.create({ data: { - ratedUserId: '1', + postedToId: '1', raterUserId: '2', professionalism: 5, reliability: 5, @@ -295,7 +295,7 @@ describe('User Controller Tests', () => { }); expect(response.statusCode).toBe(201); - expect(response.json().ratedUserId).toBe('2'); + expect(response.json().postedToId).toBe('2'); expect(response.json().raterUserId).toBe('1'); expect(response.json().professionalism).toBe(5); expect(response.json().reliability).toBe(5); @@ -307,7 +307,7 @@ describe('User Controller Tests', () => { }, }); expect(rating).toBeDefined(); - expect(rating.ratedUserId).toBe('2'); + expect(rating.postedToId).toBe('2'); expect(rating.raterUserId).toBe('1'); expect(rating.professionalism).toBe(5); expect(rating.reliability).toBe(5); @@ -330,7 +330,7 @@ describe('User Controller Tests', () => { }); expect(response.statusCode).toBe(201); - expect(response.json().ratedUserId).toBe('2'); + expect(response.json().postedToId).toBe('2'); expect(response.json().raterUserId).toBe(null); expect(response.json().professionalism).toBe(5); expect(response.json().reliability).toBe(5); @@ -343,7 +343,7 @@ describe('User Controller Tests', () => { }, }); expect(rating).toBeDefined(); - expect(rating.ratedUserId).toBe('2'); + expect(rating.postedToId).toBe('2'); expect(rating.raterUserId).toBe(null); expect(rating.professionalism).toBe(5); expect(rating.reliability).toBe(5);