diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java new file mode 100644 index 0000000..1b6ec8b --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseDeveloperDto.java @@ -0,0 +1,7 @@ +package com.videogamescatalogue.backend.dto.external; + +public record ApiResponseDeveloperDto( + Long id, + String name +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java index 53741cf..6ed5f6f 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/external/ApiResponseFullGameDto.java @@ -15,6 +15,8 @@ public record ApiResponseFullGameDto( List genres, + List developers, + BigDecimal rating ) { } diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java new file mode 100644 index 0000000..64a1ee1 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/developer/DeveloperDto.java @@ -0,0 +1,8 @@ +package com.videogamescatalogue.backend.dto.internal.developer; + +public record DeveloperDto( + Long id, + Long apiId, + String name +) { +} diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java index 69f8089..1008165 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameDto.java @@ -1,5 +1,6 @@ package com.videogamescatalogue.backend.dto.internal.game; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import java.math.BigDecimal; @@ -12,6 +13,7 @@ public record GameDto( String backgroundImage, Set platforms, Set genres, + Set developers, BigDecimal apiRating, String description ) { diff --git a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java index 1df968d..bfa39fc 100644 --- a/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java +++ b/src/main/java/com/videogamescatalogue/backend/dto/internal/game/GameWithStatusDto.java @@ -1,5 +1,6 @@ package com.videogamescatalogue.backend.dto.internal.game; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.model.UserGame; @@ -13,6 +14,7 @@ public record GameWithStatusDto( String backgroundImage, Set platforms, Set genres, + Set developers, BigDecimal apiRating, String description, UserGame.GameStatus status diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java new file mode 100644 index 0000000..28f0e20 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperMapper.java @@ -0,0 +1,24 @@ +package com.videogamescatalogue.backend.mapper.developer; + +import com.videogamescatalogue.backend.config.MapperConfig; +import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; +import com.videogamescatalogue.backend.model.Developer; +import java.util.List; +import java.util.Set; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface DeveloperMapper { + @Mapping(source = "id", target = "apiId") + @Mapping(target = "id", ignore = true) + Developer toModel(ApiResponseDeveloperDto apiResponseDeveloperDto); + + Set toModelSet(List developers); + + Set toDtoSet(Set developers); + + DeveloperDto toDto(Developer developer); + +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java new file mode 100644 index 0000000..9897f7b --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/mapper/developer/DeveloperProvider.java @@ -0,0 +1,48 @@ +package com.videogamescatalogue.backend.mapper.developer; + +import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; +import com.videogamescatalogue.backend.model.Developer; +import com.videogamescatalogue.backend.repository.DeveloperRepository; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.mapstruct.Named; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class DeveloperProvider { + private final DeveloperMapper developerMapper; + private final DeveloperRepository developerRepository; + + @Named("toDevelopersSet") + public Set toDevelopersSet(List developers) { + List developerApiIds = developers.stream() + .map(ApiResponseDeveloperDto::id) + .toList(); + + List existingDevelopers = developerRepository.findAllByApiIdIn(developerApiIds); + + Map existingDevelopersMap = existingDevelopers.stream() + .collect(Collectors.toMap( + Developer::getApiId, + d -> d + )); + + Set developersSet = developers.stream() + .map(d -> existingDevelopersMap.getOrDefault( + d.id(), + developerMapper.toModel(d))) + .collect(Collectors.toSet()); + + return developersSet; + } + + @Named("toDeveloperDtosSet") + public Set toDeveloperDtosSet(Set developers) { + return developerMapper.toDtoSet(developers); + } +} diff --git a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java index 3458fe7..513dc15 100644 --- a/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java +++ b/src/main/java/com/videogamescatalogue/backend/mapper/game/GameMapper.java @@ -6,6 +6,7 @@ import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.exception.ParsingException; +import com.videogamescatalogue.backend.mapper.developer.DeveloperProvider; import com.videogamescatalogue.backend.mapper.genre.GenreProvider; import com.videogamescatalogue.backend.mapper.platform.PlatformProvider; import com.videogamescatalogue.backend.model.Game; @@ -17,7 +18,11 @@ import org.mapstruct.Mapping; import org.mapstruct.Named; -@Mapper(config = MapperConfig.class, uses = {PlatformProvider.class, GenreProvider.class}) +@Mapper(config = MapperConfig.class, uses = { + PlatformProvider.class, + GenreProvider.class, + DeveloperProvider.class +}) public interface GameMapper { List toModelList(List games); @@ -34,14 +39,20 @@ public interface GameMapper { @Mapping(source = "released", target = "year", qualifiedByName = "toYear") @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformsSet") @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenresSet") + @Mapping(source = "developers", target = "developers", qualifiedByName = "toDevelopersSet") @Mapping(source = "rating", target = "apiRating") Game toModel(ApiResponseFullGameDto apiResponseGameDto); @Mapping(source = "platforms", target = "platforms", qualifiedByName = "toPlatformDtosSet") @Mapping(source = "genres", target = "genres", qualifiedByName = "toGenreDtosSet") + @Mapping(source = "developers", target = "developers", qualifiedByName = "toDeveloperDtosSet") GameDto toDto(Game game); @Mapping(target = "status", source = "status") + @Mapping( + source = "game.developers", target = "developers", + qualifiedByName = "toDeveloperDtosSet" + ) GameWithStatusDto toDtoWithStatus(Game game, UserGame.GameStatus status); @Named("toYear") diff --git a/src/main/java/com/videogamescatalogue/backend/model/Developer.java b/src/main/java/com/videogamescatalogue/backend/model/Developer.java new file mode 100644 index 0000000..8af6844 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/model/Developer.java @@ -0,0 +1,26 @@ +package com.videogamescatalogue.backend.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "developers") +@Getter +@Setter +public class Developer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private Long apiId; + + @Column(nullable = false) + private String name; +} diff --git a/src/main/java/com/videogamescatalogue/backend/model/Game.java b/src/main/java/com/videogamescatalogue/backend/model/Game.java index 449fda0..1fd14e8 100644 --- a/src/main/java/com/videogamescatalogue/backend/model/Game.java +++ b/src/main/java/com/videogamescatalogue/backend/model/Game.java @@ -57,6 +57,15 @@ public class Game { ) private Set genres = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "games_developers", + joinColumns = @JoinColumn(name = "game_id"), + inverseJoinColumns = @JoinColumn(name = "developer_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"game_id", "developer_id"}) + ) + private Set developers = new HashSet<>(); + private BigDecimal apiRating; private String description; diff --git a/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java b/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java new file mode 100644 index 0000000..fbb8a77 --- /dev/null +++ b/src/main/java/com/videogamescatalogue/backend/repository/DeveloperRepository.java @@ -0,0 +1,12 @@ +package com.videogamescatalogue.backend.repository; + +import com.videogamescatalogue.backend.model.Developer; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DeveloperRepository extends JpaRepository { + Optional findByApiId(Long apiId); + + List findAllByApiIdIn(List apiIds); +} diff --git a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java index 8671744..8e4f841 100644 --- a/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java +++ b/src/main/java/com/videogamescatalogue/backend/service/game/GameServiceImpl.java @@ -5,18 +5,23 @@ import com.videogamescatalogue.backend.dto.internal.GameSearchParameters; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; +import com.videogamescatalogue.backend.mapper.developer.DeveloperMapper; import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Developer; import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.model.User; import com.videogamescatalogue.backend.model.UserGame; +import com.videogamescatalogue.backend.repository.DeveloperRepository; import com.videogamescatalogue.backend.repository.GameRepository; import com.videogamescatalogue.backend.repository.SpecificationBuilder; import com.videogamescatalogue.backend.repository.UserGameRepository; import com.videogamescatalogue.backend.service.RawgApiClient; +import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -32,9 +37,11 @@ public class GameServiceImpl implements GameService { private final RawgApiClient apiClient; private final GameMapper gameMapper; + private final DeveloperMapper developerMapper; private final GameRepository gameRepository; private final SpecificationBuilder specificationBuilder; private final UserGameRepository userGameRepository; + private final DeveloperRepository developerRepository; @Override public void fetchBestGames() { @@ -82,6 +89,7 @@ public Page getAllGamesFromDb(Pageable pageable) { .map(gameMapper::toDto); } + @Transactional @Override public GameWithStatusDto getByApiId(Long apiId, User user) { Game game = findOrUpdate(apiId); @@ -154,15 +162,26 @@ private Game findOrUpdate(Long apiId) { } Game game = gameOptional.get(); if (game.getDescription() == null) { - return updateGameDescription(apiId, game); + updateGameDescription(apiId, game); + } + if (game.getDevelopers().isEmpty()) { + updateGameDevelopers(apiId, game); } return game; } - private Game updateGameDescription(Long apiId, Game game) { + private void updateGameDescription(Long apiId, Game game) { ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); game.setDescription(apiGame.description()); - return gameRepository.save(game); + gameRepository.save(game); + } + + private void updateGameDevelopers(Long apiId, Game game) { + ApiResponseFullGameDto apiGame = apiClient.getGameById(apiId); + Set developers = developerMapper.toModelSet(apiGame.developers()); + developerRepository.saveAll(developers); + game.setDevelopers(developers); + gameRepository.save(game); } private Game findFromApi(Long apiId) { diff --git a/src/main/resources/db/changelog/changes/16-create-developers-table.yaml b/src/main/resources/db/changelog/changes/16-create-developers-table.yaml new file mode 100644 index 0000000..2cb4f89 --- /dev/null +++ b/src/main/resources/db/changelog/changes/16-create-developers-table.yaml @@ -0,0 +1,33 @@ +databaseChangeLog: + - changeSet: + id: create-developers-table + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: developers + changes: + - createTable: + tableName: developers + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: api_id + type: BIGINT + constraints: + nullable: false + unique: true + + - column: + name: name + type: VARCHAR(200) + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml b/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml new file mode 100644 index 0000000..bc69400 --- /dev/null +++ b/src/main/resources/db/changelog/changes/17-create-games-developers-table.yaml @@ -0,0 +1,43 @@ +databaseChangeLog: + - changeSet: + id: create-games-developers-table + author: julia + preConditions: + - onFail: MARK_RAN + - not: + - tableExists: + tableName: games_developers + changes: + - createTable: + tableName: games_developers + columns: + - column: + name: game_id + type: BIGINT + constraints: + nullable: false + + - column: + name: developer_id + type: BIGINT + constraints: + nullable: false + + - addPrimaryKey: + tableName: games_developers + columnNames: game_id, developer_id + constraintName: pk_games_developers + + - addForeignKeyConstraint: + baseTableName: games_developers + baseColumnNames: game_id + referencedTableName: games + referencedColumnNames: id + constraintName: fk_games_developers_game + + - addForeignKeyConstraint: + baseTableName: games_developers + baseColumnNames: developer_id + referencedTableName: developers + referencedColumnNames: id + constraintName: fk_games_developers_developer diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 0763141..6baf17b 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -29,3 +29,7 @@ databaseChangeLog: file: classpath:/db/changelog/changes/14-create-user-game-table.yaml - include: file: classpath:/db/changelog/changes/15-create-comments-table.yaml + - include: + file: classpath:/db/changelog/changes/16-create-developers-table.yaml + - include: + file: classpath:/db/changelog/changes/17-create-games-developers-table.yaml diff --git a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java index 850368b..7f4e014 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/game/GameServiceImplTest.java @@ -9,15 +9,18 @@ import static org.mockito.Mockito.when; import com.videogamescatalogue.backend.dto.external.ApiPlatformWrapper; +import com.videogamescatalogue.backend.dto.external.ApiResponseDeveloperDto; import com.videogamescatalogue.backend.dto.external.ApiResponseFullGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGameDto; import com.videogamescatalogue.backend.dto.external.ApiResponseGenreDto; import com.videogamescatalogue.backend.dto.external.ApiResponsePlatformDto; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.game.GameWithStatusDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; import com.videogamescatalogue.backend.mapper.game.GameMapper; +import com.videogamescatalogue.backend.model.Developer; import com.videogamescatalogue.backend.model.Game; import com.videogamescatalogue.backend.model.Genre; import com.videogamescatalogue.backend.model.Platform; @@ -84,6 +87,11 @@ void setUp() { genreModel.setId(20L); genreModel.setName(Genre.Name.ACTION); + Developer developer = new Developer(); + developer.setId(30L); + developer.setApiId(457389L); + developer.setName("Developer"); + gameModel = new Game(); gameModel.setApiId(1L); gameModel.setName("Game"); @@ -93,6 +101,8 @@ void setUp() { gameModel.setGenres(Set.of(genreModel)); gameModel.setApiRating(BigDecimal.valueOf(4.75)); gameModel.setDescription(null); + gameModel.setDevelopers(Set.of(developer)); + gameModelList = List.of(gameModel); responseGamesIds = List.of(1L); zeroGamesToSave = List.of(); @@ -102,24 +112,35 @@ void setUp() { PlatformDto platformDto = new PlatformDto("PC"); GenreDto genreDto = new GenreDto("Action"); + DeveloperDto developerDto = new DeveloperDto( + 30L, 457389L, "Developer" + ); gameDto = new GameDto( 1L, "Game", 2025, "link", Set.of(platformDto), Set.of(genreDto), + Set.of(developerDto), BigDecimal.valueOf(4.75), null ); oneGameDtoPage = new PageImpl<>(List.of(gameDto)); gameWithDescrAndNullStatusDto = new GameWithStatusDto( 1L, "Game", 2025, "link", Set.of(platformDto), Set.of(genreDto), + Set.of(developerDto), BigDecimal.valueOf(4.75), "description", null ); + ApiResponseDeveloperDto apiResponseDeveloperDto = + new ApiResponseDeveloperDto( + 457389L, "Developer" + ); + apiResponseFullGameDto = new ApiResponseFullGameDto( 1L, "Game", "description", "2025-12-12", "link", List.of(apiPlatformWrapper), List.of(apiResponseGenreDto), + List.of(apiResponseDeveloperDto), BigDecimal.valueOf(4.75) ); diff --git a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java index 441d9d7..25000c1 100644 --- a/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java +++ b/src/test/java/com/videogamescatalogue/backend/service/usergame/UserGameServiceImplTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import com.videogamescatalogue.backend.dto.internal.developer.DeveloperDto; import com.videogamescatalogue.backend.dto.internal.game.GameDto; import com.videogamescatalogue.backend.dto.internal.genre.GenreDto; import com.videogamescatalogue.backend.dto.internal.platform.PlatformDto; @@ -45,9 +46,10 @@ class UserGameServiceImplTest { private User user; private UserGame userGame; private UserGameDto userGameDto; - private GameDto gameDto; private PlatformDto platformDto; private GenreDto genreDto; + private DeveloperDto developerDto; + private GameDto gameDto; @BeforeEach void setUp() { @@ -79,11 +81,16 @@ void setUp() { genreDto = new GenreDto("Action"); + developerDto = new DeveloperDto( + 30L, 23456L, "Developer" + ); + gameDto = new GameDto( 1234L, "Game name", 2016, "link", Set.of(platformDto), Set.of(genreDto), + Set.of(developerDto), BigDecimal.valueOf(4.8), "description" );