diff --git a/.env.example b/.env.example
index c984446..79502c6 100755
--- a/.env.example
+++ b/.env.example
@@ -8,3 +8,4 @@ DOMAIN_WEBSITE=localhost:3000
SUPERTOKENS_CORE_URI=http://localhost:3567
REDIS_URL=redis://localhost:6379
SERVER_PORT=5000
+RABBITMQ_URI=amqp://rabbitmq:rabbitmq@localhost:5672
\ No newline at end of file
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 9baa4fd..af8f388 100755
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -1,47 +1,6 @@
version: '3'
services:
- supertokens-db:
- hostname: supertokens-db
- image: mysql:latest
- environment:
- MYSQL_ROOT_PASSWORD: ${SUPERTOKENS_DB_PASS}
- MYSQL_USER: ${SUPERTOKENS_DB_USER}
- MYSQL_PASSWORD: ${SUPERTOKENS_DB_PASS}
- MYSQL_DATABASE: supertokens
-
- networks:
- - game_node_app
- restart: always
- volumes:
- - supertokens-db:/var/lib/mysql
- healthcheck:
- test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
- timeout: 20s
- retries: 10
-
- supertokens:
- hostname: supertokens
- image: registry.supertokens.io/supertokens/supertokens-mysql:7.0
-
- depends_on:
- - supertokens-db
-
- environment:
- MYSQL_CONNECTION_URI: mysql://${SUPERTOKENS_DB_USER}:${SUPERTOKENS_DB_PASS}@supertokens-db:3306/supertokens
-
- networks:
- - game_node_app
- restart: always
-
- healthcheck:
- test: >
- bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"'
- interval: 10s
- timeout: 5s
- retries: 5
-
-
db:
image: mysql:latest
environment:
@@ -88,6 +47,7 @@ services:
DB_USER: ${DB_USER}
DB_PORT: 3306
SUPERTOKENS_CORE_URI: http://@supertokens:3567
+ RABBITMQ_URI: amqp://${RABBITMQ_USERNAME}:${RABBITMQ_PASSWORD}@rabbitmq:5672
REDIS_URL: redis://redis:6379
SERVER_PORT: 5000
DOMAIN_API: https://server.gamenode.app
@@ -120,5 +80,4 @@ volumes:
redis:
db:
gamenode:
- supertokens-db:
diff --git a/docker-compose.yml b/docker-compose.yml
index 72e999c..e3e679a 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -64,6 +64,21 @@ services:
networks:
- game_node_app
+ rabbitmq:
+ image: rabbitmq:3.13-management
+ ports:
+ # These will be available at your localhost:
+ - "5672:5672"
+ - "25672:25672" # (erlang) communication between the nodes and CLI tool
+ - "15672:15672" # communication with the web management API
+
+ volumes:
+ - rabbitmq:/var/lib/rabbitmq/
+
+ environment:
+ RABBITMQ_DEFAULT_USER: gamenode
+ RABBITMQ_DEFAULT_PASS: gamenode
+
networks:
game_node_app:
@@ -73,4 +88,5 @@ volumes:
redis:
db:
manticore:
+ rabbitmq:
diff --git a/package.json b/package.json
index 2ea2087..6e3abcc 100755
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
- "start:prod": "node --max-old-space-size=4096 dist/src/main",
+ "start:prod": "node --max-old-space-size=2048 dist/src/main",
"start:dev:docker": "docker compose up --build -d && yarn start:dev",
"typeorm": "typeorm-ts-node-commonjs",
"migration:generate": "yarn run typeorm migration:generate ./src/migrations/all -d ./data-source.ts",
@@ -25,9 +25,10 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
+ "@golevelup/nestjs-rabbitmq": "^5.1.0",
"@liaoliaots/nestjs-redis-health": "^9.0.4",
"@nestjs/axios": "^3.0.0",
- "@nestjs/bull": "^10.0.1",
+ "@nestjs/bullmq": "^10.1.0",
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.2.7",
"@nestjs/config": "^3.1.1",
@@ -45,7 +46,7 @@
"async-retry": "^1.3.3",
"axios": "^1.4.0",
"bad-words": "^3.0.4",
- "bull": "^4.10.4",
+ "bullmq": "^5.4.2",
"cache-manager": "^5.2.2",
"cache-manager-redis-yet": "^4.1.1",
"class-transformer": "^0.5.1",
diff --git a/server_swagger.json b/server_swagger.json
index 0dfab22..e905b91 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/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/notifications":{"get":{"operationId":"NotificationsController_findAllAndAggregate","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/PaginatedNotificationAggregationDto"}}}}},"tags":["notifications"]}},"/v1/notifications/{id}/view":{"put":{"operationId":"NotificationsController_updateViewedStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationViewUpdateDto"}}}},"responses":{"200":{"description":""}},"tags":["notifications"]}},"/v1/notifications/stream":{"get":{"operationId":"NotificationsController_stream","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["notifications"]}},"/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"]},"Notification":{"type":"object","properties":{"id":{"type":"number"},"sourceType":{"type":"string","enum":["game","review","activity","profile"]},"category":{"type":"string","description":"What this notification's about. E.g.: a new like, a new follower, a game launch, etc.","enum":["follow","like","comment","launch"]},"review":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Review"}]},"reviewId":{"type":"string","nullable":true},"game":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Game"}]},"gameId":{"type":"number","nullable":true},"activity":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Activity"}]},"activityId":{"type":"string","nullable":true},"profile":{"nullable":true,"description":"User responsible for generating this notification (e.g. user that liked a review).","allOf":[{"$ref":"#/components/schemas/Profile"}]},"profileUserId":{"type":"string","nullable":true,"description":"User responsible for generating this notification (e.g. user that liked a review).\nWhen null/undefined, the notification was generated by the 'system'."},"isViewed":{"type":"boolean"},"targetProfile":{"nullable":true,"description":"User which is the target for this notification. \nIf this is empty (null/undefined), the notification is targeted at all users. \nNot to be confused with the 'profile' property.","allOf":[{"$ref":"#/components/schemas/Profile"}]},"targetProfileUserId":{"type":"string","nullable":true,"description":"User which is the target for this notification. \nIf this is empty (null/undefined), the notification is targeted at all users. \nNot to be confused with the 'profile' property."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","sourceType","category","review","reviewId","game","gameId","activity","activityId","profile","profileUserId","isViewed","targetProfile","targetProfileUserId","createdAt","updatedAt"]},"NotificationAggregateDto":{"type":"object","properties":{"sourceId":{"oneOf":[{"type":"string"},{"type":"number"}]},"category":{"type":"string","enum":["follow","like","comment","launch"]},"sourceType":{"type":"string","enum":["game","review","activity","profile"]},"notifications":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}}},"required":["sourceId","category","sourceType","notifications"]},"PaginatedNotificationAggregationDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationAggregateDto"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"NotificationViewUpdateDto":{"type":"object","properties":{"isViewed":{"type":"boolean"}},"required":["isViewed"]},"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"}]},"targetUserId":{"type":"string","minLength":36},"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
+{"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/notifications":{"get":{"operationId":"NotificationsController_findAllAndAggregate","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/PaginatedNotificationAggregationDto"}}}}},"tags":["notifications"]}},"/v1/notifications/{id}/view":{"put":{"operationId":"NotificationsController_updateViewedStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"number"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationViewUpdateDto"}}}},"responses":{"200":{"description":""}},"tags":["notifications"]}},"/v1/notifications/stream":{"get":{"operationId":"NotificationsController_stream","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["notifications"]}},"/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/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":"@description The primary key of the library entity.\nAlso 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"]},"Notification":{"type":"object","properties":{"id":{"type":"number"},"sourceType":{"type":"string","enum":["game","review","activity","profile"]},"category":{"type":"string","description":"What this notification's about. E.g.: a new like, a new follower, a game launch, etc.","enum":["follow","like","comment","launch"]},"review":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Review"}]},"reviewId":{"type":"string","nullable":true},"game":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Game"}]},"gameId":{"type":"number","nullable":true},"activity":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Activity"}]},"activityId":{"type":"string","nullable":true},"profile":{"nullable":true,"description":"User responsible for generating this notification (e.g. user that liked a review).","allOf":[{"$ref":"#/components/schemas/Profile"}]},"profileUserId":{"type":"string","nullable":true,"description":"User responsible for generating this notification (e.g. user that liked a review).\nWhen null/undefined, the notification was generated by the 'system'."},"isViewed":{"type":"boolean"},"targetProfile":{"nullable":true,"description":"User which is the target for this notification. \nIf this is empty (null/undefined), the notification is targeted at all users. \nNot to be confused with the 'profile' property.","allOf":[{"$ref":"#/components/schemas/Profile"}]},"targetProfileUserId":{"type":"string","nullable":true,"description":"User which is the target for this notification. \nIf this is empty (null/undefined), the notification is targeted at all users. \nNot to be confused with the 'profile' property."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","sourceType","category","review","reviewId","game","gameId","activity","activityId","profile","profileUserId","isViewed","targetProfile","targetProfileUserId","createdAt","updatedAt"]},"NotificationAggregateDto":{"type":"object","properties":{"sourceId":{"oneOf":[{"type":"string"},{"type":"number"}]},"category":{"type":"string","enum":["follow","like","comment","launch"]},"sourceType":{"type":"string","enum":["game","review","activity","profile"]},"notifications":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}}},"required":["sourceId","category","sourceType","notifications"]},"PaginatedNotificationAggregationDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationAggregateDto"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"NotificationViewUpdateDto":{"type":"object","properties":{"isViewed":{"type":"boolean"}},"required":["isViewed"]},"ActivitiesFeedPaginatedResponseDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}},"pagination":{"$ref":"#/components/schemas/PaginationInfo"}},"required":["data","pagination"]},"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"}]},"targetUserId":{"type":"string","minLength":36},"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/achievements/achievements-queue/achievements-queue.processor.ts b/src/achievements/achievements-queue/achievements-queue.processor.ts
index 6f95757..2b7966e 100644
--- a/src/achievements/achievements-queue/achievements-queue.processor.ts
+++ b/src/achievements/achievements-queue/achievements-queue.processor.ts
@@ -1,21 +1,26 @@
-import { Process, Processor } from "@nestjs/bull";
+import { Processor, WorkerHost } from "@nestjs/bullmq";
import {
ACHIEVEMENTS_QUEUE_NAME,
ACHIEVEMENTS_QUEUE_TRACKING_JOB_NAME,
AchievementsQueueJob,
} from "./achievements-queue.constants";
-import { Job } from "bull";
+import { Job } from "bullmq";
import { AchievementsService } from "../achievements.service";
+import { WorkerHostProcessor } from "../../utils/WorkerHostProcessor";
@Processor(ACHIEVEMENTS_QUEUE_NAME)
-export class AchievementsQueueProcessor {
- constructor(private readonly achievementsService: AchievementsService) {}
+export class AchievementsQueueProcessor extends WorkerHostProcessor {
+ constructor(private readonly achievementsService: AchievementsService) {
+ super();
+ }
- @Process(ACHIEVEMENTS_QUEUE_TRACKING_JOB_NAME)
- async processTrackingJobs(job: Job) {
- return this.achievementsService.trackAchievementsProgress(
- job.data.targetUserId,
- job.data.category,
- );
+ async process(job: Job): Promise {
+ switch (job.name) {
+ case ACHIEVEMENTS_QUEUE_TRACKING_JOB_NAME:
+ return this.achievementsService.trackAchievementsProgress(
+ job.data.targetUserId,
+ job.data.category,
+ );
+ }
}
}
diff --git a/src/achievements/achievements-queue/achievements-queue.service.spec.ts b/src/achievements/achievements-queue/achievements-queue.service.spec.ts
index a2537b6..248fea8 100644
--- a/src/achievements/achievements-queue/achievements-queue.service.spec.ts
+++ b/src/achievements/achievements-queue/achievements-queue.service.spec.ts
@@ -1,14 +1,13 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AchievementsQueueService } from "./achievements-queue.service";
import { ACHIEVEMENTS_QUEUE_NAME } from "./achievements-queue.constants";
-import { BullModule, getQueueToken } from "@nestjs/bull";
+import { BullModule, getQueueToken } from "@nestjs/bullmq";
describe("AchievementsQueueService", () => {
let service: AchievementsQueueService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
- imports: [BullModule.forRoot({})],
providers: [
AchievementsQueueService,
{
diff --git a/src/achievements/achievements-queue/achievements-queue.service.ts b/src/achievements/achievements-queue/achievements-queue.service.ts
index b0d02c2..9e23f51 100644
--- a/src/achievements/achievements-queue/achievements-queue.service.ts
+++ b/src/achievements/achievements-queue/achievements-queue.service.ts
@@ -1,11 +1,11 @@
import { Injectable } from "@nestjs/common";
-import { InjectQueue } from "@nestjs/bull";
+import { InjectQueue } from "@nestjs/bullmq";
import {
ACHIEVEMENTS_QUEUE_NAME,
ACHIEVEMENTS_QUEUE_TRACKING_JOB_NAME,
AchievementsQueueJob,
} from "./achievements-queue.constants";
-import { Queue } from "bull";
+import { Queue } from "bullmq";
@Injectable()
export class AchievementsQueueService {
diff --git a/src/achievements/achievements.module.ts b/src/achievements/achievements.module.ts
index 1a7b5d0..27dd2ac 100644
--- a/src/achievements/achievements.module.ts
+++ b/src/achievements/achievements.module.ts
@@ -5,7 +5,7 @@ import { AchievementsQueueService } from "./achievements-queue/achievements-queu
import { TypeOrmModule } from "@nestjs/typeorm";
import { ObtainedAchievement } from "./entities/obtained-achievement.entity";
import { AchievementsQueueProcessor } from "./achievements-queue/achievements-queue.processor";
-import { BullModule } from "@nestjs/bull";
+import { BullModule } from "@nestjs/bullmq";
import { ACHIEVEMENTS_QUEUE_NAME } from "./achievements-queue/achievements-queue.constants";
import { LevelModule } from "../level/level.module";
diff --git a/src/activities/activities-queue/activities-queue-processor.ts b/src/activities/activities-queue/activities-queue-processor.ts
index 79e046e..28e881d 100755
--- a/src/activities/activities-queue/activities-queue-processor.ts
+++ b/src/activities/activities-queue/activities-queue-processor.ts
@@ -1,36 +1,41 @@
-import { Process, Processor } from "@nestjs/bull";
+import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Activity } from "../activities-repository/entities/activity.entity";
-import { Job } from "bull";
+import { Job } from "bullmq";
import { Injectable, Logger } from "@nestjs/common";
import { ActivitiesRepositoryService } from "../activities-repository/activities-repository.service";
+import { WorkerHostProcessor } from "../../utils/WorkerHostProcessor";
@Processor("activities")
@Injectable()
-export class ActivitiesQueueProcessor {
- private readonly logger = new Logger(ActivitiesQueueProcessor.name);
+export class ActivitiesQueueProcessor extends WorkerHostProcessor {
+ logger = new Logger(ActivitiesQueueProcessor.name);
constructor(
private activitiesRepositoryService: ActivitiesRepositoryService,
- ) {}
-
- @Process("addActivity")
- async addActivity(job: Job) {
- try {
- await this.activitiesRepositoryService.create(job.data);
- } catch (e) {
- this.logger.error("Error while processing activity: ", e);
- this.logger.log("This error happened for data: ", job.data);
- }
+ ) {
+ super();
}
- @Process("deleteActivity")
- async removeActivity(job: Job) {
- try {
- const sourceId = job.data;
- await this.activitiesRepositoryService.deleteBySourceId(sourceId);
- } catch (e) {
- this.logger.error("Error while deleting activity: ", e);
- this.logger.log("This error happened for data: ", job.data);
+ async process(job: Job) {
+ if (job.name === "addActivity") {
+ try {
+ await this.activitiesRepositoryService.create(
+ job.data as Activity,
+ );
+ } catch (e) {
+ this.logger.error("Error while processing activity: ", e);
+ this.logger.log("This error happened for data: ", job.data);
+ }
+ } else if (job.name === "deleteActivity") {
+ try {
+ const sourceId = job.data;
+ await this.activitiesRepositoryService.deleteBySourceId(
+ sourceId as string,
+ );
+ } catch (e) {
+ this.logger.error("Error while deleting activity: ", e);
+ this.logger.log("This error happened for data: ", job.data);
+ }
}
}
}
diff --git a/src/activities/activities-queue/activities-queue.module.ts b/src/activities/activities-queue/activities-queue.module.ts
index 71750f8..5b236c3 100755
--- a/src/activities/activities-queue/activities-queue.module.ts
+++ b/src/activities/activities-queue/activities-queue.module.ts
@@ -1,4 +1,4 @@
-import { BullModule } from "@nestjs/bull";
+import { BullModule } from "@nestjs/bullmq";
import { Module } from "@nestjs/common";
import { ProfileModule } from "../../profile/profile.module";
import { ActivitiesQueueProcessor } from "./activities-queue-processor";
diff --git a/src/activities/activities-queue/activities-queue.service.spec.ts b/src/activities/activities-queue/activities-queue.service.spec.ts
index c5a9ed8..63b8006 100755
--- a/src/activities/activities-queue/activities-queue.service.spec.ts
+++ b/src/activities/activities-queue/activities-queue.service.spec.ts
@@ -1,6 +1,6 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ActivitiesQueueService } from "./activities-queue.service";
-import { BullModule, getQueueToken } from "@nestjs/bull";
+import { getQueueToken } from "@nestjs/bullmq";
describe("ActivitiesFeedService", () => {
let service: ActivitiesQueueService;
diff --git a/src/activities/activities-queue/activities-queue.service.ts b/src/activities/activities-queue/activities-queue.service.ts
index 5772af0..4411436 100755
--- a/src/activities/activities-queue/activities-queue.service.ts
+++ b/src/activities/activities-queue/activities-queue.service.ts
@@ -1,11 +1,12 @@
-import { InjectQueue } from "@nestjs/bull";
+import { InjectQueue } from "@nestjs/bullmq";
import { Injectable, Logger } from "@nestjs/common";
-import { Queue } from "bull";
+import { Queue } from "bullmq";
import { ActivityCreate } from "./activities-queue.constants";
@Injectable()
export class ActivitiesQueueService {
private readonly logger = new Logger(ActivitiesQueueService.name);
+
constructor(
@InjectQueue("activities") private readonly activitiesQueue: Queue,
) {}
diff --git a/src/app.module.ts b/src/app.module.ts
index 3d2a3b9..5453b0e 100755
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -5,7 +5,7 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { CacheModule } from "@nestjs/cache-manager";
import { ScheduleModule } from "@nestjs/schedule";
import { redisStore } from "cache-manager-redis-yet";
-import { BullModule } from "@nestjs/bull";
+import { BullModule } from "@nestjs/bullmq";
import { LoggerMiddleware } from "./app.logger.middlewhare";
import { GlobalModule } from "./global/global.module";
import { CollectionsModule } from "./collections/collections.module";
@@ -72,7 +72,7 @@ import { NotificationsModule } from "./notifications/notifications.module";
const redisPort = new URL(redisUrl!).port;
return {
- redis: {
+ connection: {
host: redisHost,
port: parseInt(redisPort as string) as any,
autoResubscribe: true,
@@ -84,8 +84,8 @@ import { NotificationsModule } from "./notifications/notifications.module";
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: true,
- attempts: 10,
- backoff: 5000,
+ attempts: 3,
+ backoff: 10000,
},
};
},
diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts
index 049c3b4..ede5b9c 100755
--- a/src/auth/auth.guard.ts
+++ b/src/auth/auth.guard.ts
@@ -1,4 +1,9 @@
-import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
+import {
+ CanActivate,
+ ExecutionContext,
+ Injectable,
+ Logger,
+} from "@nestjs/common";
import { Error as STError } from "supertokens-node";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
@@ -15,9 +20,20 @@ import UserRoles from "supertokens-node/recipe/userroles";
*/
@Injectable()
export class AuthGuard implements CanActivate {
+ private readonly logger = new Logger(AuthGuard.name);
constructor(private readonly reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise {
+ const ctxType = context.getType<"http" | "rmq">();
+
+ if (ctxType === "rmq") {
+ this.logger.warn(
+ `Warning: AuthGuard can't be used in a RabbitMQ service/handler`,
+ );
+
+ return true;
+ }
+
const ctx = context.switchToHttp();
let err = undefined;
diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts
index 5917ec2..886ca3a 100755
--- a/src/auth/auth.service.ts
+++ b/src/auth/auth.service.ts
@@ -4,7 +4,6 @@ import Session from "supertokens-node/recipe/session";
import Dashboard from "supertokens-node/recipe/dashboard";
import ThirdPartyPasswordless from "supertokens-node/recipe/thirdpartypasswordless";
import UserRoles from "supertokens-node/recipe/userroles";
-import jwt from "supertokens-node/recipe/jwt";
import {
SupertokensConfigInjectionToken,
AuthModuleConfig,
@@ -31,7 +30,6 @@ export class AuthService {
apiKey: this.config.apiKey,
},
recipeList: [
- jwt.init(),
ThirdPartyPasswordless.init({
flowType: "USER_INPUT_CODE",
contactMethod: "EMAIL",
diff --git a/src/auth/jwt-auth/jwt-auth.guard.ts b/src/auth/jwt-auth/jwt-auth.guard.ts
deleted file mode 100755
index 4aab7a6..0000000
--- a/src/auth/jwt-auth/jwt-auth.guard.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
-import * as JsonWebToken from "jsonwebtoken";
-import { JwtHeader } from "jsonwebtoken";
-import jwksClient from "jwks-rsa";
-import * as process from "process";
-import { Reflector } from "@nestjs/core";
-
-/**
- * Jwt based auth guard. Checks for valid JWT token which is signed by another service/microservice.
- * Should be used for microservice communication.
- */
-@Injectable()
-export class JwtAuthGuard implements CanActivate {
- private readonly JWKS_URI = `${process.env.DOMAIN_API}/v1/auth/jwt/jwks.json`;
-
- constructor(private readonly reflector: Reflector) {}
-
- /**
- * @param jwtHeader - JWT header, from the decoded token
- * @private
- */
- async getSigningKey(jwtHeader: JwtHeader) {
- const client = jwksClient({
- jwksUri: this.JWKS_URI,
- });
- try {
- const signingKey = await client.getSigningKey(jwtHeader.kid);
- return signingKey.getPublicKey();
- } catch (e) {
- console.error(e);
- }
- }
-
- /**
- * This same logic should be applied to all services/microservices.
- * @param context
- */
- async canActivate(context: ExecutionContext): Promise {
- const ctx = context.switchToHttp();
-
- const isPublic = this.reflector.get(
- "isPublic",
- context.getHandler(),
- );
-
- if (isPublic) {
- return true;
- }
-
- const headers = ctx.getRequest().headers;
- const authorization = headers.authorization as string;
- const bearerToken = authorization?.split("Bearer ")[1];
- if (!authorization || !bearerToken) {
- return false;
- }
-
- const decodedToken = JsonWebToken.decode(bearerToken, {
- complete: true,
- });
- if (!decodedToken) {
- return false;
- }
- try {
- const jwtHeader = decodedToken.header;
- const signingKey = await this.getSigningKey(jwtHeader);
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- JsonWebToken.verify(bearerToken, signingKey, {
- algorithms: ["RS256"],
- });
- } catch (e) {
- console.error(e);
- return false;
- }
-
- return true;
- }
-}
diff --git a/src/auth/jwt-auth/jwt-auth.module.ts b/src/auth/jwt-auth/jwt-auth.module.ts
deleted file mode 100644
index 85e220b..0000000
--- a/src/auth/jwt-auth/jwt-auth.module.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Module } from "@nestjs/common";
-import { JwtAuthService } from "./jwt-auth.service";
-
-@Module({
- providers: [JwtAuthService],
- exports: [JwtAuthService],
-})
-export class JwtAuthModule {}
diff --git a/src/auth/jwt-auth/jwt-auth.service.ts b/src/auth/jwt-auth/jwt-auth.service.ts
deleted file mode 100644
index b776c97..0000000
--- a/src/auth/jwt-auth/jwt-auth.service.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Inject, Injectable } from "@nestjs/common";
-import { CACHE_MANAGER, CacheStore } from "@nestjs/cache-manager";
-import jwt from "supertokens-node/recipe/jwt";
-
-@Injectable()
-export class JwtAuthService {
- private readonly JWT_CACHE_KEY = "jwt-token";
-
- constructor(
- @Inject(CACHE_MANAGER) private readonly cacheManager: CacheStore,
- ) {}
-
- private async getKeyFromCache(): Promise {
- return this.cacheManager.get(this.JWT_CACHE_KEY);
- }
-
- private async createJWT() {
- const jwtResponse = await jwt.createJWT({
- // Mandatory
- source: "microservice",
- });
- if (jwtResponse.status === "OK") {
- return jwtResponse.jwt;
- }
-
- throw new Error("Unable to create JWT. Should never come here.");
- }
-
- public async getJwtToken(): Promise {
- const possibleJwtToken = await this.getKeyFromCache();
- if (possibleJwtToken) {
- return possibleJwtToken;
- }
-
- return await this.createJWT();
- }
-}
diff --git a/src/follow/follow.service.spec.ts b/src/follow/follow.service.spec.ts
index 00ee8c1..e5b76c2 100644
--- a/src/follow/follow.service.spec.ts
+++ b/src/follow/follow.service.spec.ts
@@ -2,7 +2,6 @@ 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";
diff --git a/src/game/game-repository/game-repository.module.ts b/src/game/game-repository/game-repository.module.ts
index 755f903..da9bc9f 100644
--- a/src/game/game-repository/game-repository.module.ts
+++ b/src/game/game-repository/game-repository.module.ts
@@ -14,7 +14,6 @@ import { GameLocalization } from "./entities/game-localization.entity";
import { GameMode } from "./entities/game-mode.entity";
import { GamePlatform } from "./entities/game-platform.entity";
import { GameKeyword } from "./entities/game-keyword.entity";
-import { JwtAuthModule } from "../../auth/jwt-auth/jwt-auth.module";
import { GameRepositoryController } from "./game-repository.controller";
import { GameInvolvedCompany } from "./entities/game-involved-company.entity";
import { GameCompany } from "./entities/game-company.entity";
diff --git a/src/global/global.module.ts b/src/global/global.module.ts
index 81dab35..b2e4e03 100644
--- a/src/global/global.module.ts
+++ b/src/global/global.module.ts
@@ -2,6 +2,8 @@ import { Global, Module } from "@nestjs/common";
import * as process from "process";
import { SupertokensConfigInjectionToken } from "../auth/config.interface";
import { ConfigModule } from "@nestjs/config";
+import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq";
+import { IGDB_SYNC_RABBITMQ_QUEUE_CONFIG } from "../sync/igdb/igdb-sync.constants";
@Global()
@Module({
@@ -9,6 +11,29 @@ import { ConfigModule } from "@nestjs/config";
ConfigModule.forRoot({
isGlobal: true,
}),
+ /**
+ * This module allows for easy integration with RabbitMQ by allowing us to mark
+ * services' (injectables) methods as message handlers.
+ * See 'igdb-sync.service.ts' for an example.
+ */
+ RabbitMQModule.forRootAsync(RabbitMQModule, {
+ useFactory: () => {
+ const rabbitUri = process.env.RABBITMQ_URI;
+ if (!rabbitUri)
+ throw new Error("RABBITMQ_URI must be defined.");
+ return {
+ exchanges: [
+ {
+ name: "sync",
+ type: "direct",
+ createExchangeIfNotExists: true,
+ },
+ ],
+ uri: rabbitUri,
+ queues: [IGDB_SYNC_RABBITMQ_QUEUE_CONFIG],
+ };
+ },
+ }),
],
providers: [
// Add global providers here
@@ -28,6 +53,6 @@ import { ConfigModule } from "@nestjs/config";
provide: SupertokensConfigInjectionToken,
},
],
- exports: [SupertokensConfigInjectionToken],
+ exports: [SupertokensConfigInjectionToken, RabbitMQModule],
})
export class GlobalModule {}
diff --git a/src/notifications/notifications-queue.processor.ts b/src/notifications/notifications-queue.processor.ts
index f510e56..08179f0 100644
--- a/src/notifications/notifications-queue.processor.ts
+++ b/src/notifications/notifications-queue.processor.ts
@@ -1,22 +1,26 @@
-import { Process, Processor } from "@nestjs/bull";
+import { Processor } from "@nestjs/bullmq";
import { NotificationsService } from "./notifications.service";
-import { Job } from "bull";
+import { Job } from "bullmq";
import {
NOTIFICATIONS_QUEUE_NAME,
NOTIFICATIONS_REGISTER_JOB_NAME,
} from "./notifications.constants";
import { CreateNotificationDto } from "./dto/create-notification.dto";
+import { WorkerHostProcessor } from "../utils/WorkerHostProcessor";
@Processor(NOTIFICATIONS_QUEUE_NAME)
-export class NotificationsQueueProcessor {
- constructor(private readonly notificationsService: NotificationsService) {}
+export class NotificationsQueueProcessor extends WorkerHostProcessor {
+ constructor(private readonly notificationsService: NotificationsService) {
+ super();
+ }
- @Process(NOTIFICATIONS_REGISTER_JOB_NAME)
async process(job: Job) {
- try {
- await this.notificationsService.create(job.data);
- } catch (e) {
- console.error(e);
+ if (job.name === NOTIFICATIONS_REGISTER_JOB_NAME) {
+ try {
+ await this.notificationsService.create(job.data);
+ } catch (e) {
+ console.error(e);
+ }
}
}
}
diff --git a/src/notifications/notifications-queue.service.ts b/src/notifications/notifications-queue.service.ts
index 5df7d16..51dc50f 100644
--- a/src/notifications/notifications-queue.service.ts
+++ b/src/notifications/notifications-queue.service.ts
@@ -1,7 +1,7 @@
import { Injectable, Logger } from "@nestjs/common";
import { CreateNotificationDto } from "./dto/create-notification.dto";
-import { InjectQueue } from "@nestjs/bull";
-import { Queue } from "bull";
+import { InjectQueue } from "@nestjs/bullmq";
+import { Queue } from "bullmq";
import {
NOTIFICATIONS_QUEUE_NAME,
NOTIFICATIONS_REGISTER_JOB_NAME,
@@ -10,6 +10,7 @@ import {
@Injectable()
export class NotificationsQueueService {
private readonly logger = new Logger(NotificationsQueueService.name);
+
constructor(
@InjectQueue(NOTIFICATIONS_QUEUE_NAME)
private readonly queue: Queue,
diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts
index 70eb226..971fe47 100644
--- a/src/notifications/notifications.module.ts
+++ b/src/notifications/notifications.module.ts
@@ -4,7 +4,7 @@ import { NotificationsController } from "./notifications.controller";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Notification } from "./entity/notification.entity";
import { NotificationsQueueService } from "./notifications-queue.service";
-import { BullModule } from "@nestjs/bull";
+import { BullModule } from "@nestjs/bullmq";
import { NOTIFICATIONS_QUEUE_NAME } from "./notifications.constants";
import { NotificationsQueueProcessor } from "./notifications-queue.processor";
diff --git a/src/profile/entities/profile.entity.ts b/src/profile/entities/profile.entity.ts
index d4737d5..7e4770d 100755
--- a/src/profile/entities/profile.entity.ts
+++ b/src/profile/entities/profile.entity.ts
@@ -6,7 +6,6 @@ import {
ManyToMany,
OneToOne,
PrimaryColumn,
- PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
import { ProfileAvatar } from "./profile-avatar.entity";
diff --git a/src/statistics/statistics-queue/statistics-queue.module.ts b/src/statistics/statistics-queue/statistics-queue.module.ts
index c64e981..5273067 100644
--- a/src/statistics/statistics-queue/statistics-queue.module.ts
+++ b/src/statistics/statistics-queue/statistics-queue.module.ts
@@ -1,7 +1,7 @@
import { Module } from "@nestjs/common";
import { StatisticsQueueService } from "./statistics-queue.service";
import { StatisticsQueueController } from "./statistics-queue.controller";
-import { BullModule } from "@nestjs/bull";
+import { BullModule } from "@nestjs/bullmq";
import { STATISTICS_QUEUE_NAME } from "./statistics-queue.constants";
import { StatisticsModule } from "../statistics.module";
import { TypeOrmModule } from "@nestjs/typeorm";
diff --git a/src/statistics/statistics-queue/statistics-queue.processor.ts b/src/statistics/statistics-queue/statistics-queue.processor.ts
index 0d4bde9..cc4bae1 100644
--- a/src/statistics/statistics-queue/statistics-queue.processor.ts
+++ b/src/statistics/statistics-queue/statistics-queue.processor.ts
@@ -1,31 +1,36 @@
-import { Process, Processor } from "@nestjs/bull";
+import { Processor } from "@nestjs/bullmq";
import { StatisticsService } from "../statistics.service";
-import { Job } from "bull";
+import { Job } from "bullmq";
import {
StatisticsLikeAction,
StatisticsViewAction,
} from "./statistics-queue.types";
import { STATISTICS_QUEUE_NAME } from "./statistics-queue.constants";
+import { WorkerHostProcessor } from "../../utils/WorkerHostProcessor";
@Processor(STATISTICS_QUEUE_NAME)
-export class StatisticsQueueProcessor {
- constructor(private readonly statisticsService: StatisticsService) {}
-
- @Process("like")
- async processLikes(job: Job) {
- try {
- await this.statisticsService.handleLike(job.data);
- } catch (e) {
- console.error(e);
- }
+export class StatisticsQueueProcessor extends WorkerHostProcessor {
+ constructor(private readonly statisticsService: StatisticsService) {
+ super();
}
- @Process("view")
- async processViews(job: Job) {
- try {
- await this.statisticsService.handleView(job.data);
- } catch (e) {
- console.error(e);
+ async process(job: Job) {
+ if (job.name === "like") {
+ try {
+ await this.statisticsService.handleLike(
+ job.data as StatisticsLikeAction,
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ } else if (job.name === "view") {
+ try {
+ await this.statisticsService.handleView(
+ job.data as StatisticsViewAction,
+ );
+ } catch (e) {
+ console.error(e);
+ }
}
}
}
diff --git a/src/statistics/statistics-queue/statistics-queue.service.spec.ts b/src/statistics/statistics-queue/statistics-queue.service.spec.ts
index ea0a60d..6288ca0 100644
--- a/src/statistics/statistics-queue/statistics-queue.service.spec.ts
+++ b/src/statistics/statistics-queue/statistics-queue.service.spec.ts
@@ -1,6 +1,6 @@
import { Test, TestingModule } from "@nestjs/testing";
import { StatisticsQueueService } from "./statistics-queue.service";
-import { getQueueToken } from "@nestjs/bull";
+import { getQueueToken } from "@nestjs/bullmq";
import { STATISTICS_QUEUE_NAME } from "./statistics-queue.constants";
describe("StatisticsQueueService", () => {
diff --git a/src/statistics/statistics-queue/statistics-queue.service.ts b/src/statistics/statistics-queue/statistics-queue.service.ts
index 5d95af0..bcf403d 100644
--- a/src/statistics/statistics-queue/statistics-queue.service.ts
+++ b/src/statistics/statistics-queue/statistics-queue.service.ts
@@ -1,13 +1,13 @@
import { Injectable } from "@nestjs/common";
import { StatisticsActionType } from "../statistics.constants";
import { STATISTICS_QUEUE_NAME } from "./statistics-queue.constants";
-import { Queue } from "bull";
+import { Queue } from "bullmq";
import {
StatisticsLikeAction,
StatisticsViewAction,
} from "./statistics-queue.types";
import { StatisticsActionDto } from "./dto/statistics-action.dto";
-import { InjectQueue } from "@nestjs/bull";
+import { InjectQueue } from "@nestjs/bullmq";
@Injectable()
export class StatisticsQueueService {
diff --git a/src/sync/igdb/dto/create-game.dto.ts b/src/sync/igdb/dto/create-game.dto.ts
deleted file mode 100644
index 7cdf7b5..0000000
--- a/src/sync/igdb/dto/create-game.dto.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export class CreateGameDto {
- games: any[];
-}
diff --git a/src/sync/igdb/game-queue.constants.ts b/src/sync/igdb/game-queue.constants.ts
deleted file mode 100644
index 21c45b3..0000000
--- a/src/sync/igdb/game-queue.constants.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const IGDB_SYNC_QUEUE_NAME = "igdb-sync-queue";
diff --git a/src/sync/igdb/igdb-sync.constants.ts b/src/sync/igdb/igdb-sync.constants.ts
new file mode 100644
index 0000000..18159fd
--- /dev/null
+++ b/src/sync/igdb/igdb-sync.constants.ts
@@ -0,0 +1,21 @@
+import { RabbitMQQueueConfig } from "@golevelup/nestjs-rabbitmq";
+
+/**
+ * BullMQ queue name, not to be confused with RabbitMQ queue name.
+ */
+export const IGDB_SYNC_QUEUE_NAME = "igdb-sync-queue";
+
+/**
+ * BullMQ queue job name
+ */
+export const IGDB_SYNC_JOB_NAME = "igdb-sync";
+
+export const IGDB_SYNC_RABBITMQ_QUEUE_CONFIG: RabbitMQQueueConfig = {
+ name: "sync-igdb",
+ routingKey: "sync-igdb",
+ exchange: "sync",
+ options: {
+ durable: true,
+ },
+ createQueueIfNotExists: true,
+};
diff --git a/src/sync/igdb/igdb-sync.controller.ts b/src/sync/igdb/igdb-sync.controller.ts
deleted file mode 100644
index 1991ea6..0000000
--- a/src/sync/igdb/igdb-sync.controller.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Body, Controller, Post, UseGuards } from "@nestjs/common";
-import { IgdbSyncService } from "./igdb-sync.service";
-import { CreateGameDto } from "./dto/create-game.dto";
-import { JwtAuthGuard } from "../../auth/jwt-auth/jwt-auth.guard";
-import { ApiTags } from "@nestjs/swagger";
-
-@Controller("sync/igdb")
-@ApiTags("sync-igdb")
-@UseGuards(JwtAuthGuard)
-export class IgdbSyncController {
- constructor(private readonly gameQueueService: IgdbSyncService) {}
-
- @Post()
- async sync(@Body() dto: CreateGameDto) {
- await this.gameQueueService.handle(dto.games);
- }
-}
diff --git a/src/sync/igdb/igdb-sync.module.ts b/src/sync/igdb/igdb-sync.module.ts
index dad1cc3..a42d78c 100644
--- a/src/sync/igdb/igdb-sync.module.ts
+++ b/src/sync/igdb/igdb-sync.module.ts
@@ -1,9 +1,8 @@
import { Module } from "@nestjs/common";
import { IgdbSyncService } from "./igdb-sync.service";
-import { BullModule } from "@nestjs/bull";
+import { BullModule } from "@nestjs/bullmq";
import { IgdbSyncProcessor } from "./igdb-sync.processor";
-import { IGDB_SYNC_QUEUE_NAME } from "./game-queue.constants";
-import { IgdbSyncController } from "./igdb-sync.controller";
+import { IGDB_SYNC_QUEUE_NAME } from "./igdb-sync.constants";
import { GameRepositoryModule } from "../../game/game-repository/game-repository.module";
/**
@@ -15,11 +14,6 @@ import { GameRepositoryModule } from "../../game/game-repository/game-repository
imports: [
BullModule.registerQueue({
name: IGDB_SYNC_QUEUE_NAME,
- limiter: {
- // Process only one job (chunk of games) per second
- max: 1,
- duration: 1000,
- },
defaultJobOptions: {
// If this is not used, Redis will take a lot of ram for completed jobs
removeOnComplete: true,
@@ -28,9 +22,9 @@ import { GameRepositoryModule } from "../../game/game-repository/game-repository
backoff: 300,
},
}),
+
GameRepositoryModule,
],
providers: [IgdbSyncService, IgdbSyncProcessor],
- controllers: [IgdbSyncController],
})
export class IgdbSyncModule {}
diff --git a/src/sync/igdb/igdb-sync.processor.ts b/src/sync/igdb/igdb-sync.processor.ts
index 125b7d5..067ba6f 100644
--- a/src/sync/igdb/igdb-sync.processor.ts
+++ b/src/sync/igdb/igdb-sync.processor.ts
@@ -1,7 +1,10 @@
-import { Process, Processor } from "@nestjs/bull";
+import { Processor } from "@nestjs/bullmq";
import { Logger } from "@nestjs/common";
-import { Job } from "bull";
-import { IGDB_SYNC_QUEUE_NAME } from "./game-queue.constants";
+import { Job } from "bullmq";
+import {
+ IGDB_SYNC_JOB_NAME,
+ IGDB_SYNC_QUEUE_NAME,
+} from "./igdb-sync.constants";
import isEmptyObject from "../../utils/isEmptyObject";
import {
objectKeysToCamelCase,
@@ -10,6 +13,7 @@ import {
import { PartialGame } from "../../game/game-repository/game-repository.types";
import { GameRepositoryCreateService } from "../../game/game-repository/game-repository-create.service";
+import { WorkerHostProcessor } from "../../utils/WorkerHostProcessor";
/**
* Recursively converts types of a game object.
@@ -54,27 +58,30 @@ function normalizeIgdbResults(results: any[]) {
}
@Processor(IGDB_SYNC_QUEUE_NAME)
-export class IgdbSyncProcessor {
- private logger = new Logger(IgdbSyncProcessor.name);
+export class IgdbSyncProcessor extends WorkerHostProcessor {
+ logger = new Logger(IgdbSyncProcessor.name);
constructor(
private readonly gameRepositoryCreateService: GameRepositoryCreateService,
- ) {}
+ ) {
+ super();
+ }
- @Process({
- concurrency: 1,
- })
async process(job: Job) {
- const results = job.data;
+ if (job.name === IGDB_SYNC_JOB_NAME) {
+ const results = job.data;
- const normalizedResults = normalizeIgdbResults(results);
+ const normalizedResults = normalizeIgdbResults(results);
- const tasks: Promise[] = [];
+ const tasks: Promise[] = [];
- for (const result of normalizedResults) {
- tasks.push(this.gameRepositoryCreateService.createOrUpdate(result));
- }
+ for (const result of normalizedResults) {
+ tasks.push(
+ this.gameRepositoryCreateService.createOrUpdate(result),
+ );
+ }
- await Promise.all(tasks);
+ await Promise.all(tasks);
+ }
}
}
diff --git a/src/sync/igdb/igdb-sync.service.spec.ts b/src/sync/igdb/igdb-sync.service.spec.ts
index 6a2547d..f5c11ac 100644
--- a/src/sync/igdb/igdb-sync.service.spec.ts
+++ b/src/sync/igdb/igdb-sync.service.spec.ts
@@ -1,7 +1,7 @@
import { Test, TestingModule } from "@nestjs/testing";
import { IgdbSyncService } from "./igdb-sync.service";
-import { getQueueToken } from "@nestjs/bull";
-import { IGDB_SYNC_QUEUE_NAME } from "./game-queue.constants";
+import { getQueueToken } from "@nestjs/bullmq";
+import { IGDB_SYNC_QUEUE_NAME } from "./igdb-sync.constants";
describe("IgdbSyncService", () => {
let service: IgdbSyncService;
diff --git a/src/sync/igdb/igdb-sync.service.ts b/src/sync/igdb/igdb-sync.service.ts
index 84bfa2f..061c65c 100644
--- a/src/sync/igdb/igdb-sync.service.ts
+++ b/src/sync/igdb/igdb-sync.service.ts
@@ -1,7 +1,11 @@
import { Injectable, Logger } from "@nestjs/common";
-import { InjectQueue } from "@nestjs/bull";
-import { Queue } from "bull";
-import { IGDB_SYNC_QUEUE_NAME } from "./game-queue.constants";
+import { RabbitSubscribe } from "@golevelup/nestjs-rabbitmq";
+import { InjectQueue } from "@nestjs/bullmq";
+import {
+ IGDB_SYNC_JOB_NAME,
+ IGDB_SYNC_QUEUE_NAME,
+} from "./igdb-sync.constants";
+import { Queue } from "bullmq";
/**
* Queue responsible for syncing games from IGDB (results already fetched) to our database.
@@ -14,13 +18,46 @@ export class IgdbSyncService {
constructor(
@InjectQueue(IGDB_SYNC_QUEUE_NAME)
- private readonly gameQueue: Queue,
+ private readonly igdbSyncQueue: Queue,
) {}
+ private msgToChunks(msg: NonNullable