Skip to content

Commit

Permalink
Merge pull request #27 from game-node-app/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Lamarcke authored Mar 1, 2024
2 parents 7bf4ca6 + 0a9dea4 commit 5a23bb8
Show file tree
Hide file tree
Showing 15 changed files with 252 additions and 9 deletions.
2 changes: 1 addition & 1 deletion server_swagger.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/follow/dto/follow-register.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsNotEmpty, IsString, Max, Min } from "class-validator";

export class FollowRegisterDto {
@IsNotEmpty()
@IsString()
@Min(36)
@Max(36)
followedUserId: string;
}
3 changes: 3 additions & 0 deletions src/follow/dto/follow-status.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class FollowStatusDto {
isFollowing: boolean;
}
6 changes: 5 additions & 1 deletion src/follow/entity/user-follow.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from "typeorm";
import { Profile } from "../../profile/entities/profile.entity";
Expand All @@ -14,7 +15,10 @@ export class UserFollow {
id: number;
@ManyToMany(() => Profile)
@JoinTable()
profile: Profile;
follower: Profile;
@ManyToMany(() => Profile)
@JoinTable()
followed: Profile;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
Expand Down
18 changes: 18 additions & 0 deletions src/follow/follow.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FollowController } from './follow.controller';

describe('FollowController', () => {
let controller: FollowController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [FollowController],
}).compile();

controller = module.get<FollowController>(FollowController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
43 changes: 43 additions & 0 deletions src/follow/follow.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Body, Controller, Get, Post, Query, UseGuards } from "@nestjs/common";
import { AuthGuard } from "../auth/auth.guard";
import { FollowService } from "./follow.service";
import { FollowRegisterDto } from "./dto/follow-register.dto";
import { SessionContainer } from "supertokens-node/recipe/session";
import { Session } from "../auth/session.decorator";
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
import { FollowStatusDto } from "./dto/follow-status.dto";

@Controller("follow")
@ApiTags("follow")
@UseGuards(AuthGuard)
export class FollowController {
constructor(private followService: FollowService) {}

@Post()
async registerFollow(
@Session() session: SessionContainer,
@Body() dto: FollowRegisterDto,
) {
return await this.followService.registerFollow(
session.getUserId(),
dto.followedUserId,
);
}

@Get("status")
@ApiOkResponse({
status: 200,
type: FollowStatusDto,
})
async getFollowerStatus(
@Query("followerUserId") followerUserId: string,
@Query("followedUserId") followedUserId: string,
) {
return this.followService.getStatus(followerUserId, followedUserId);
}

@Get("count")
async getFollowersCount(@Query("targetUserId") targetUserId: string) {
return await this.followService.getFollowersCount(targetUserId);
}
}
2 changes: 2 additions & 0 deletions src/follow/follow.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Module } from "@nestjs/common";
import { FollowService } from "./follow.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserFollow } from "./entity/user-follow.entity";
import { FollowController } from './follow.controller';

@Module({
imports: [TypeOrmModule.forFeature([UserFollow])],
providers: [FollowService],
controllers: [FollowController],
})
export class FollowModule {}
4 changes: 3 additions & 1 deletion src/follow/follow.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Test, TestingModule } from "@nestjs/testing";
import { FollowService } from "./follow.service";
import { getMockRepositoryProvider } from "../../test/mocks/repositoryMocks";
import { UserFollow } from "./entity/user-follow.entity";

describe("FollowService", () => {
let service: FollowService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [FollowService],
providers: [FollowService, getMockRepositoryProvider(UserFollow)],
}).compile();

service = module.get<FollowService>(FollowService);
Expand Down
75 changes: 71 additions & 4 deletions src/follow/follow.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,88 @@
import { Injectable } from "@nestjs/common";
import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { UserFollow } from "./entity/user-follow.entity";
import { Repository } from "typeorm";
import { FollowStatusDto } from "./dto/follow-status.dto";

@Injectable()
export class FollowService {
private readonly logger = new Logger(FollowService.name);

constructor(
@InjectRepository(UserFollow)
private userFollowRepository: Repository<UserFollow>,
) {}

public async registerFollow(
followingUserId: string,
followerUserId: string,
followedUserId: string,
) {
if (followerUserId === followedUserId) {
throw new HttpException(
"User can't follow itself.",
HttpStatus.I_AM_A_TEAPOT,
);
}
try {
await this.userFollowRepository.save({});
} catch (e) {}
await this.userFollowRepository.save({
follower: {
userId: followerUserId,
},
followed: {
userId: followedUserId,
},
});
} catch (e) {
this.logger.error(e);
throw new HttpException(
"Error while registering user follow",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

public async getStatus(
followerUserId: string,
followedUserId: string,
): Promise<FollowStatusDto> {
const userIdLength = 36;
const params = [followerUserId, followedUserId] as const;
for (const param of params) {
if (typeof param !== "string" || param.length !== userIdLength) {
throw new HttpException(
"Malformed parameters.",
HttpStatus.BAD_REQUEST,
);
}
}

if (followerUserId === followedUserId) {
return {
isFollowing: false,
};
}

const exist = await this.userFollowRepository.exist({
where: {
follower: {
userId: followerUserId,
},
followed: {
userId: followedUserId,
},
},
});

return {
isFollowing: exist,
};
}

public async getFollowersCount(userId: string) {
return await this.userFollowRepository.countBy({
followed: {
userId,
},
});
}
}
4 changes: 2 additions & 2 deletions src/profile/entities/profile.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export class Profile {
default: 0,
})
followersCount: number;
@ManyToMany(() => UserFollow, (userFollow) => userFollow.profile)
@ManyToMany(() => UserFollow, (userFollow) => userFollow.follower)
followers: UserFollow[];
@ManyToMany(() => UserFollow, (userFollow) => userFollow.profile)
@ManyToMany(() => UserFollow, (userFollow) => userFollow.follower)
following: UserFollow[];
@CreateDateColumn()
createdAt: Date;
Expand Down
7 changes: 7 additions & 0 deletions src/reviews/dto/review-score-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsNumber } from "class-validator";

