Skip to content

Commit

Permalink
feat: write-review-page (#71)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
adelinaenache authored May 19, 2024
1 parent 4f6b81a commit 2b4edfb
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 64 deletions.
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions src/prisma/migrations/20240509195830_fix_namings/migration.sql
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 0 additions & 16 deletions src/prisma/prisma.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
}
}
31 changes: 16 additions & 15 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions src/schemas/user.properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
};
29 changes: 20 additions & 9 deletions src/user/controller/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
Param,
Post,
Put,
Query,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { CurrentUser } from '../../decorators/current-user.decorator';
Expand All @@ -25,16 +27,18 @@ 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,
} from '../../schemas/review.properties';
// 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()
Expand Down Expand Up @@ -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' },
Expand All @@ -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')
Expand Down Expand Up @@ -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',
Expand All @@ -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);
}
}
56 changes: 39 additions & 17 deletions src/user/service/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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');
}

Expand All @@ -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,
Expand All @@ -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;
}
Expand All @@ -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,
Expand All @@ -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');
}
Expand All @@ -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(
Expand Down Expand Up @@ -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;
}
Expand All @@ -213,15 +235,15 @@ 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,
communication: true,
},
});

return JSON.stringify({
return {
professionalism: result._avg.professionalism ?? 0,
reliability: result._avg.reliability ?? 0,
communication: result._avg.communication ?? 0,
Expand All @@ -230,6 +252,6 @@ export class UserService {
result._avg.reliability +
result._avg.communication) /
3,
});
};
}
}
Loading

0 comments on commit 2b4edfb

Please sign in to comment.