From f73815ac74120295bbe05899b43c8ccde83fd2c8 Mon Sep 17 00:00:00 2001 From: Lamarcke Date: Fri, 1 Mar 2024 09:21:54 -0300 Subject: [PATCH 1/5] - follower remove system --- src/follow/dto/follow-remove.dto.ts | 3 ++ src/follow/follow.controller.ts | 44 +++++++++++++++++++++-------- src/follow/follow.service.ts | 12 ++++++++ 3 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/follow/dto/follow-remove.dto.ts diff --git a/src/follow/dto/follow-remove.dto.ts b/src/follow/dto/follow-remove.dto.ts new file mode 100644 index 0000000..6e0aa5c --- /dev/null +++ b/src/follow/dto/follow-remove.dto.ts @@ -0,0 +1,3 @@ +import { FollowRegisterDto } from "./follow-register.dto"; + +export class FollowRemoveDto extends FollowRegisterDto {} diff --git a/src/follow/follow.controller.ts b/src/follow/follow.controller.ts index df32cf0..a253f80 100644 --- a/src/follow/follow.controller.ts +++ b/src/follow/follow.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, Post, Query, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + Delete, + 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"; @@ -6,6 +14,8 @@ 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"; +import { FollowRemoveDto } from "./dto/follow-remove.dto"; +import { Public } from "../auth/public.decorator"; @Controller("follow") @ApiTags("follow") @@ -13,22 +23,12 @@ import { FollowStatusDto } from "./dto/follow-status.dto"; 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, }) + @Public() async getFollowerStatus( @Query("followerUserId") followerUserId: string, @Query("followedUserId") followedUserId: string, @@ -37,7 +37,27 @@ export class FollowController { } @Get("count") + @Public() async getFollowersCount(@Query("targetUserId") targetUserId: string) { return await this.followService.getFollowersCount(targetUserId); } + + @Post() + async registerFollow( + @Session() session: SessionContainer, + @Body() dto: FollowRegisterDto, + ) { + return await this.followService.registerFollow( + session.getUserId(), + dto.followedUserId, + ); + } + + @Delete() + async removeFollow( + @Session() session: SessionContainer, + @Body() dto: FollowRemoveDto, + ) { + return await this.followService.removeFollow(session.getUserId(), dto); + } } diff --git a/src/follow/follow.service.ts b/src/follow/follow.service.ts index 2c3054c..01efdd8 100644 --- a/src/follow/follow.service.ts +++ b/src/follow/follow.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from "@nestjs/typeorm"; import { UserFollow } from "./entity/user-follow.entity"; import { Repository } from "typeorm"; import { FollowStatusDto } from "./dto/follow-status.dto"; +import { FollowRemoveDto } from "./dto/follow-remove.dto"; @Injectable() export class FollowService { @@ -85,4 +86,15 @@ export class FollowService { }, }); } + + async removeFollow(userId: string, dto: FollowRemoveDto) { + await this.userFollowRepository.delete({ + follower: { + userId, + }, + followed: { + userId: dto.followedUserId, + }, + }); + } } From f8134302d65d1dadcccafda8aebc6d94b595d0b1 Mon Sep 17 00:00:00 2001 From: Lamarcke Date: Fri, 1 Mar 2024 10:00:26 -0300 Subject: [PATCH 2/5] - testing fixes --- .../activities-feed.controller.spec.ts | 12 ++++++- .../activities-feed.service.ts | 14 ++++---- .../collections-entries.service.spec.ts | 33 ------------------ src/follow/follow.controller.spec.ts | 33 +++++++++++------- src/follow/follow.controller.ts | 5 ++- src/follow/follow.service.spec.ts | 34 +++++++++++++++++++ src/follow/follow.service.ts | 26 +++++++++----- src/profile/profile.service.spec.ts | 8 ++++- src/statistics/statistics.controller.spec.ts | 6 ++++ tsconfig.json | 1 + 10 files changed, 107 insertions(+), 65 deletions(-) diff --git a/src/activities/activities-feed/activities-feed.controller.spec.ts b/src/activities/activities-feed/activities-feed.controller.spec.ts index 32072d8..89bf531 100755 --- a/src/activities/activities-feed/activities-feed.controller.spec.ts +++ b/src/activities/activities-feed/activities-feed.controller.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from "@nestjs/testing"; import { ActivitiesFeedController } from "./activities-feed.controller"; import { ActivitiesFeedService } from "./activities-feed.service"; +import { CACHE_MANAGER } from "@nestjs/cache-manager"; describe("ActivitiesFeedController", () => { let controller: ActivitiesFeedController; @@ -8,7 +9,16 @@ describe("ActivitiesFeedController", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ActivitiesFeedController], - providers: [ActivitiesFeedService], + providers: [ + { + provide: ActivitiesFeedService, + useValue: {}, + }, + { + provide: CACHE_MANAGER, + useValue: {}, + }, + ], }).compile(); controller = module.get( diff --git a/src/activities/activities-feed/activities-feed.service.ts b/src/activities/activities-feed/activities-feed.service.ts index fdc6bf8..c9aef8d 100755 --- a/src/activities/activities-feed/activities-feed.service.ts +++ b/src/activities/activities-feed/activities-feed.service.ts @@ -1,11 +1,4 @@ import { HttpException, HttpStatus, Inject, Injectable } from "@nestjs/common"; -import { CollectionsEntriesService } from "src/collections/collections-entries/collections-entries.service"; -import { ReviewsService } from "src/reviews/reviews.service"; -import { - ActivityCriteria, - ActivityType, -} from "../activities-queue/activities-queue.constants"; -import { ProfileService } from "src/profile/profile.service"; import { Activity } from "../activities-repository/entities/activity.entity"; import { ActivitiesFeedRequestDto } from "./dto/activities-feed-request.dto"; import { ActivitiesRepositoryService } from "../activities-repository/activities-repository.service"; @@ -13,6 +6,13 @@ import { CACHE_MANAGER } from "@nestjs/cache-manager"; import { Cache } from "cache-manager"; import { TPaginationData } from "../../utils/pagination/pagination-response.dto"; import { StatisticsService } from "../../statistics/statistics.service"; +import { ProfileService } from "../../profile/profile.service"; +import { ReviewsService } from "../../reviews/reviews.service"; +import { CollectionsEntriesService } from "../../collections/collections-entries/collections-entries.service"; +import { + ActivityCriteria, + ActivityType, +} from "../activities-queue/activities-queue.constants"; export const ACTIVITY_FEED_CACHE_KEY = "queue-feed"; diff --git a/src/collections/collections-entries/collections-entries.service.spec.ts b/src/collections/collections-entries/collections-entries.service.spec.ts index 9741465..f714d81 100755 --- a/src/collections/collections-entries/collections-entries.service.spec.ts +++ b/src/collections/collections-entries/collections-entries.service.spec.ts @@ -104,37 +104,4 @@ describe("CollectionsEntriesService", () => { }), ); }); - - it("should attach a review when re-creating a entry", async () => { - const userId = "1"; - const dto: CreateCollectionEntryDto = { - isFavorite: true, - gameId: 1942, - collectionIds: ["111111"], - platformIds: [EGamePlatformIds.PC.valueOf()], - }; - const reviewFindSpy = jest.spyOn( - reviewService, - "findOneByUserIdAndGameId", - ); - reviewFindSpy.mockImplementation(async () => { - return { id: "review12345" } as Review; - }); - jest.spyOn(repository, "save").mockImplementationOnce(async () => { - return { id: "12345" } as CollectionEntry; - }); - - const createSpy = jest.spyOn(service, "createOrUpdate"); - const repositorySaveSpy = jest.spyOn(repository, "save"); - await service.createOrUpdate(userId, dto); - expect(reviewFindSpy).toBeCalled(); - expect(createSpy).toHaveReturned(); - expect(repositorySaveSpy).toBeCalledWith( - expect.objectContaining({ - review: { - id: "review12345", - }, - }), - ); - }); }); diff --git a/src/follow/follow.controller.spec.ts b/src/follow/follow.controller.spec.ts index 812b8a0..3cfb230 100644 --- a/src/follow/follow.controller.spec.ts +++ b/src/follow/follow.controller.spec.ts @@ -1,18 +1,25 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { FollowController } from './follow.controller'; +import { Test, TestingModule } from "@nestjs/testing"; +import { FollowController } from "./follow.controller"; +import { FollowService } from "./follow.service"; -describe('FollowController', () => { - let controller: FollowController; +describe("FollowController", () => { + let controller: FollowController; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [FollowController], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: FollowService, + useValue: {}, + }, + ], + controllers: [FollowController], + }).compile(); - controller = module.get(FollowController); - }); + controller = module.get(FollowController); + }); - it('should be defined', () => { - expect(controller).toBeDefined(); - }); + it("should be defined", () => { + expect(controller).toBeDefined(); + }); }); diff --git a/src/follow/follow.controller.ts b/src/follow/follow.controller.ts index a253f80..d7c8d81 100644 --- a/src/follow/follow.controller.ts +++ b/src/follow/follow.controller.ts @@ -58,6 +58,9 @@ export class FollowController { @Session() session: SessionContainer, @Body() dto: FollowRemoveDto, ) { - return await this.followService.removeFollow(session.getUserId(), dto); + return await this.followService.removeFollow( + session.getUserId(), + dto.followedUserId, + ); } } diff --git a/src/follow/follow.service.spec.ts b/src/follow/follow.service.spec.ts index 2ab5239..00ee8c1 100644 --- a/src/follow/follow.service.spec.ts +++ b/src/follow/follow.service.spec.ts @@ -2,9 +2,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"; +import { FollowRegisterDto } from "./dto/follow-register.dto"; +import Mocked = jest.Mocked; +import { Repository } from "typeorm"; +import { getRepositoryToken } from "@nestjs/typeorm"; describe("FollowService", () => { let service: FollowService; + let repository: Mocked>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -12,9 +17,38 @@ describe("FollowService", () => { }).compile(); service = module.get(FollowService); + repository = module.get(getRepositoryToken(UserFollow)); }); it("should be defined", () => { expect(service).toBeDefined(); }); + + it("should register follow", async () => { + const followerId = "54321"; + const followedId = "12345"; + await service.registerFollow(followerId, followedId); + expect(repository.save).toHaveBeenCalledWith({ + followed: { + userId: followedId, + }, + follower: { + userId: followerId, + }, + } as UserFollow); + }); + + it("should remove follow", async () => { + const followerId = "54321"; + const followedId = "12345"; + await service.removeFollow(followerId, followedId); + expect(repository.delete).toHaveBeenCalledWith({ + followed: { + userId: followedId, + }, + follower: { + userId: followerId, + }, + } as UserFollow); + }); }); diff --git a/src/follow/follow.service.ts b/src/follow/follow.service.ts index 01efdd8..08a6791 100644 --- a/src/follow/follow.service.ts +++ b/src/follow/follow.service.ts @@ -87,14 +87,22 @@ export class FollowService { }); } - async removeFollow(userId: string, dto: FollowRemoveDto) { - await this.userFollowRepository.delete({ - follower: { - userId, - }, - followed: { - userId: dto.followedUserId, - }, - }); + async removeFollow(followerUserId: string, followedUserId: string) { + try { + await this.userFollowRepository.delete({ + follower: { + userId: followerUserId, + }, + followed: { + userId: followedUserId, + }, + }); + } catch (e) { + console.error(e); + throw new HttpException( + "Error while removing follow", + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } diff --git a/src/profile/profile.service.spec.ts b/src/profile/profile.service.spec.ts index 9c30e1c..66560c0 100755 --- a/src/profile/profile.service.spec.ts +++ b/src/profile/profile.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from "@nestjs/testing"; import { ProfileService } from "./profile.service"; +import { getMockRepositoriesProviders } from "../../test/mocks/repositoryMocks"; +import { Profile } from "./entities/profile.entity"; +import { ProfileAvatar } from "./entities/profile-avatar.entity"; describe("ProfileService", () => { let service: ProfileService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ProfileService], + providers: [ + ProfileService, + ...getMockRepositoriesProviders([Profile, ProfileAvatar]), + ], }).compile(); service = module.get(ProfileService); diff --git a/src/statistics/statistics.controller.spec.ts b/src/statistics/statistics.controller.spec.ts index 781eff3..940a1a8 100644 --- a/src/statistics/statistics.controller.spec.ts +++ b/src/statistics/statistics.controller.spec.ts @@ -1,6 +1,8 @@ import { Test, TestingModule } from "@nestjs/testing"; import { StatisticsController } from "./statistics.controller"; import { StatisticsService } from "./statistics.service"; +import { CACHE_MANAGER, CacheInterceptor } from "@nestjs/cache-manager"; +import { PaginationInterceptor } from "../interceptor/pagination.interceptor"; describe("StatisticsController", () => { let controller: StatisticsController; @@ -13,6 +15,10 @@ describe("StatisticsController", () => { provide: StatisticsService, useValue: {}, }, + { + provide: CACHE_MANAGER, + useValue: {}, + }, ], }).compile(); diff --git a/tsconfig.json b/tsconfig.json index 2d3fd9c..a07b4bb 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ESNext", + "esModuleInterop": true, "sourceMap": true, "outDir": "./dist", "baseUrl": "./", From 701b8ac1972cbe703c9e61a8a562f79a569b0e44 Mon Sep 17 00:00:00 2001 From: Lamarcke Date: Fri, 1 Mar 2024 10:53:00 -0300 Subject: [PATCH 3/5] - --- src/achievements/achievements.module.ts | 4 ++-- src/achievements/achievements.service.spec.ts | 8 ++++---- src/achievements/achievements.service.ts | 5 +++-- src/app.module.ts | 4 ++-- src/level/dto/create-level.dto.ts | 1 + src/level/dto/update-level.dto.ts | 4 ++++ .../entities/user-level.entity.ts | 2 +- .../level.constants.ts} | 0 .../level.controller.spec.ts} | 14 +++++++------- .../level.controller.ts} | 10 +++++----- src/level/level.module.ts | 13 +++++++++++++ .../level.service.spec.ts} | 8 ++++---- .../level.service.ts} | 4 ++-- src/user-init/user-init.module.ts | 9 ++------- src/user-init/user-init.service.spec.ts | 8 ++++---- src/user-init/user-init.service.ts | 4 ++-- src/user-level/dto/create-user-level.dto.ts | 1 - src/user-level/dto/update-user-level.dto.ts | 4 ---- src/user-level/user-level.module.ts | 13 ------------- 19 files changed, 56 insertions(+), 60 deletions(-) create mode 100644 src/level/dto/create-level.dto.ts create mode 100644 src/level/dto/update-level.dto.ts rename src/{user-level => level}/entities/user-level.entity.ts (93%) rename src/{user-level/user-level.constants.ts => level/level.constants.ts} (100%) rename src/{user-level/user-level.controller.spec.ts => level/level.controller.spec.ts} (50%) rename src/{user-level/user-level.controller.ts => level/level.controller.ts} (63%) create mode 100644 src/level/level.module.ts rename src/{user-level/user-level.service.spec.ts => level/level.service.spec.ts} (93%) rename src/{user-level/user-level.service.ts => level/level.service.ts} (97%) delete mode 100644 src/user-level/dto/create-user-level.dto.ts delete mode 100644 src/user-level/dto/update-user-level.dto.ts delete mode 100644 src/user-level/user-level.module.ts diff --git a/src/achievements/achievements.module.ts b/src/achievements/achievements.module.ts index 7365319..1a7b5d0 100644 --- a/src/achievements/achievements.module.ts +++ b/src/achievements/achievements.module.ts @@ -7,7 +7,7 @@ import { ObtainedAchievement } from "./entities/obtained-achievement.entity"; import { AchievementsQueueProcessor } from "./achievements-queue/achievements-queue.processor"; import { BullModule } from "@nestjs/bull"; import { ACHIEVEMENTS_QUEUE_NAME } from "./achievements-queue/achievements-queue.constants"; -import { UserLevelModule } from "../user-level/user-level.module"; +import { LevelModule } from "../level/level.module"; @Module({ imports: [ @@ -18,7 +18,7 @@ import { UserLevelModule } from "../user-level/user-level.module"; removeOnFail: false, }, }), - UserLevelModule, + LevelModule, ], controllers: [AchievementsController], providers: [ diff --git a/src/achievements/achievements.service.spec.ts b/src/achievements/achievements.service.spec.ts index 4e50a94..c185858 100644 --- a/src/achievements/achievements.service.spec.ts +++ b/src/achievements/achievements.service.spec.ts @@ -11,13 +11,13 @@ import { AchievementCategory } from "./achievements.constants"; import Mocked = jest.Mocked; import sleep from "../utils/sleep"; import { CollectionEntry } from "../collections/collections-entries/entities/collection-entry.entity"; -import { UserLevelService } from "../user-level/user-level.service"; +import { LevelService } from "../level/level.service"; describe("AchievementsService", () => { let service: AchievementsService; let dataSource: Mocked; let obtainedAchievementRepository: Mocked>; - let userLevelService: Mocked; + let userLevelService: Mocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -31,7 +31,7 @@ describe("AchievementsService", () => { }, }, { - provide: UserLevelService, + provide: LevelService, useValue: { increaseExp: jest.fn(), }, @@ -44,7 +44,7 @@ describe("AchievementsService", () => { obtainedAchievementRepository = module.get( getRepositoryToken(ObtainedAchievement), ); - userLevelService = module.get(UserLevelService); + userLevelService = module.get(LevelService); }); it("should be defined", () => { diff --git a/src/achievements/achievements.service.ts b/src/achievements/achievements.service.ts index 162384c..b180462 100644 --- a/src/achievements/achievements.service.ts +++ b/src/achievements/achievements.service.ts @@ -10,7 +10,7 @@ import { AchievementCategory } from "./achievements.constants"; import { Profile } from "../profile/entities/profile.entity"; import { GetAchievementsRequestDto } from "./dto/get-achievements-request.dto"; import { UpdateFeaturedObtainedAchievementDto } from "./dto/update-featured-obtained-achievement.dto"; -import { UserLevelService } from "../user-level/user-level.service"; +import { LevelService } from "../level/level.service"; function validateAchievements() { achievementsData.forEach((achievement, index, array) => { @@ -42,11 +42,12 @@ function validateAchievements() { @Injectable() export class AchievementsService { private readonly logger = new Logger(AchievementsService.name); + constructor( @InjectRepository(ObtainedAchievement) private obtainedAchievementsRepository: Repository, private dataSource: DataSource, - private userLevelService: UserLevelService, + private userLevelService: LevelService, ) { validateAchievements(); } diff --git a/src/app.module.ts b/src/app.module.ts index 4a6bf50..35c7d0c 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,7 +16,7 @@ import { StatisticsQueueModule } from "./statistics/statistics-queue/statistics- import { ActivitiesFeedModule } from "./activities/activities-feed/activities-feed.module"; import { seconds, ThrottlerModule } from "@nestjs/throttler"; import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis"; -import { UserLevelModule } from "./user-level/user-level.module"; +import { LevelModule } from "./level/level.module"; import { HealthModule } from "./health/health.module"; import { AchievementsModule } from "./achievements/achievements.module"; import { FollowModule } from "./follow/follow.module"; @@ -118,7 +118,7 @@ import { IgdbSyncModule } from "./sync/igdb/igdb-sync.module"; CollectionsModule, StatisticsModule, StatisticsQueueModule, - UserLevelModule, + LevelModule, HealthModule, AchievementsModule, FollowModule, diff --git a/src/level/dto/create-level.dto.ts b/src/level/dto/create-level.dto.ts new file mode 100644 index 0000000..5d28b2c --- /dev/null +++ b/src/level/dto/create-level.dto.ts @@ -0,0 +1 @@ +export class CreateLevelDto {} diff --git a/src/level/dto/update-level.dto.ts b/src/level/dto/update-level.dto.ts new file mode 100644 index 0000000..5d8ca73 --- /dev/null +++ b/src/level/dto/update-level.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateLevelDto } from "./create-level.dto"; + +export class UpdateLevelDto extends PartialType(CreateLevelDto) {} diff --git a/src/user-level/entities/user-level.entity.ts b/src/level/entities/user-level.entity.ts similarity index 93% rename from src/user-level/entities/user-level.entity.ts rename to src/level/entities/user-level.entity.ts index 797a14c..66452ba 100644 --- a/src/user-level/entities/user-level.entity.ts +++ b/src/level/entities/user-level.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; import { Profile } from "../../profile/entities/profile.entity"; -import { BASE_LEVEL_UP_COST } from "../user-level.constants"; +import { BASE_LEVEL_UP_COST } from "../level.constants"; @Entity() export class UserLevel { diff --git a/src/user-level/user-level.constants.ts b/src/level/level.constants.ts similarity index 100% rename from src/user-level/user-level.constants.ts rename to src/level/level.constants.ts diff --git a/src/user-level/user-level.controller.spec.ts b/src/level/level.controller.spec.ts similarity index 50% rename from src/user-level/user-level.controller.spec.ts rename to src/level/level.controller.spec.ts index c27abde..0f83952 100644 --- a/src/user-level/user-level.controller.spec.ts +++ b/src/level/level.controller.spec.ts @@ -1,19 +1,19 @@ import { Test, TestingModule } from "@nestjs/testing"; -import { UserLevelController } from "./user-level.controller"; -import { UserLevelService } from "./user-level.service"; +import { LevelController } from "./level.controller"; +import { LevelService } from "./level.service"; import { getMockRepositoryProvider } from "../../test/mocks/repositoryMocks"; import { UserLevel } from "./entities/user-level.entity"; -describe("UserLevelController", () => { - let controller: UserLevelController; +describe("LevelController", () => { + let controller: LevelController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [UserLevelController], - providers: [UserLevelService, getMockRepositoryProvider(UserLevel)], + controllers: [LevelController], + providers: [LevelService, getMockRepositoryProvider(UserLevel)], }).compile(); - controller = module.get(UserLevelController); + controller = module.get(LevelController); }); it("should be defined", () => { diff --git a/src/user-level/user-level.controller.ts b/src/level/level.controller.ts similarity index 63% rename from src/user-level/user-level.controller.ts rename to src/level/level.controller.ts index a53788a..f834c96 100644 --- a/src/user-level/user-level.controller.ts +++ b/src/level/level.controller.ts @@ -1,12 +1,12 @@ import { Controller, Get, Param } from "@nestjs/common"; -import { UserLevelService } from "./user-level.service"; +import { LevelService } from "./level.service"; import { ApiOkResponse, ApiTags } from "@nestjs/swagger"; import { UserLevel } from "./entities/user-level.entity"; -@Controller("user/level") -@ApiTags("user-level") -export class UserLevelController { - constructor(private readonly userLevelService: UserLevelService) {} +@Controller("level") +@ApiTags("level") +export class LevelController { + constructor(private readonly userLevelService: LevelService) {} @Get(":userId") @ApiOkResponse({ status: 200, type: UserLevel }) diff --git a/src/level/level.module.ts b/src/level/level.module.ts new file mode 100644 index 0000000..81981df --- /dev/null +++ b/src/level/level.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { LevelService } from "./level.service"; +import { LevelController } from "./level.controller"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { UserLevel } from "./entities/user-level.entity"; + +@Module({ + imports: [TypeOrmModule.forFeature([UserLevel])], + controllers: [LevelController], + providers: [LevelService], + exports: [LevelService], +}) +export class LevelModule {} diff --git a/src/user-level/user-level.service.spec.ts b/src/level/level.service.spec.ts similarity index 93% rename from src/user-level/user-level.service.spec.ts rename to src/level/level.service.spec.ts index fda1ba1..985c3a3 100644 --- a/src/user-level/user-level.service.spec.ts +++ b/src/level/level.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from "@nestjs/testing"; -import { UserLevelService } from "./user-level.service"; +import { LevelService } from "./level.service"; import { getMockRepositoryProvider } from "../../test/mocks/repositoryMocks"; import { UserLevel } from "./entities/user-level.entity"; import Mocked = jest.Mocked; @@ -20,15 +20,15 @@ function getMockUserLevel() { } describe("UserLevelService", () => { - let service: UserLevelService; + let service: LevelService; let repository: Mocked>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [getMockRepositoryProvider(UserLevel), UserLevelService], + providers: [getMockRepositoryProvider(UserLevel), LevelService], }).compile(); - service = module.get(UserLevelService); + service = module.get(LevelService); repository = module.get(getRepositoryToken(UserLevel)); }); diff --git a/src/user-level/user-level.service.ts b/src/level/level.service.ts similarity index 97% rename from src/user-level/user-level.service.ts rename to src/level/level.service.ts index fc24046..057a8b0 100644 --- a/src/user-level/user-level.service.ts +++ b/src/level/level.service.ts @@ -2,10 +2,10 @@ import { HttpException, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { UserLevel } from "./entities/user-level.entity"; import { Repository } from "typeorm"; -import { BASE_LEVEL_UP_COST } from "./user-level.constants"; +import { BASE_LEVEL_UP_COST } from "./level.constants"; @Injectable() -export class UserLevelService { +export class LevelService { private readonly currentMaximumLevel = 50; /** * The base amount to multiply the current user-level requirement when a user levels up diff --git a/src/user-init/user-init.module.ts b/src/user-init/user-init.module.ts index 5ed7196..1daf0c7 100755 --- a/src/user-init/user-init.module.ts +++ b/src/user-init/user-init.module.ts @@ -3,16 +3,11 @@ import { UserInitService } from "./user-init.service"; import { LibrariesModule } from "../libraries/libraries.module"; import { CollectionsModule } from "../collections/collections.module"; import { ProfileModule } from "../profile/profile.module"; -import { UserLevelModule } from "../user-level/user-level.module"; +import { LevelModule } from "../level/level.module"; @Module({ exports: [UserInitService], - imports: [ - LibrariesModule, - CollectionsModule, - ProfileModule, - UserLevelModule, - ], + imports: [LibrariesModule, CollectionsModule, ProfileModule, LevelModule], providers: [UserInitService], }) export class UserInitModule {} diff --git a/src/user-init/user-init.service.spec.ts b/src/user-init/user-init.service.spec.ts index 6a77afe..674c265 100755 --- a/src/user-init/user-init.service.spec.ts +++ b/src/user-init/user-init.service.spec.ts @@ -4,7 +4,7 @@ import { CollectionsService } from "../collections/collections.service"; import { LibrariesService } from "../libraries/libraries.service"; import Mocked = jest.Mocked; import { ProfileService } from "../profile/profile.service"; -import { UserLevelService } from "../user-level/user-level.service"; +import { LevelService } from "../level/level.service"; const mockUserId = "1234"; @@ -12,7 +12,7 @@ describe("UserInitService", () => { let service: UserInitService; let librariesService: Mocked; let profileService: Mocked; - let userLevelService: Mocked; + let userLevelService: Mocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -40,7 +40,7 @@ describe("UserInitService", () => { }, }, { - provide: UserLevelService, + provide: LevelService, useValue: { findOneByUserId: jest.fn(), findOneById: jest.fn(), @@ -53,7 +53,7 @@ describe("UserInitService", () => { service = module.get(UserInitService); librariesService = module.get(LibrariesService); profileService = module.get(ProfileService); - userLevelService = module.get(UserLevelService); + userLevelService = module.get(LevelService); }); it("should be defined", () => { diff --git a/src/user-init/user-init.service.ts b/src/user-init/user-init.service.ts index 0225ab7..b1c7674 100755 --- a/src/user-init/user-init.service.ts +++ b/src/user-init/user-init.service.ts @@ -5,7 +5,7 @@ import { ProfileService } from "../profile/profile.service"; // import UserRoles from "supertokens-node/recipe/userroles"; // import { EUserRoles } from "../../utils/constants"; import { DEFAULT_COLLECTIONS } from "../collections/collections.constants"; -import { UserLevelService } from "../user-level/user-level.service"; +import { LevelService } from "../level/level.service"; /** * This service is responsible for initializing data/entities required for usage when a user performs a login.
@@ -18,7 +18,7 @@ export class UserInitService { private collectionsService: CollectionsService, private librariesService: LibrariesService, private profileService: ProfileService, - private userLevelService: UserLevelService, + private userLevelService: LevelService, ) {} /** diff --git a/src/user-level/dto/create-user-level.dto.ts b/src/user-level/dto/create-user-level.dto.ts deleted file mode 100644 index d9f855f..0000000 --- a/src/user-level/dto/create-user-level.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateUserLevelDto {} diff --git a/src/user-level/dto/update-user-level.dto.ts b/src/user-level/dto/update-user-level.dto.ts deleted file mode 100644 index a97c9ac..0000000 --- a/src/user-level/dto/update-user-level.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateUserLevelDto } from "./create-user-level.dto"; - -export class UpdateUserLevelDto extends PartialType(CreateUserLevelDto) {} diff --git a/src/user-level/user-level.module.ts b/src/user-level/user-level.module.ts deleted file mode 100644 index 8d1d6bb..0000000 --- a/src/user-level/user-level.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from "@nestjs/common"; -import { UserLevelService } from "./user-level.service"; -import { UserLevelController } from "./user-level.controller"; -import { TypeOrmModule } from "@nestjs/typeorm"; -import { UserLevel } from "./entities/user-level.entity"; - -@Module({ - imports: [TypeOrmModule.forFeature([UserLevel])], - controllers: [UserLevelController], - providers: [UserLevelService], - exports: [UserLevelService], -}) -export class UserLevelModule {} From ee48aa1b744a55429e8b908e64f168e24046a3f1 Mon Sep 17 00:00:00 2001 From: Lamarcke Date: Fri, 1 Mar 2024 12:42:21 -0300 Subject: [PATCH 4/5] - Add UserRole validation logic to AuthGuard --- src/app.module.ts | 2 ++ src/auth/auth.guard.ts | 24 ++++++++++++++++- src/auth/roles.decorator.ts | 9 +++++++ src/notifications/notifications.module.ts | 4 +++ src/user-init/user-init.service.ts | 32 ++++++++++++++++++++--- 5 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/auth/roles.decorator.ts create mode 100644 src/notifications/notifications.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 35c7d0c..94e90a6 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -21,6 +21,7 @@ import { HealthModule } from "./health/health.module"; import { AchievementsModule } from "./achievements/achievements.module"; import { FollowModule } from "./follow/follow.module"; import { IgdbSyncModule } from "./sync/igdb/igdb-sync.module"; +import { NotificationsModule } from './notifications/notifications.module'; /** * IMPORTANT: For any package that uses the "ioredis" module internally, make sure to use "forRootAsync". @@ -122,6 +123,7 @@ import { IgdbSyncModule } from "./sync/igdb/igdb-sync.module"; HealthModule, AchievementsModule, FollowModule, + NotificationsModule, ], }) export class AppModule implements NestModule { diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index b1f718d..049c3b4 100755 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -3,6 +3,8 @@ import { Error as STError } from "supertokens-node"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { Reflector } from "@nestjs/core"; +import { SessionRequest } from "supertokens-node/lib/build/framework/express"; +import UserRoles from "supertokens-node/recipe/userroles"; /** * Default AuthGuard that checks for a valid session. @@ -25,11 +27,31 @@ export class AuthGuard implements CanActivate { "isPublic", context.getHandler(), ); - console.log("isPublic: ", isPublic); + + const requiredRoles = this.reflector.get( + "roles", + context.getHandler(), + ); // You can create an optional version of this by passing {sessionRequired: false} to verifySession await verifySession({ sessionRequired: !isPublic, + /** + * Override Supertokens validators to use UserRole validation logic. + * @param globalClaimValidators + */ + overrideGlobalClaimValidators: (globalClaimValidators) => { + const validators = [...globalClaimValidators]; + if (requiredRoles && requiredRoles.length > 0) { + for (const role of requiredRoles) { + validators.push( + UserRoles.UserRoleClaim.validators.includes(role), + ); + } + } + + return validators; + }, })(ctx.getRequest(), resp, (res) => { err = res; }); diff --git a/src/auth/roles.decorator.ts b/src/auth/roles.decorator.ts new file mode 100644 index 0000000..3a23071 --- /dev/null +++ b/src/auth/roles.decorator.ts @@ -0,0 +1,9 @@ +import { SetMetadata } from "@nestjs/common"; + +/** + * Decorator that enforces user roles to be verified by SuperTokens. + * MUST be used with the AuthGuard. + * @param roles + * @constructor + */ +export const Roles = (roles: string[]) => SetMetadata("roles", roles); diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts new file mode 100644 index 0000000..7fc2bae --- /dev/null +++ b/src/notifications/notifications.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class NotificationsModule {} diff --git a/src/user-init/user-init.service.ts b/src/user-init/user-init.service.ts index b1c7674..429b17e 100755 --- a/src/user-init/user-init.service.ts +++ b/src/user-init/user-init.service.ts @@ -2,8 +2,8 @@ import { Injectable, Logger } from "@nestjs/common"; import { CollectionsService } from "../collections/collections.service"; import { LibrariesService } from "../libraries/libraries.service"; import { ProfileService } from "../profile/profile.service"; -// import UserRoles from "supertokens-node/recipe/userroles"; -// import { EUserRoles } from "../../utils/constants"; +import UserRoles from "supertokens-node/recipe/userroles"; +import { EUserRoles } from "../utils/constants"; import { DEFAULT_COLLECTIONS } from "../collections/collections.constants"; import { LevelService } from "../level/level.service"; @@ -12,6 +12,7 @@ import { LevelService } from "../level/level.service"; */ @Injectable() export class UserInitService { + private readonly defaultTenantId = "public"; private logger = new Logger(UserInitService.name); constructor( @@ -19,7 +20,31 @@ export class UserInitService { private librariesService: LibrariesService, private profileService: ProfileService, private userLevelService: LevelService, - ) {} + ) { + this.createUserRoles(); + } + + private createUserRoles() { + for (const role of Object.values(EUserRoles)) { + UserRoles.createNewRoleOrAddPermissions(role, []) + .then() + .catch((e) => { + this.logger.error(e); + }); + } + } + + private async initUserRole(userId: string) { + try { + await UserRoles.addRoleToUser( + this.defaultTenantId, + userId, + EUserRoles.USER, + ); + } catch (e) { + console.error(e); + } + } /** * Initialize the user @@ -31,6 +56,7 @@ export class UserInitService { `Started init routine for userId ${userId} at ${new Date().toISOString()}`, ); const initPromises: Promise[] = [ + this.initUserRole(userId), this.initProfile(userId), this.initLibrary(userId), this.initLevel(userId), From 308eb17d633fd3a602f78f8a6311b49974dadce1 Mon Sep 17 00:00:00 2001 From: Lamarcke Date: Sat, 2 Mar 2024 03:10:51 -0300 Subject: [PATCH 5/5] - --- public/icons/nintendo.png | Bin 718 -> 913 bytes server_swagger.json | 2 +- src/follow/follow.service.ts | 6 ++++++ src/reviews/dto/find-all-reviews-by-id.dto.ts | 10 ++++++++++ src/reviews/reviews.controller.ts | 6 ++++++ src/statistics/statistics.constants.ts | 2 ++ 6 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/reviews/dto/find-all-reviews-by-id.dto.ts diff --git a/public/icons/nintendo.png b/public/icons/nintendo.png index 3f090b626745691229817389f2b960f116c4e986..9e49513c8657d6a02b09f507395939b576d57b3d 100644 GIT binary patch literal 913 zcmV;C18)3@P)pF zR;xjy(Xhe>73i3p3UYlkH98;TTCLWkDzoO#U79!^^Ub=P_|H@BvxEYcC~mg7uqJ&Y*0R+ z!SOT+=!jBi^__Br?RKlnAo2Ju{gEhzmLnd*s|Dg1s_<}jic_0Zj>oF5G{E2G= zcVb8}eX%6U<#H}a#;rL!=wM-Rm(10OBnj6ynM_z8mPLP%Yx(^c7hKl@5rPz?+7U>Zs1H6_0CEqIy+lpOESQ5- z8|2za#-?uWOYq;2Jj5lKK&2eQM>3~yn56AJp(^=8H-uY{UJG0D!TGG;LzT8G=F&`I3s|3o6{Ha?KpVSi;~~wJ3AYn5#1X2%~x;Z=R2ZxkEA)tt8^J` zn5Q8M&OF#>I7|>n3IuI>ds%*@P;V8Fq_!NkM_ zWKsE~t3pY16kio{r1{4$(6$Od_rC3>6fr^155H&zH#1J3}Q~;3*6^+#c zs*o!Q@(X6jymi@dQoF#rGo|^QX=2})wQ8he{{*UrP_aue6W+v|>@AO!LwME?We* zTuHhyW#?Sexmw=>_Dh8Gyj{tzP zuI^@e9x&aa%xKc%#DGa(o8#vz@b_kX4m`)I6z%;<@>AaIg>GlK-O@O?+{$Kb+s$bG zh->@0r88#QeVUVapkztXr}VF4VU8kqoz^6uby1zh|JHlYqUAq%*Q{m@3S9VWdB?Xm zY(>0}S(*MWzrpLm){<1z{#JV3!u$hgUIfoJ*wgUfug|XBXq%>cQ}*3v+}U`OQAp)G z$2+d%Fm7>;I*zvux1PQesQjiH&LYOKpYtZuVTprZLyFo>6JE*id^6bBT6KB*OXC@@ z&hA*>{e` zn)O_jRwcy|pN-GVTN7Ge|LjKM4_%M>4qxIqzKTBJ(oSaV``Lb5YtN37C#KW=-WBY9 zcH&IGbKIf4x7WH>(ZBeDPh diff --git a/server_swagger.json b/server_swagger.json index 5368fd9..b65a122 100644 --- a/server_swagger.json +++ b/server_swagger.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/v1/libraries":{"get":{"operationId":"LibrariesController_findOwn","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Library"}}}}},"tags":["libraries"]}},"/v1/libraries/{id}":{"get":{"operationId":"LibrariesController_findOneByIdWithPermissions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Library"}}}}},"tags":["libraries"]}},"/v1/collections/{id}":{"get":{"operationId":"CollectionsController_findOneByIdWithPermissions","summary":"","description":"Returns a collection which the user has access to\n\n(Either its own collection or a public one)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}}},"tags":["collections"]},"patch":{"operationId":"CollectionsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCollectionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["collections"]},"delete":{"operationId":"CollectionsController_delete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["collections"]}},"/v1/collections/library/{userId}":{"get":{"operationId":"CollectionsController_findAllByUserIdWithPermissions","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}}}}}},"tags":["collections"]}},"/v1/collections":{"post":{"operationId":"CollectionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCollectionDto"}}}},"responses":{"201":{"description":""}},"tags":["collections"]}},"/v1/reviews":{"post":{"operationId":"ReviewsController_createOrUpdate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReviewDto"}}}},"responses":{"201":{"description":""}},"tags":["reviews"]},"get":{"operationId":"ReviewsController_findOneByUserIdAndGameId","parameters":[{"name":"id","required":true,"in":"query","schema":{"type":"string"}},{"name":"gameId","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}}},"tags":["reviews"]}},"/v1/reviews/score":{"get":{"operationId":"ReviewsController_getScoreForGameId","parameters":[{"name":"gameId","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewScoreResponseDto"}}}}},"tags":["reviews"]}},"/v1/reviews/profile/{userId}":{"get":{"operationId":"ReviewsController_findAllByUserId","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindReviewPaginatedDto"}}}}},"tags":["reviews"]}},"/v1/reviews/game/{id}":{"get":{"operationId":"ReviewsController_findAllByGameId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindReviewPaginatedDto"}}}}},"tags":["reviews"]}},"/v1/reviews/{id}":{"get":{"operationId":"ReviewsController_findOneById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}}},"tags":["reviews"]},"delete":{"operationId":"ReviewsController_delete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["reviews"]}},"/v1/profile":{"patch":{"operationId":"ProfileController_update","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdateProfileDto"}}}},"responses":{"200":{"description":""}},"tags":["profile"]},"get":{"operationId":"ProfileController_findOwn","summary":"","description":"Used to access own profile","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}}},"tags":["profile"]}},"/v1/profile/{id}":{"get":{"operationId":"ProfileController_findOneById","summary":"","description":"Used to access other users' profiles","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}}},"tags":["profile"]}},"/v1/achievements":{"get":{"operationId":"AchievementsController_getAchievements","parameters":[{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedAchievementsResponseDto"}}}}},"tags":["achievements"]}},"/v1/achievements/obtained/{id}":{"get":{"operationId":"AchievementsController_getObtainedAchievement","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"targetUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObtainedAchievement"}}}}},"tags":["achievements"]}},"/v1/achievements/obtained":{"get":{"operationId":"AchievementsController_getAllObtainedAchievements","parameters":[{"name":"targetUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ObtainedAchievement"}}}}}},"tags":["achievements"]}},"/v1/achievements/featured":{"put":{"operationId":"AchievementsController_updateFeaturedObtainedAchievement","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeaturedObtainedAchievementDto"}}}},"responses":{"200":{"description":""}},"tags":["achievements"]}},"/v1/user/level/{userId}":{"get":{"operationId":"UserLevelController_findOne","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLevel"}}}}},"tags":["user-level"]}},"/v1/collections-entries":{"post":{"operationId":"CollectionsEntriesController_createOrUpdate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCollectionEntryDto"}}}},"responses":{"201":{"description":""}},"tags":["collections-entries"]}},"/v1/collections-entries/game/{id}":{"get":{"operationId":"CollectionsEntriesController_findOwnEntryByGameId","summary":"","description":"Returns a specific collection entry based on game ID","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntry"}}}},"400":{"description":"Invalid query"}},"tags":["collections-entries"]}},"/v1/collections-entries/{id}":{"delete":{"operationId":"CollectionsEntriesController_deleteOwnEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["collections-entries"]}},"/v1/collections-entries/game/{id}/favorite":{"post":{"operationId":"CollectionsEntriesController_changeFavoriteStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFavoriteStatusCollectionEntryDto"}}}},"responses":{"204":{"description":""}},"tags":["collections-entries"]}},"/v1/collections-entries/library/{id}":{"get":{"operationId":"CollectionsEntriesController_findAllByLibraryId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntriesPaginatedResponseDto"}}}}},"tags":["collections-entries"]}},"/v1/collections-entries/library/{id}/favorites":{"get":{"operationId":"CollectionsEntriesController_findFavoritesByLibraryId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntriesPaginatedResponseDto"}}}}},"tags":["collections-entries"]}},"/v1/collections-entries/collection/{id}":{"get":{"operationId":"CollectionsEntriesController_findAllByCollectionId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntriesPaginatedResponseDto"}}}}},"tags":["collections-entries"]}},"/v1/statistics":{"post":{"operationId":"StatisticsController_findOneBySourceIdAndType","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindOneStatisticsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["statistics"]}},"/v1/statistics/trending/games":{"post":{"operationId":"StatisticsController_findTrendingGames","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindStatisticsTrendingGamesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsPaginatedResponseDto"}}}}},"tags":["statistics"]}},"/v1/statistics/trending/reviews":{"post":{"operationId":"StatisticsController_findTrendingReviews","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindStatisticsTrendingReviewsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsPaginatedResponseDto"}}}}},"tags":["statistics"]}},"/v1/statistics/status":{"get":{"operationId":"StatisticsController_getStatus","parameters":[{"name":"statisticsId","required":true,"in":"query","schema":{"type":"number"}},{"name":"sourceType","required":true,"in":"query","schema":{"enum":["game","review"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsStatus"}}}}},"tags":["statistics"]}},"/v1/health":{"get":{"operationId":"HealthController_health","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["health"]}},"/v1/activities/feed":{"get":{"operationId":"ActivitiesFeedController_buildActivitiesFeed","parameters":[{"name":"criteria","required":true,"in":"query","schema":{"enum":["following","trending","latest"],"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivitiesFeedPaginatedResponseDto"}}}}},"tags":["activities-feed"]}},"/v1/sync/igdb":{"post":{"operationId":"IgdbSyncController_sync","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGameDto"}}}},"responses":{"201":{"description":""}},"tags":["sync-igdb"]}},"/v1/game/repository/resource":{"get":{"operationId":"GameRepositoryController_getResource","parameters":[{"name":"resourceName","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["game-repository"]}},"/v1/game/repository/platforms/icon":{"post":{"operationId":"GameRepositoryController_getIconNamesForPlatformAbbreviations","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IconNamesForPlatformRequestDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["game-repository"]}},"/v1/game/repository/{id}":{"post":{"operationId":"GameRepositoryController_findOneById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRepositoryFindOneDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}}}},"tags":["game-repository"]}},"/v1/game/repository":{"post":{"operationId":"GameRepositoryController_findAllByIds","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRepositoryFindAllDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}}}}},"tags":["game-repository"]}},"/v1/statistics/queue/like":{"post":{"operationId":"StatisticsQueueController_addLike","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsActionDto"}}}},"responses":{"201":{"description":""}},"tags":["statistics-queue"]},"delete":{"operationId":"StatisticsQueueController_removeLike","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsActionDto"}}}},"responses":{"200":{"description":""}},"tags":["statistics-queue"]}},"/v1/statistics/queue/view":{"post":{"operationId":"StatisticsQueueController_addView","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsActionDto"}}}},"responses":{"201":{"description":""}},"tags":["statistics-queue"]}},"/v1/follow":{"post":{"operationId":"FollowController_registerFollow","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRegisterDto"}}}},"responses":{"201":{"description":""}},"tags":["follow"]}},"/v1/follow/status":{"get":{"operationId":"FollowController_getFollowerStatus","parameters":[{"name":"followerUserId","required":true,"in":"query","schema":{"type":"string"}},{"name":"followedUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowStatusDto"}}}}},"tags":["follow"]}},"/v1/follow/count":{"get":{"operationId":"FollowController_getFollowersCount","parameters":[{"name":"targetUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["follow"]}}},"info":{"title":"GameNode API","description":"API docs for the videogame catalog system GameNode.

Built with love by the GameNode team.","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"schemas":{"Library":{"type":"object","properties":{"userId":{"type":"string","description":"Also used to share the library with other users.\n\nSame as SuperTokens' userId."},"collections":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["userId","collections","createdAt","updatedAt"]},"GameCover":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["game","id"]},"GameCollection":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"slug":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"checksum":{"type":"string"},"url":{"type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}},"required":["id","name","slug","createdAt","updatedAt","checksum","url","games"]},"GameAlternativeName":{"type":"object","properties":{"id":{"type":"number"},"comment":{"type":"string"},"name":{"type":"string"},"checksum":{"type":"string"},"game":{"$ref":"#/components/schemas/Game"}},"required":["id","game"]},"GameArtwork":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["game","id"]},"GameScreenshot":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["game","id"]},"GameLocalization":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["game","id","createdAt","updatedAt"]},"GameMode":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["game","id","createdAt","updatedAt"]},"GameGenre":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["games","id","createdAt","updatedAt"]},"GameTheme":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","createdAt","updatedAt"]},"GamePlayerPerspective":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["games","id","createdAt","updatedAt"]},"GameEngineLogo":{"type":"object","properties":{"engine":{"$ref":"#/components/schemas/GameEngine"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["engine","id"]},"GameCompanyLogo":{"type":"object","properties":{"company":{"$ref":"#/components/schemas/GameCompany"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["company","id"]},"GameCompany":{"type":"object","properties":{"id":{"type":"number"},"changeDate":{"format":"date-time","type":"string"},"changeDateCategory":{"type":"string"},"changedCompany":{"$ref":"#/components/schemas/GameCompany"},"checksum":{"type":"string"},"country":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"description":{"type":"string"},"logo":{"$ref":"#/components/schemas/GameCompanyLogo"},"name":{"type":"string"},"parent":{"$ref":"#/components/schemas/GameCompany"},"slug":{"type":"string"},"startDate":{"format":"date-time","type":"string"},"startDateCategory":{"type":"string"},"updatedAt":{"format":"date-time","type":"string"},"url":{"type":"string"}},"required":["id","createdAt","name","slug","updatedAt"]},"GamePlatform":{"type":"object","properties":{"id":{"type":"number"},"abbreviation":{"type":"string"},"alternative_name":{"type":"string"},"category":{"type":"number","enum":[1,2,3,4,5,6]},"checksum":{"type":"string"},"generation":{"type":"number"},"name":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"collectionEntries":{"type":"array","items":{"$ref":"#/components/schemas/CollectionEntry"}}},"required":["id","abbreviation","alternative_name","category","checksum","generation","name","createdAt","updatedAt","games","collectionEntries"]},"GameEngine":{"type":"object","properties":{"logo":{"$ref":"#/components/schemas/GameEngineLogo"},"companies":{"type":"array","items":{"$ref":"#/components/schemas/GameCompany"}},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/GamePlatform"}},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["logo","companies","platforms","games","id","createdAt","updatedAt"]},"GameKeyword":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["game","id","createdAt","updatedAt"]},"GameFranchise":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["games","id","createdAt","updatedAt"]},"GameExternalGame":{"type":"object","properties":{"id":{"type":"number"},"uid":{"type":"string","description":"Corresponds to the game id on the target source (see GameExternalGameCategory).\nIt's called uid, not uuid."},"category":{"type":"number","enum":[0,5,10,11,13,14,15,20,22,23,26,28,29,30,31,32,36,37,54,55]},"media":{"type":"number","enum":[1,2]},"checksum":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"year":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}},"required":["id","uid","createdAt","updatedAt","games"]},"GameInvolvedCompany":{"type":"object","properties":{"id":{"type":"number"},"checksum":{"type":"string"},"company":{"$ref":"#/components/schemas/GameCompany"},"companyId":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"developer":{"type":"boolean"},"porting":{"type":"boolean"},"publisher":{"type":"boolean"},"supporting":{"type":"boolean"},"updatedAt":{"format":"date-time","type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}},"required":["id","company","companyId","createdAt","developer","porting","publisher","supporting","updatedAt","games"]},"Game":{"type":"object","properties":{"id":{"type":"number","description":"Should be mapped to the IGDB ID of the game."},"name":{"type":"string"},"slug":{"type":"string"},"aggregatedRating":{"type":"number"},"aggregatedRatingCount":{"type":"number"},"category":{"enum":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14],"type":"number"},"status":{"enum":[0,2,3,4,5,6,7,8],"type":"number"},"summary":{"type":"string"},"storyline":{"type":"string"},"checksum":{"type":"string"},"url":{"type":"string"},"firstReleaseDate":{"format":"date-time","type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"dlcs":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"dlcOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expansions":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expansionOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expandedGames":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expandedGameOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"similarGames":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"similarGameOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remakes":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remakeOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remasters":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remasterOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"cover":{"$ref":"#/components/schemas/GameCover"},"collection":{"$ref":"#/components/schemas/GameCollection"},"alternativeNames":{"type":"array","items":{"$ref":"#/components/schemas/GameAlternativeName"}},"artworks":{"type":"array","items":{"$ref":"#/components/schemas/GameArtwork"}},"screenshots":{"type":"array","items":{"$ref":"#/components/schemas/GameScreenshot"}},"gameLocalizations":{"type":"array","items":{"$ref":"#/components/schemas/GameLocalization"}},"gameModes":{"type":"array","items":{"$ref":"#/components/schemas/GameMode"}},"genres":{"type":"array","items":{"$ref":"#/components/schemas/GameGenre"}},"themes":{"type":"array","items":{"$ref":"#/components/schemas/GameTheme"}},"playerPerspectives":{"type":"array","items":{"$ref":"#/components/schemas/GamePlayerPerspective"}},"gameEngines":{"type":"array","items":{"$ref":"#/components/schemas/GameEngine"}},"keywords":{"type":"array","items":{"$ref":"#/components/schemas/GameKeyword"}},"franchises":{"type":"array","items":{"$ref":"#/components/schemas/GameFranchise"}},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/GamePlatform"}},"externalGames":{"type":"array","items":{"$ref":"#/components/schemas/GameExternalGame"}},"involvedCompanies":{"type":"array","items":{"$ref":"#/components/schemas/GameInvolvedCompany"}},"source":{"type":"string","description":"Oh dear maintainer, please forgive me for using transient fields.","default":"MYSQL","enum":["MYSQL","MANTICORE"]}},"required":["id","name","slug","category","status","summary","storyline","checksum","url","firstReleaseDate","createdAt","updatedAt","involvedCompanies","source"]},"ProfileAvatar":{"type":"object","properties":{"id":{"type":"number"},"mimetype":{"type":"string"},"extension":{"type":"string"},"size":{"type":"number"},"filename":{"type":"string"},"encoding":{"type":"string"},"profile":{"$ref":"#/components/schemas/Profile"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","mimetype","extension","size","filename","encoding","profile","createdAt","updatedAt"]},"UserFollow":{"type":"object","properties":{"id":{"type":"number"},"follower":{"$ref":"#/components/schemas/Profile"},"followed":{"$ref":"#/components/schemas/Profile"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","follower","followed","createdAt","updatedAt"]},"Profile":{"type":"object","properties":{"userId":{"type":"string","description":"Shareable string ID\n\nSame as SuperTokens' userId."},"username":{"type":"string"},"bio":{"type":"string"},"avatar":{"$ref":"#/components/schemas/ProfileAvatar"},"followersCount":{"type":"number"},"followers":{"type":"array","items":{"$ref":"#/components/schemas/UserFollow"}},"following":{"type":"array","items":{"$ref":"#/components/schemas/UserFollow"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["userId","username","bio","avatar","followersCount","followers","following","createdAt","updatedAt"]},"Review":{"type":"object","properties":{"id":{"type":"string"},"content":{"type":"string"},"rating":{"type":"number"},"game":{"$ref":"#/components/schemas/Game"},"gameId":{"type":"number"},"profile":{"$ref":"#/components/schemas/Profile"},"profileUserId":{"type":"string"},"collectionEntry":{"$ref":"#/components/schemas/CollectionEntry"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","content","rating","game","gameId","profile","profileUserId","collectionEntry","createdAt","updatedAt"]},"CollectionEntry":{"type":"object","properties":{"id":{"type":"string"},"collections":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"game":{"$ref":"#/components/schemas/Game"},"gameId":{"type":"number"},"ownedPlatforms":{"description":"The platforms on which the user owns the game.","type":"array","items":{"$ref":"#/components/schemas/GamePlatform"}},"review":{"$ref":"#/components/schemas/Review"},"isFavorite":{"type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","collections","game","gameId","ownedPlatforms","review","isFavorite","createdAt","updatedAt"]},"Collection":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean"},"library":{"$ref":"#/components/schemas/Library"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/CollectionEntry"}},"isFeatured":{"type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","description","isPublic","library","entries","isFeatured","createdAt","updatedAt"]},"CreateCollectionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean","default":true},"isFeatured":{"type":"boolean","default":false}},"required":["name","isPublic","isFeatured"]},"UpdateCollectionDto":{"type":"object","properties":{}},"CreateReviewDto":{"type":"object","properties":{"gameId":{"type":"number"},"content":{"type":"string","minLength":3},"rating":{"type":"number","minimum":0,"maximum":5}},"required":["gameId","content","rating"]},"ReviewScoreDistribution":{"type":"object","properties":{"1":{"type":"number"},"2":{"type":"number"},"3":{"type":"number"},"4":{"type":"number"},"5":{"type":"number"},"total":{"type":"number","description":"Total number of reviews"}},"required":["1","2","3","4","5","total"]},"ReviewScoreResponseDto":{"type":"object","properties":{"median":{"type":"number"},"distribution":{"$ref":"#/components/schemas/ReviewScoreDistribution"}},"required":["median","distribution"]},"PaginationInfo":{"type":"object","properties":{"totalItems":{"type":"number","description":"Total number of items available for the current query"},"totalPages":{"type":"number","description":"Total number of pages available for the current query"},"hasNextPage":{"type":"boolean","description":"If this query allows for a next page"}},"required":["totalItems","totalPages","hasNextPage"]},"FindReviewPaginatedDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"UpdateProfileDto":{"type":"object","properties":{"username":{"type":"string","minLength":4,"maxLength":20},"avatar":{"type":"object"},"bio":{"type":"string","minLength":1,"maxLength":240}}},"AchievementDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"expGainAmount":{"type":"number"},"category":{"type":"number","enum":[0,1,2,3]}},"required":["id","name","description","expGainAmount","category"]},"PaginatedAchievementsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AchievementDto"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"ObtainedAchievement":{"type":"object","properties":{"id":{"type":"string"},"profile":{"$ref":"#/components/schemas/Profile"},"isFeatured":{"type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","profile","isFeatured","createdAt","updatedAt"]},"UpdateFeaturedObtainedAchievementDto":{"type":"object","properties":{"id":{"type":"string"},"isFeatured":{"type":"boolean"}},"required":["id","isFeatured"]},"UserLevel":{"type":"object","properties":{"userId":{"type":"string","description":"Should be the same as the profile's UserId"},"profile":{"$ref":"#/components/schemas/Profile"},"currentLevel":{"type":"number"},"currentLevelExp":{"type":"number","description":"XP in the current user-level"},"levelUpExpCost":{"type":"number","description":"Threshold XP to hit the next user-level"},"expMultiplier":{"type":"number","description":"The multiplier to apply to all exp gains"}},"required":["userId","profile","currentLevel","currentLevelExp","levelUpExpCost","expMultiplier"]},"CreateCollectionEntryDto":{"type":"object","properties":{"collectionIds":{"type":"array","items":{"type":"string"}},"gameId":{"type":"number"},"platformIds":{"type":"array","items":{"type":"number","enum":[6,7,8,9,48,167,11,12,49,169,130,170]}},"isFavorite":{"type":"boolean","default":false}},"required":["collectionIds","gameId","platformIds","isFavorite"]},"CreateFavoriteStatusCollectionEntryDto":{"type":"object","properties":{"isFavorite":{"type":"boolean","default":false}},"required":["isFavorite"]},"CollectionEntriesPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CollectionEntry"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"FindOneStatisticsDto":{"type":"object","properties":{"sourceId":{"oneOf":[{"type":"string"},{"type":"number"}]},"sourceType":{"type":"string","enum":["game","review"]}},"required":["sourceId","sourceType"]},"GameRepositoryFilterDto":{"type":"object","properties":{"status":{"type":"number","enum":[0,2,3,4,5,6,7,8]},"category":{"type":"number","enum":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]},"themes":{"type":"array","items":{"type":"number"}},"gameModes":{"type":"array","items":{"type":"number"}},"platforms":{"type":"array","items":{"type":"number"}},"genres":{"type":"array","items":{"type":"number"}},"search":{"type":"string"},"offset":{"type":"number","default":0},"limit":{"type":"number","default":20},"orderBy":{"type":"object"}}},"FindStatisticsTrendingGamesDto":{"type":"object","properties":{"criteria":{"$ref":"#/components/schemas/GameRepositoryFilterDto"},"period":{"type":"string","enum":["day","week","month","quarter","half_year","year"]},"search":{"type":"string"},"offset":{"type":"number","default":0},"limit":{"type":"number","default":20},"orderBy":{"type":"object"}},"required":["period"]},"UserView":{"type":"object","properties":{"id":{"type":"number"},"profile":{"$ref":"#/components/schemas/Profile"},"statistics":{"$ref":"#/components/schemas/Statistics"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","statistics","createdAt","updatedAt"]},"UserLike":{"type":"object","properties":{"id":{"type":"number"},"profile":{"$ref":"#/components/schemas/Profile"},"statistics":{"$ref":"#/components/schemas/Statistics"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","profile","statistics","createdAt","updatedAt"]},"Statistics":{"type":"object","properties":{"id":{"type":"number"},"sourceType":{"type":"string","enum":["game","review"]},"viewsCount":{"type":"number"},"likesCount":{"type":"number"},"views":{"type":"array","items":{"$ref":"#/components/schemas/UserView"}},"likes":{"type":"array","items":{"$ref":"#/components/schemas/UserLike"}},"game":{"$ref":"#/components/schemas/Game"},"gameId":{"type":"number"},"review":{"$ref":"#/components/schemas/Review"},"reviewId":{"type":"string"}},"required":["id","sourceType","viewsCount","likesCount","views","likes"]},"StatisticsPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Statistics"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"FindStatisticsTrendingReviewsDto":{"type":"object","properties":{"gameId":{"type":"number"},"period":{"type":"string","enum":["day","week","month","quarter","half_year","year"]},"search":{"type":"string"},"offset":{"type":"number","default":0},"limit":{"type":"number","default":20},"orderBy":{"type":"object"}},"required":["period"]},"StatisticsStatus":{"type":"object","properties":{"isLiked":{"type":"boolean"},"isViewed":{"type":"boolean"}},"required":["isLiked","isViewed"]},"Activity":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["REVIEW","FOLLOW","COLLECTION_ENTRY"]},"sourceId":{"type":"string"},"metadata":{"type":"object","nullable":true},"profile":{"description":"The associated profile with this Activity","allOf":[{"$ref":"#/components/schemas/Profile"}]},"profileUserId":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","type","sourceId","metadata","profile","profileUserId","createdAt","updatedAt"]},"ActivitiesFeedPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"CreateGameDto":{"type":"object","properties":{"games":{"type":"array","items":{"type":"object"}}},"required":["games"]},"IconNamesForPlatformRequestDto":{"type":"object","properties":{"platformAbbreviations":{"type":"array","items":{"type":"string"}}},"required":["platformAbbreviations"]},"GameRepositoryFindOneDto":{"type":"object","properties":{"relations":{"type":"object"}}},"GameRepositoryFindAllDto":{"type":"object","properties":{"gameIds":{"type":"array","items":{"type":"number"}},"relations":{"type":"object"}},"required":["gameIds"]},"StatisticsActionDto":{"type":"object","properties":{"sourceId":{"oneOf":[{"type":"string"},{"type":"number"}]},"sourceType":{"enum":["game","review"],"type":"string"}},"required":["sourceId","sourceType"]},"FollowRegisterDto":{"type":"object","properties":{"followedUserId":{"type":"string","minLength":36}},"required":["followedUserId"]},"FollowStatusDto":{"type":"object","properties":{"isFollowing":{"type":"boolean"}},"required":["isFollowing"]}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/v1/libraries":{"get":{"operationId":"LibrariesController_findOwn","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Library"}}}}},"tags":["libraries"]}},"/v1/libraries/{id}":{"get":{"operationId":"LibrariesController_findOneByIdWithPermissions","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Library"}}}}},"tags":["libraries"]}},"/v1/collections/{id}":{"get":{"operationId":"CollectionsController_findOneByIdWithPermissions","summary":"","description":"Returns a collection which the user has access to\n\n(Either its own collection or a public one)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}}},"tags":["collections"]},"patch":{"operationId":"CollectionsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCollectionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["collections"]},"delete":{"operationId":"CollectionsController_delete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["collections"]}},"/v1/collections/library/{userId}":{"get":{"operationId":"CollectionsController_findAllByUserIdWithPermissions","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}}}}}},"tags":["collections"]}},"/v1/collections":{"post":{"operationId":"CollectionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCollectionDto"}}}},"responses":{"201":{"description":""}},"tags":["collections"]}},"/v1/reviews":{"post":{"operationId":"ReviewsController_createOrUpdate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReviewDto"}}}},"responses":{"201":{"description":""}},"tags":["reviews"]},"get":{"operationId":"ReviewsController_findOneByUserIdAndGameId","parameters":[{"name":"id","required":true,"in":"query","schema":{"type":"string"}},{"name":"gameId","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}}},"tags":["reviews"]}},"/v1/reviews/all":{"post":{"operationId":"ReviewsController_findAllById","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindAllReviewsByIdDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Review"}}}}}},"tags":["reviews"]}},"/v1/reviews/score":{"get":{"operationId":"ReviewsController_getScoreForGameId","parameters":[{"name":"gameId","required":true,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewScoreResponseDto"}}}}},"tags":["reviews"]}},"/v1/reviews/profile/{userId}":{"get":{"operationId":"ReviewsController_findAllByUserId","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindReviewPaginatedDto"}}}}},"tags":["reviews"]}},"/v1/reviews/game/{id}":{"get":{"operationId":"ReviewsController_findAllByGameId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindReviewPaginatedDto"}}}}},"tags":["reviews"]}},"/v1/reviews/{id}":{"get":{"operationId":"ReviewsController_findOneById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}}},"tags":["reviews"]},"delete":{"operationId":"ReviewsController_delete","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["reviews"]}},"/v1/profile":{"patch":{"operationId":"ProfileController_update","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdateProfileDto"}}}},"responses":{"200":{"description":""}},"tags":["profile"]},"get":{"operationId":"ProfileController_findOwn","summary":"","description":"Used to access own profile","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}}},"tags":["profile"]}},"/v1/profile/{id}":{"get":{"operationId":"ProfileController_findOneById","summary":"","description":"Used to access other users' profiles","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}}},"tags":["profile"]}},"/v1/achievements":{"get":{"operationId":"AchievementsController_getAchievements","parameters":[{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedAchievementsResponseDto"}}}}},"tags":["achievements"]}},"/v1/achievements/obtained/{id}":{"get":{"operationId":"AchievementsController_getObtainedAchievement","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"targetUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObtainedAchievement"}}}}},"tags":["achievements"]}},"/v1/achievements/obtained":{"get":{"operationId":"AchievementsController_getAllObtainedAchievements","parameters":[{"name":"targetUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ObtainedAchievement"}}}}}},"tags":["achievements"]}},"/v1/achievements/featured":{"put":{"operationId":"AchievementsController_updateFeaturedObtainedAchievement","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeaturedObtainedAchievementDto"}}}},"responses":{"200":{"description":""}},"tags":["achievements"]}},"/v1/level/{userId}":{"get":{"operationId":"LevelController_findOne","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLevel"}}}}},"tags":["level"]}},"/v1/collections-entries":{"post":{"operationId":"CollectionsEntriesController_createOrUpdate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCollectionEntryDto"}}}},"responses":{"201":{"description":""}},"tags":["collections-entries"]}},"/v1/collections-entries/game/{id}":{"get":{"operationId":"CollectionsEntriesController_findOwnEntryByGameId","summary":"","description":"Returns a specific collection entry based on game ID","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntry"}}}},"400":{"description":"Invalid query"}},"tags":["collections-entries"]}},"/v1/collections-entries/{id}":{"delete":{"operationId":"CollectionsEntriesController_deleteOwnEntry","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["collections-entries"]}},"/v1/collections-entries/game/{id}/favorite":{"post":{"operationId":"CollectionsEntriesController_changeFavoriteStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFavoriteStatusCollectionEntryDto"}}}},"responses":{"204":{"description":""}},"tags":["collections-entries"]}},"/v1/collections-entries/library/{id}":{"get":{"operationId":"CollectionsEntriesController_findAllByLibraryId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntriesPaginatedResponseDto"}}}}},"tags":["collections-entries"]}},"/v1/collections-entries/library/{id}/favorites":{"get":{"operationId":"CollectionsEntriesController_findFavoritesByLibraryId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntriesPaginatedResponseDto"}}}}},"tags":["collections-entries"]}},"/v1/collections-entries/collection/{id}":{"get":{"operationId":"CollectionsEntriesController_findAllByCollectionId","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectionEntriesPaginatedResponseDto"}}}}},"tags":["collections-entries"]}},"/v1/statistics":{"post":{"operationId":"StatisticsController_findOneBySourceIdAndType","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindOneStatisticsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["statistics"]}},"/v1/statistics/trending/games":{"post":{"operationId":"StatisticsController_findTrendingGames","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindStatisticsTrendingGamesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsPaginatedResponseDto"}}}}},"tags":["statistics"]}},"/v1/statistics/trending/reviews":{"post":{"operationId":"StatisticsController_findTrendingReviews","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindStatisticsTrendingReviewsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsPaginatedResponseDto"}}}}},"tags":["statistics"]}},"/v1/statistics/status":{"get":{"operationId":"StatisticsController_getStatus","parameters":[{"name":"statisticsId","required":true,"in":"query","schema":{"type":"number"}},{"name":"sourceType","required":true,"in":"query","schema":{"enum":["game","review"],"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsStatus"}}}}},"tags":["statistics"]}},"/v1/health":{"get":{"operationId":"HealthController_health","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["health"]}},"/v1/activities/feed":{"get":{"operationId":"ActivitiesFeedController_buildActivitiesFeed","parameters":[{"name":"criteria","required":true,"in":"query","schema":{"enum":["following","trending","latest"],"type":"string"}},{"name":"offset","required":false,"in":"query","schema":{"default":0,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"default":20,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivitiesFeedPaginatedResponseDto"}}}}},"tags":["activities-feed"]}},"/v1/sync/igdb":{"post":{"operationId":"IgdbSyncController_sync","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGameDto"}}}},"responses":{"201":{"description":""}},"tags":["sync-igdb"]}},"/v1/game/repository/resource":{"get":{"operationId":"GameRepositoryController_getResource","parameters":[{"name":"resourceName","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["game-repository"]}},"/v1/game/repository/platforms/icon":{"post":{"operationId":"GameRepositoryController_getIconNamesForPlatformAbbreviations","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IconNamesForPlatformRequestDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["game-repository"]}},"/v1/game/repository/{id}":{"post":{"operationId":"GameRepositoryController_findOneById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRepositoryFindOneDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}}}},"tags":["game-repository"]}},"/v1/game/repository":{"post":{"operationId":"GameRepositoryController_findAllByIds","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameRepositoryFindAllDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}}}}},"tags":["game-repository"]}},"/v1/statistics/queue/like":{"post":{"operationId":"StatisticsQueueController_addLike","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsActionDto"}}}},"responses":{"201":{"description":""}},"tags":["statistics-queue"]},"delete":{"operationId":"StatisticsQueueController_removeLike","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsActionDto"}}}},"responses":{"200":{"description":""}},"tags":["statistics-queue"]}},"/v1/statistics/queue/view":{"post":{"operationId":"StatisticsQueueController_addView","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatisticsActionDto"}}}},"responses":{"201":{"description":""}},"tags":["statistics-queue"]}},"/v1/follow/status":{"get":{"operationId":"FollowController_getFollowerStatus","parameters":[{"name":"followerUserId","required":true,"in":"query","schema":{"type":"string"}},{"name":"followedUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowStatusDto"}}}}},"tags":["follow"]}},"/v1/follow/count":{"get":{"operationId":"FollowController_getFollowersCount","parameters":[{"name":"targetUserId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["follow"]}},"/v1/follow":{"post":{"operationId":"FollowController_registerFollow","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRegisterDto"}}}},"responses":{"201":{"description":""}},"tags":["follow"]},"delete":{"operationId":"FollowController_removeFollow","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRemoveDto"}}}},"responses":{"200":{"description":""}},"tags":["follow"]}}},"info":{"title":"GameNode API","description":"API docs for the videogame catalog system GameNode.

Built with love by the GameNode team.","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"schemas":{"Library":{"type":"object","properties":{"userId":{"type":"string","description":"Also used to share the library with other users.\n\nSame as SuperTokens' userId."},"collections":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["userId","collections","createdAt","updatedAt"]},"GameCover":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["game","id"]},"GameCollection":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"slug":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"checksum":{"type":"string"},"url":{"type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}},"required":["id","name","slug","createdAt","updatedAt","checksum","url","games"]},"GameAlternativeName":{"type":"object","properties":{"id":{"type":"number"},"comment":{"type":"string"},"name":{"type":"string"},"checksum":{"type":"string"},"game":{"$ref":"#/components/schemas/Game"}},"required":["id","game"]},"GameArtwork":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["game","id"]},"GameScreenshot":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["game","id"]},"GameLocalization":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["game","id","createdAt","updatedAt"]},"GameMode":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["game","id","createdAt","updatedAt"]},"GameGenre":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["games","id","createdAt","updatedAt"]},"GameTheme":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","createdAt","updatedAt"]},"GamePlayerPerspective":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["games","id","createdAt","updatedAt"]},"GameEngineLogo":{"type":"object","properties":{"engine":{"$ref":"#/components/schemas/GameEngine"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["engine","id"]},"GameCompanyLogo":{"type":"object","properties":{"company":{"$ref":"#/components/schemas/GameCompany"},"id":{"type":"number"},"alphaChannel":{"type":"boolean"},"animated":{"type":"boolean"},"height":{"type":"number"},"imageId":{"type":"string"},"url":{"type":"string"},"width":{"type":"number"},"checksum":{"type":"string"}},"required":["company","id"]},"GameCompany":{"type":"object","properties":{"id":{"type":"number"},"changeDate":{"format":"date-time","type":"string"},"changeDateCategory":{"type":"string"},"changedCompany":{"$ref":"#/components/schemas/GameCompany"},"checksum":{"type":"string"},"country":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"description":{"type":"string"},"logo":{"$ref":"#/components/schemas/GameCompanyLogo"},"name":{"type":"string"},"parent":{"$ref":"#/components/schemas/GameCompany"},"slug":{"type":"string"},"startDate":{"format":"date-time","type":"string"},"startDateCategory":{"type":"string"},"updatedAt":{"format":"date-time","type":"string"},"url":{"type":"string"}},"required":["id","createdAt","name","slug","updatedAt"]},"GamePlatform":{"type":"object","properties":{"id":{"type":"number"},"abbreviation":{"type":"string"},"alternative_name":{"type":"string"},"category":{"type":"number","enum":[1,2,3,4,5,6]},"checksum":{"type":"string"},"generation":{"type":"number"},"name":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"collectionEntries":{"type":"array","items":{"$ref":"#/components/schemas/CollectionEntry"}}},"required":["id","abbreviation","alternative_name","category","checksum","generation","name","createdAt","updatedAt","games","collectionEntries"]},"GameEngine":{"type":"object","properties":{"logo":{"$ref":"#/components/schemas/GameEngineLogo"},"companies":{"type":"array","items":{"$ref":"#/components/schemas/GameCompany"}},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/GamePlatform"}},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["logo","companies","platforms","games","id","createdAt","updatedAt"]},"GameKeyword":{"type":"object","properties":{"game":{"$ref":"#/components/schemas/Game"},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["game","id","createdAt","updatedAt"]},"GameFranchise":{"type":"object","properties":{"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"id":{"type":"number"},"checksum":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"url":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["games","id","createdAt","updatedAt"]},"GameExternalGame":{"type":"object","properties":{"id":{"type":"number"},"uid":{"type":"string","description":"Corresponds to the game id on the target source (see GameExternalGameCategory).\nIt's called uid, not uuid."},"category":{"type":"number","enum":[0,5,10,11,13,14,15,20,22,23,26,28,29,30,31,32,36,37,54,55]},"media":{"type":"number","enum":[1,2]},"checksum":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"year":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}},"required":["id","uid","createdAt","updatedAt","games"]},"GameInvolvedCompany":{"type":"object","properties":{"id":{"type":"number"},"checksum":{"type":"string"},"company":{"$ref":"#/components/schemas/GameCompany"},"companyId":{"type":"number"},"createdAt":{"format":"date-time","type":"string"},"developer":{"type":"boolean"},"porting":{"type":"boolean"},"publisher":{"type":"boolean"},"supporting":{"type":"boolean"},"updatedAt":{"format":"date-time","type":"string"},"games":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}},"required":["id","company","companyId","createdAt","developer","porting","publisher","supporting","updatedAt","games"]},"Game":{"type":"object","properties":{"id":{"type":"number","description":"Should be mapped to the IGDB ID of the game."},"name":{"type":"string"},"slug":{"type":"string"},"aggregatedRating":{"type":"number"},"aggregatedRatingCount":{"type":"number"},"category":{"enum":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14],"type":"number"},"status":{"enum":[0,2,3,4,5,6,7,8],"type":"number"},"summary":{"type":"string"},"storyline":{"type":"string"},"checksum":{"type":"string"},"url":{"type":"string"},"firstReleaseDate":{"format":"date-time","type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"dlcs":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"dlcOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expansions":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expansionOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expandedGames":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"expandedGameOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"similarGames":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"similarGameOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remakes":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remakeOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remasters":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"remasterOf":{"type":"array","items":{"$ref":"#/components/schemas/Game"}},"cover":{"$ref":"#/components/schemas/GameCover"},"collection":{"$ref":"#/components/schemas/GameCollection"},"alternativeNames":{"type":"array","items":{"$ref":"#/components/schemas/GameAlternativeName"}},"artworks":{"type":"array","items":{"$ref":"#/components/schemas/GameArtwork"}},"screenshots":{"type":"array","items":{"$ref":"#/components/schemas/GameScreenshot"}},"gameLocalizations":{"type":"array","items":{"$ref":"#/components/schemas/GameLocalization"}},"gameModes":{"type":"array","items":{"$ref":"#/components/schemas/GameMode"}},"genres":{"type":"array","items":{"$ref":"#/components/schemas/GameGenre"}},"themes":{"type":"array","items":{"$ref":"#/components/schemas/GameTheme"}},"playerPerspectives":{"type":"array","items":{"$ref":"#/components/schemas/GamePlayerPerspective"}},"gameEngines":{"type":"array","items":{"$ref":"#/components/schemas/GameEngine"}},"keywords":{"type":"array","items":{"$ref":"#/components/schemas/GameKeyword"}},"franchises":{"type":"array","items":{"$ref":"#/components/schemas/GameFranchise"}},"platforms":{"type":"array","items":{"$ref":"#/components/schemas/GamePlatform"}},"externalGames":{"type":"array","items":{"$ref":"#/components/schemas/GameExternalGame"}},"involvedCompanies":{"type":"array","items":{"$ref":"#/components/schemas/GameInvolvedCompany"}},"source":{"type":"string","description":"Oh dear maintainer, please forgive me for using transient fields.","default":"MYSQL","enum":["MYSQL","MANTICORE"]}},"required":["id","name","slug","category","status","summary","storyline","checksum","url","firstReleaseDate","createdAt","updatedAt","involvedCompanies","source"]},"ProfileAvatar":{"type":"object","properties":{"id":{"type":"number"},"mimetype":{"type":"string"},"extension":{"type":"string"},"size":{"type":"number"},"filename":{"type":"string"},"encoding":{"type":"string"},"profile":{"$ref":"#/components/schemas/Profile"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","mimetype","extension","size","filename","encoding","profile","createdAt","updatedAt"]},"UserFollow":{"type":"object","properties":{"id":{"type":"number"},"follower":{"$ref":"#/components/schemas/Profile"},"followed":{"$ref":"#/components/schemas/Profile"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","follower","followed","createdAt","updatedAt"]},"Profile":{"type":"object","properties":{"userId":{"type":"string","description":"Shareable string ID\n\nSame as SuperTokens' userId."},"username":{"type":"string"},"bio":{"type":"string"},"avatar":{"$ref":"#/components/schemas/ProfileAvatar"},"followersCount":{"type":"number"},"followers":{"type":"array","items":{"$ref":"#/components/schemas/UserFollow"}},"following":{"type":"array","items":{"$ref":"#/components/schemas/UserFollow"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["userId","username","bio","avatar","followersCount","followers","following","createdAt","updatedAt"]},"Review":{"type":"object","properties":{"id":{"type":"string"},"content":{"type":"string"},"rating":{"type":"number"},"game":{"$ref":"#/components/schemas/Game"},"gameId":{"type":"number"},"profile":{"$ref":"#/components/schemas/Profile"},"profileUserId":{"type":"string"},"collectionEntry":{"$ref":"#/components/schemas/CollectionEntry"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","content","rating","game","gameId","profile","profileUserId","collectionEntry","createdAt","updatedAt"]},"CollectionEntry":{"type":"object","properties":{"id":{"type":"string"},"collections":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"game":{"$ref":"#/components/schemas/Game"},"gameId":{"type":"number"},"ownedPlatforms":{"description":"The platforms on which the user owns the game.","type":"array","items":{"$ref":"#/components/schemas/GamePlatform"}},"review":{"$ref":"#/components/schemas/Review"},"isFavorite":{"type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","collections","game","gameId","ownedPlatforms","review","isFavorite","createdAt","updatedAt"]},"Collection":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean"},"library":{"$ref":"#/components/schemas/Library"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/CollectionEntry"}},"isFeatured":{"type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","description","isPublic","library","entries","isFeatured","createdAt","updatedAt"]},"CreateCollectionDto":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"isPublic":{"type":"boolean","default":true},"isFeatured":{"type":"boolean","default":false}},"required":["name","isPublic","isFeatured"]},"UpdateCollectionDto":{"type":"object","properties":{}},"CreateReviewDto":{"type":"object","properties":{"gameId":{"type":"number"},"content":{"type":"string","minLength":3},"rating":{"type":"number","minimum":0,"maximum":5}},"required":["gameId","content","rating"]},"FindAllReviewsByIdDto":{"type":"object","properties":{"reviewsIds":{"type":"array","items":{"type":"string"}}},"required":["reviewsIds"]},"ReviewScoreDistribution":{"type":"object","properties":{"1":{"type":"number"},"2":{"type":"number"},"3":{"type":"number"},"4":{"type":"number"},"5":{"type":"number"},"total":{"type":"number","description":"Total number of reviews"}},"required":["1","2","3","4","5","total"]},"ReviewScoreResponseDto":{"type":"object","properties":{"median":{"type":"number"},"distribution":{"$ref":"#/components/schemas/ReviewScoreDistribution"}},"required":["median","distribution"]},"PaginationInfo":{"type":"object","properties":{"totalItems":{"type":"number","description":"Total number of items available for the current query"},"totalPages":{"type":"number","description":"Total number of pages available for the current query"},"hasNextPage":{"type":"boolean","description":"If this query allows for a next page"}},"required":["totalItems","totalPages","hasNextPage"]},"FindReviewPaginatedDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Review"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"UpdateProfileDto":{"type":"object","properties":{"username":{"type":"string","minLength":4,"maxLength":20},"avatar":{"type":"object"},"bio":{"type":"string","minLength":1,"maxLength":240}}},"AchievementDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"expGainAmount":{"type":"number"},"category":{"type":"number","enum":[0,1,2,3]}},"required":["id","name","description","expGainAmount","category"]},"PaginatedAchievementsResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AchievementDto"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"ObtainedAchievement":{"type":"object","properties":{"id":{"type":"string"},"profile":{"$ref":"#/components/schemas/Profile"},"isFeatured":{"type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","profile","isFeatured","createdAt","updatedAt"]},"UpdateFeaturedObtainedAchievementDto":{"type":"object","properties":{"id":{"type":"string"},"isFeatured":{"type":"boolean"}},"required":["id","isFeatured"]},"UserLevel":{"type":"object","properties":{"userId":{"type":"string","description":"Should be the same as the profile's UserId"},"profile":{"$ref":"#/components/schemas/Profile"},"currentLevel":{"type":"number"},"currentLevelExp":{"type":"number","description":"XP in the current user-level"},"levelUpExpCost":{"type":"number","description":"Threshold XP to hit the next user-level"},"expMultiplier":{"type":"number","description":"The multiplier to apply to all exp gains"}},"required":["userId","profile","currentLevel","currentLevelExp","levelUpExpCost","expMultiplier"]},"CreateCollectionEntryDto":{"type":"object","properties":{"collectionIds":{"type":"array","items":{"type":"string"}},"gameId":{"type":"number"},"platformIds":{"type":"array","items":{"type":"number","enum":[6,7,8,9,48,167,11,12,49,169,130,170]}},"isFavorite":{"type":"boolean","default":false}},"required":["collectionIds","gameId","platformIds","isFavorite"]},"CreateFavoriteStatusCollectionEntryDto":{"type":"object","properties":{"isFavorite":{"type":"boolean","default":false}},"required":["isFavorite"]},"CollectionEntriesPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CollectionEntry"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"FindOneStatisticsDto":{"type":"object","properties":{"sourceId":{"oneOf":[{"type":"string"},{"type":"number"}]},"sourceType":{"type":"string","enum":["game","review"]}},"required":["sourceId","sourceType"]},"GameRepositoryFilterDto":{"type":"object","properties":{"status":{"type":"number","enum":[0,2,3,4,5,6,7,8]},"category":{"type":"number","enum":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]},"themes":{"type":"array","items":{"type":"number"}},"gameModes":{"type":"array","items":{"type":"number"}},"platforms":{"type":"array","items":{"type":"number"}},"genres":{"type":"array","items":{"type":"number"}},"search":{"type":"string"},"offset":{"type":"number","default":0},"limit":{"type":"number","default":20},"orderBy":{"type":"object"}}},"FindStatisticsTrendingGamesDto":{"type":"object","properties":{"criteria":{"$ref":"#/components/schemas/GameRepositoryFilterDto"},"period":{"type":"string","enum":["day","week","month","quarter","half_year","year","all"]},"search":{"type":"string"},"offset":{"type":"number","default":0},"limit":{"type":"number","default":20},"orderBy":{"type":"object"}},"required":["period"]},"UserView":{"type":"object","properties":{"id":{"type":"number"},"profile":{"$ref":"#/components/schemas/Profile"},"statistics":{"$ref":"#/components/schemas/Statistics"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","statistics","createdAt","updatedAt"]},"UserLike":{"type":"object","properties":{"id":{"type":"number"},"profile":{"$ref":"#/components/schemas/Profile"},"statistics":{"$ref":"#/components/schemas/Statistics"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","profile","statistics","createdAt","updatedAt"]},"Statistics":{"type":"object","properties":{"id":{"type":"number"},"sourceType":{"type":"string","enum":["game","review"]},"viewsCount":{"type":"number"},"likesCount":{"type":"number"},"views":{"type":"array","items":{"$ref":"#/components/schemas/UserView"}},"likes":{"type":"array","items":{"$ref":"#/components/schemas/UserLike"}},"game":{"$ref":"#/components/schemas/Game"},"gameId":{"type":"number"},"review":{"$ref":"#/components/schemas/Review"},"reviewId":{"type":"string"}},"required":["id","sourceType","viewsCount","likesCount","views","likes"]},"StatisticsPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Statistics"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"FindStatisticsTrendingReviewsDto":{"type":"object","properties":{"gameId":{"type":"number"},"period":{"type":"string","enum":["day","week","month","quarter","half_year","year","all"]},"search":{"type":"string"},"offset":{"type":"number","default":0},"limit":{"type":"number","default":20},"orderBy":{"type":"object"}},"required":["period"]},"StatisticsStatus":{"type":"object","properties":{"isLiked":{"type":"boolean"},"isViewed":{"type":"boolean"}},"required":["isLiked","isViewed"]},"Activity":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["REVIEW","FOLLOW","COLLECTION_ENTRY"]},"sourceId":{"type":"string"},"metadata":{"type":"object","nullable":true},"profile":{"description":"The associated profile with this Activity","allOf":[{"$ref":"#/components/schemas/Profile"}]},"profileUserId":{"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","type","sourceId","metadata","profile","profileUserId","createdAt","updatedAt"]},"ActivitiesFeedPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"CreateGameDto":{"type":"object","properties":{"games":{"type":"array","items":{"type":"object"}}},"required":["games"]},"IconNamesForPlatformRequestDto":{"type":"object","properties":{"platformAbbreviations":{"type":"array","items":{"type":"string"}}},"required":["platformAbbreviations"]},"GameRepositoryFindOneDto":{"type":"object","properties":{"relations":{"type":"object"}}},"GameRepositoryFindAllDto":{"type":"object","properties":{"gameIds":{"type":"array","items":{"type":"number"}},"relations":{"type":"object"}},"required":["gameIds"]},"StatisticsActionDto":{"type":"object","properties":{"sourceId":{"oneOf":[{"type":"string"},{"type":"number"}]},"sourceType":{"enum":["game","review"],"type":"string"}},"required":["sourceId","sourceType"]},"FollowStatusDto":{"type":"object","properties":{"isFollowing":{"type":"boolean"}},"required":["isFollowing"]},"FollowRegisterDto":{"type":"object","properties":{"followedUserId":{"type":"string","minLength":36}},"required":["followedUserId"]},"FollowRemoveDto":{"type":"object","properties":{"followedUserId":{"type":"string","minLength":36}},"required":["followedUserId"]}}}} \ No newline at end of file diff --git a/src/follow/follow.service.ts b/src/follow/follow.service.ts index 08a6791..9f9bede 100644 --- a/src/follow/follow.service.ts +++ b/src/follow/follow.service.ts @@ -88,6 +88,12 @@ export class FollowService { } async removeFollow(followerUserId: string, followedUserId: string) { + if (followerUserId === followedUserId) { + throw new HttpException( + "User can't unfollow itself", + HttpStatus.I_AM_A_TEAPOT, + ); + } try { await this.userFollowRepository.delete({ follower: { diff --git a/src/reviews/dto/find-all-reviews-by-id.dto.ts b/src/reviews/dto/find-all-reviews-by-id.dto.ts new file mode 100644 index 0000000..258ad61 --- /dev/null +++ b/src/reviews/dto/find-all-reviews-by-id.dto.ts @@ -0,0 +1,10 @@ +import { IsArray, IsNotEmpty, IsString } from "class-validator"; + +export class FindAllReviewsByIdDto { + @IsNotEmpty() + @IsArray() + @IsString({ + each: true, + }) + reviewsIds: string[]; +} diff --git a/src/reviews/reviews.controller.ts b/src/reviews/reviews.controller.ts index d0e60c4..b075a21 100755 --- a/src/reviews/reviews.controller.ts +++ b/src/reviews/reviews.controller.ts @@ -21,6 +21,7 @@ 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"; +import { FindAllReviewsByIdDto } from "./dto/find-all-reviews-by-id.dto"; @Controller("reviews") @ApiTags("reviews") @@ -39,6 +40,11 @@ export class ReviewsController { ); } + @Post("all") + async findAllById(@Body() dto: FindAllReviewsByIdDto) { + return this.reviewsService.findAllByIdIn(dto.reviewsIds); + } + @Get("/score") async getScoreForGameId(@Query() dto: ReviewScoreRequestDto) { return this.reviewsService.getScore(dto.gameId); diff --git a/src/statistics/statistics.constants.ts b/src/statistics/statistics.constants.ts index c58a5a9..fcd2dc5 100644 --- a/src/statistics/statistics.constants.ts +++ b/src/statistics/statistics.constants.ts @@ -17,6 +17,7 @@ export enum StatisticsPeriod { QUARTER = "quarter", HALF_YEAR = "half_year", YEAR = "year", + ALL = "all", } /** @@ -29,4 +30,5 @@ export const StatisticsPeriodToMinusDays = { [StatisticsPeriod.QUARTER]: 90, [StatisticsPeriod.HALF_YEAR]: 180, [StatisticsPeriod.YEAR]: 365, + [StatisticsPeriod.ALL]: 365 * 100, };