From 1abc03d2c1a68714ba89288e78b3a71aef0cdab5 Mon Sep 17 00:00:00 2001 From: lobo Date: Wed, 30 Sep 2026 09:37:16 +0200 Subject: [PATCH] refactor(library): auditor fields only through applyCorrection --- readme.md | 8 +++++--- .../modules/library/domain/SteamGame.java | 20 ++++++++++++++++++- .../service/GameCorrectionService.java | 3 +-- .../library/service/ManualGameService.java | 7 ++----- .../LibraryControllerAchievementsTest.java | 4 +--- .../api/LibraryControllerActivityTest.java | 3 +-- .../service/ImageRepairServiceTest.java | 3 +-- 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/readme.md b/readme.md index 6982ba6..b830467 100644 --- a/readme.md +++ b/readme.md @@ -232,11 +232,13 @@ At 01:30 `ImageRepairService` repairs the pictures reported through `POST /api/l An AUDITOR (active role) corrects a game's name, header image, speedrun link, moods, endless flag and genres for every user (`game_correction`, V20). The value is also written into the cached row (`steam_game`, `game_speedrun`, `game_mood`), so no read joins the corrections. The refresh writers skip a corrected field: the Steam sync (name), -the image repair (image), the IGDB add (name, image), the mood and speedrun upserts (SQL). **A new writer of these -fields must do the same.** "Back to source" (`DELETE`) writes back the source value from the first correction and +the image repair (image), the IGDB add (name, image), the mood and speedrun upserts (SQL). `SteamGame` has no +setter for name and image: `applyCorrection` is the only Java writer, a refresh uses the guarded updates in +`SteamGameRepository`. "Back to source" (`DELETE`) writes back the source value from the first correction and lets the next refresh ask IGDB or speedrun.com again. An image must be on a host of the frontend's CSP. Known limit: a mood or speedrun refresh that waits on the row lock while an auditor saves can still write the -source value once (its guard sees the state from before the save); the auditor saves again. +source value once (its guard sees the state from before the save); the auditor saves again. `save(new SteamGame(...))` on a +stored key also overwrites a corrected name; the sync and the IGDB add do it only after `findById` found no row. ### Merging duplicates diff --git a/src/main/java/core_service/modules/library/domain/SteamGame.java b/src/main/java/core_service/modules/library/domain/SteamGame.java index c5fe20f..2e37dc7 100644 --- a/src/main/java/core_service/modules/library/domain/SteamGame.java +++ b/src/main/java/core_service/modules/library/domain/SteamGame.java @@ -17,7 +17,6 @@ import java.time.Instant; @DynamicUpdate @Table(name = "steam_game") @Getter -@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class SteamGame { @@ -28,9 +27,11 @@ public class SteamGame { @Column(name = "name", nullable = false, length = 512) private String name; + @Setter @Column(name = "img_icon_url", length = 512) private String imgIconUrl; + @Setter @Column(name = "last_seen_at", nullable = false) private Instant lastSeenAt; @@ -45,9 +46,26 @@ public class SteamGame { } public SteamGame(Long appid, String name, String imgIconUrl, Instant lastSeenAt) { + this(appid, name, imgIconUrl, null, lastSeenAt); + } + + public SteamGame(Long appid, String name, String imgIconUrl, String imageUrl, Instant lastSeenAt) { this.appid = appid; this.name = name; this.imgIconUrl = imgIconUrl; + this.imageUrl = imageUrl; this.lastSeenAt = lastSeenAt; } + + /** + * The only Java writer of name and image on a stored game. Refreshes from a source use the guarded updates in + * {@code SteamGameRepository}, so they never overwrite a correction. + */ + public void applyCorrection(CorrectionField field, String value) { + switch (field) { + case NAME -> name = value; + case IMAGE -> imageUrl = value; + default -> throw new IllegalArgumentException(field + " is not a column of steam_game"); + } + } } diff --git a/src/main/java/core_service/modules/library/service/GameCorrectionService.java b/src/main/java/core_service/modules/library/service/GameCorrectionService.java index 5fe2051..03748c9 100644 --- a/src/main/java/core_service/modules/library/service/GameCorrectionService.java +++ b/src/main/java/core_service/modules/library/service/GameCorrectionService.java @@ -115,8 +115,7 @@ public class GameCorrectionService { private void write(CorrectionField field, SteamGame game, Object value) { switch (field) { - case NAME -> game.setName((String) value); - case IMAGE -> game.setImageUrl((String) value); + case NAME, IMAGE -> game.applyCorrection(field, (String) value); // A new row has only the corrected link, so the next check would ask speedrun.com for nothing. case SPEEDRUN -> gameSpeedrunRepository.writeWeblink(game.getAppid(), (String) value, Instant.now()); case MOODS -> gameMoodsRepository.writeMoods(game.getAppid(), strings(value), LONG_AGO); diff --git a/src/main/java/core_service/modules/library/service/ManualGameService.java b/src/main/java/core_service/modules/library/service/ManualGameService.java index 0fdd17a..71977b7 100644 --- a/src/main/java/core_service/modules/library/service/ManualGameService.java +++ b/src/main/java/core_service/modules/library/service/ManualGameService.java @@ -290,11 +290,8 @@ public class ManualGameService { private void saveCatalogEntry(long appid, IgdbApiService.Game game, Instant now) { Optional existing = steamGameRepository.findById(appid); if (existing.isEmpty()) { - SteamGame created = new SteamGame(appid, game.name(), null, now); - if (!GameKeys.isSteam(appid)) { - created.setImageUrl(game.imageUrl()); - } - steamGameRepository.save(created); + String imageUrl = GameKeys.isSteam(appid) ? null : game.imageUrl(); + steamGameRepository.save(new SteamGame(appid, game.name(), null, imageUrl, now)); } else if (appid == GameKeys.ofIgdb(game.igdbId())) { steamGameRepository.setSourceName(appid, game.name()); steamGameRepository.setImageUrl(appid, game.imageUrl()); diff --git a/src/test/java/core_service/modules/library/api/LibraryControllerAchievementsTest.java b/src/test/java/core_service/modules/library/api/LibraryControllerAchievementsTest.java index 3e84aaa..808213b 100644 --- a/src/test/java/core_service/modules/library/api/LibraryControllerAchievementsTest.java +++ b/src/test/java/core_service/modules/library/api/LibraryControllerAchievementsTest.java @@ -107,9 +107,7 @@ class LibraryControllerAchievementsTest extends AbstractIntegrationTest { game(70L, 8, null, true); // unlocks never fetched game(80L, 10, 12, true); // schema lost achievements, still perfect game(90L, 6, 0, true); // not started - SteamGame repaired = steamGameRepository.findById(20L).orElseThrow(); - repaired.setImageUrl(REPAIRED_COVER); - steamGameRepository.save(repaired); + steamGameRepository.setImageUrl(20L, REPAIRED_COVER); steamGameRepository.save(new SteamGame(95L, "Game 95", null, Instant.now())); UserGame removed = new UserGame(USER_ID, 95L, 60, Instant.now(), Instant.now()); removed.setRemovedAt(Instant.now()); diff --git a/src/test/java/core_service/modules/library/api/LibraryControllerActivityTest.java b/src/test/java/core_service/modules/library/api/LibraryControllerActivityTest.java index 3d62aac..015101f 100644 --- a/src/test/java/core_service/modules/library/api/LibraryControllerActivityTest.java +++ b/src/test/java/core_service/modules/library/api/LibraryControllerActivityTest.java @@ -53,8 +53,7 @@ class LibraryControllerActivityTest extends AbstractIntegrationTest { void seed() { userGameRepository.deleteAll(); jdbc.update("delete from user_game_playtime_day"); - SteamGame repairedPortal = new SteamGame(620L, "Portal 2", null, Instant.now()); - repairedPortal.setImageUrl(REPAIRED_COVER); + SteamGame repairedPortal = new SteamGame(620L, "Portal 2", null, REPAIRED_COVER, Instant.now()); steamGameRepository.saveAll(List.of( new SteamGame(220L, "Half-Life 2", null, Instant.now()), repairedPortal, diff --git a/src/test/java/core_service/modules/library/service/ImageRepairServiceTest.java b/src/test/java/core_service/modules/library/service/ImageRepairServiceTest.java index 273139b..132e001 100644 --- a/src/test/java/core_service/modules/library/service/ImageRepairServiceTest.java +++ b/src/test/java/core_service/modules/library/service/ImageRepairServiceTest.java @@ -136,8 +136,7 @@ class ImageRepairServiceTest extends AbstractUnitTest { @Test void repairNightly_ChecksTheStoredRepairFirst() { due(row(620L, ImageKind.COVER, Instant.now())); - SteamGame game = new SteamGame(620L, "Portal 2", null, Instant.now()); - game.setImageUrl(HASHED_620); + SteamGame game = new SteamGame(620L, "Portal 2", null, HASHED_620, Instant.now()); when(steamGameRepository.findAllById(List.of(620L))).thenReturn(List.of(game)); when(steamCdnService.isAvailable(HASHED_620)).thenReturn(true);