export class ReviewScoreRequestDto {
@IsNotEmpty()
@IsNumber()
gameId: number;
}
19 changes: 19 additions & 0 deletions src/reviews/dto/review-score-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Number of times a given review rating appears for a specific game
*/
export class ReviewScoreDistribution {
1: number;
2: number;
3: number;
4: number;
5: number;
/**
* Total number of reviews
*/
total: number;
}

export class ReviewScoreResponseDto {
median: number;
distribution: ReviewScoreDistribution;
}
8 changes: 8 additions & 0 deletions src/reviews/reviews.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { FindReviewPaginatedDto } from "./dto/find-review-paginated.dto";
import { Review } from "./entities/review.entity";
import { FindReviewDto } from "./dto/find-review.dto";
import { Public } from "../auth/public.decorator";
import { ReviewScoreRequestDto } from "./dto/review-score-request.dto";

@Controller("reviews")
@ApiTags("reviews")
Expand All @@ -38,6 +39,11 @@ export class ReviewsController {
);
}

@Get("/score")
async getScoreForGameId(@Query() dto: ReviewScoreRequestDto) {
return this.reviewsService.getScore(dto.gameId);
}

@Get("profile/:userId")
@UseInterceptors(PaginationInterceptor)
@ApiOkResponse({
Expand Down Expand Up @@ -67,6 +73,7 @@ export class ReviewsController {
}

@Get()
@Public()
@ApiOkResponse({
type: Review,
status: 200,
Expand All @@ -82,6 +89,7 @@ export class ReviewsController {
}

@Get(":id")
@Public()
async findOneById(@Param("id") id: string) {
return this.reviewsService.findOneByIdOrFail(id);
}
Expand Down
60 changes: 60 additions & 0 deletions src/reviews/reviews.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { AchievementsQueueService } from "../achievements/achievements-queue/ach
import { AchievementCategory } from "../achievements/achievements.constants";
import { StatisticsService } from "../statistics/statistics.service";
import { StatisticsSourceType } from "../statistics/statistics.constants";
import {
ReviewScoreDistribution,
ReviewScoreResponseDto,
} from "./dto/review-score-response.dto";

export class ReviewsService {
private readonly logger = new Logger(ReviewsService.name);
Expand Down Expand Up @@ -100,6 +104,62 @@ export class ReviewsService {
});
}

private getReviewsScoreDistribution(
reviews: Review[],
): ReviewScoreDistribution {
const distribution: ReviewScoreDistribution = {
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
total: reviews.length,
};
for (const num of [1, 2, 3, 4, 5] as const) {
const items = reviews
.filter(
(review) => review != undefined && review.rating === num,
)
.map((review) => review.rating);
distribution[num] = items.length;
}

return distribution;
}

private getScoresMedian(scores: number[]) {
if (scores.length === 0) {
return 0;
} else if (scores.length === 1) {
return scores[0];
}
const sortedScores = scores.toSorted((a, b) => a - b);
const middleIndex = Math.ceil(sortedScores.length / 2);
if (sortedScores.length % 2) {
return (
(sortedScores[middleIndex - 1] + sortedScores[middleIndex]) / 2
);
}

return sortedScores[middleIndex];
}

async getScore(gameId: number): Promise<ReviewScoreResponseDto> {
const reviews = await this.reviewsRepository.findBy({
gameId: gameId,
});
const scores = reviews.map((review) => {
return review.rating;
});
const median = this.getScoresMedian(scores);
const distribution = this.getReviewsScoreDistribution(reviews);
console.log(scores, median, distribution);
return {
median,
distribution,
};
}

async createOrUpdate(userId: string, createReviewDto: CreateReviewDto) {
const collectionEntry =
await this.collectionsEntriesService.findOneByUserIdAndGameIdOrFail(
Expand Down
1 change: 1 addition & 0 deletions src/statistics/statistics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export class StatisticsService {
likesCount: "DESC",
viewsCount: "DESC",
},
relationLoadStrategy: "query",
});
}

Expand Down

0 comments on commit 5a23bb8

Please sign in to comment.