Public Access
feat(library): sort by rating and priority with a stable order
Games without a rating or priority come last in both directions. Priority sorts HIGH, MEDIUM, LOW instead of by enum name, and appid breaks every tie, so pages no longer repeat or skip games with equal values.
This commit is contained in:
1 parent
d085080c13
commit
83b9c0e1cf
4 files changed
+72
-2
No files matched your search
@@ -39,7 +39,7 @@ public class LibraryController {
|
||||
|
||||
private static final Set<Integer> ALLOWED_SIZES = Set.of(20, 40, 80);
|
||||
private static final int DEFAULT_SIZE = 20;
|
||||
private static final Set<String> SORTABLE_FIELDS = Set.of("playtimeMinutes", "firstSeenAt", "lastSeenAt", "appid");
|
||||
private static final Set<String> SORTABLE_FIELDS = Set.of("playtimeMinutes", "firstSeenAt", "lastSeenAt", "appid", "rating", "priority");
|
||||
|
||||
private final LibrarySyncService syncService;
|
||||
private final LibraryQueryService queryService;
|
||||
|
||||
@@ -6,6 +6,8 @@ import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.JpaSort;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
@@ -48,6 +50,25 @@ public interface UserGameRepository extends JpaRepository<UserGame, UserGameId>
|
||||
@Param("namePattern") String namePattern,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* Makes a {@link #findOwnedGames} sort stable and meaningful: games without a rating or priority come last
|
||||
* in both directions, priority sorts by HIGH, MEDIUM, LOW (the enum names would sort alphabetically), and
|
||||
* appid breaks every tie so pages do not repeat or skip games.
|
||||
*/
|
||||
static Sort querySort(Sort requested) {
|
||||
Sort result = Sort.unsorted();
|
||||
for (Sort.Order order : requested) {
|
||||
result = result.and(switch (order.getProperty()) {
|
||||
case "rating" -> Sort.by(order.nullsLast());
|
||||
case "priority" -> Sort.by(JpaSort.unsafe(order.getDirection(),
|
||||
"(case ug.priority when 'HIGH' then 3 when 'MEDIUM' then 2 when 'LOW' then 1 end)")
|
||||
.iterator().next().nullsLast());
|
||||
default -> Sort.by(order);
|
||||
});
|
||||
}
|
||||
return result.and(Sort.by("appid"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-insensitive "contains" pattern for {@link #findOwnedGames}; % and _ in the query match literally.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.modules.library.repo.UserSteamSyncRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -35,8 +36,10 @@ public class LibraryQueryService {
|
||||
}
|
||||
|
||||
public Page<GameDto> listOwnedGames(Long userId, GameStatus status, String nameQuery, Pageable pageable) {
|
||||
Pageable stable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(),
|
||||
UserGameRepository.querySort(pageable.getSort()));
|
||||
Page<UserGame> page = userGameRepository.findOwnedGames(
|
||||
userId, status, UserGameRepository.containsPattern(nameQuery), pageable);
|
||||
userId, status, UserGameRepository.containsPattern(nameQuery), stable);
|
||||
List<Long> appids = page.getContent().stream().map(UserGame::getAppid).toList();
|
||||
Map<Long, SteamGame> meta = steamGameRepository.findAllById(appids).stream()
|
||||
.collect(Collectors.toMap(SteamGame::getAppid, g -> g));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.GamePriority;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.StatusCount;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
@@ -95,6 +96,51 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
assertEquals(2, page.getTotalElements());
|
||||
}
|
||||
|
||||
private List<Long> sortedAppids(Sort sort) {
|
||||
return userGameRepository.findOwnedGames(USER_ID, null, null, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
|
||||
.map(UserGame::getAppid).getContent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void querySort_RatingDesc_PutsUnratedLastAndBreaksTiesByAppid() {
|
||||
setBacklog(220L, 3, null);
|
||||
setBacklog(620L, 5, null);
|
||||
setBacklog(700L, 3, null);
|
||||
|
||||
assertEquals(List.of(620L, 220L, 700L, 380L), sortedAppids(Sort.by(Sort.Direction.DESC, "rating")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void querySort_PriorityDesc_IsHighMediumLowThenNone() {
|
||||
setBacklog(220L, null, GamePriority.LOW);
|
||||
setBacklog(380L, null, GamePriority.HIGH);
|
||||
setBacklog(700L, null, GamePriority.MEDIUM);
|
||||
|
||||
assertEquals(List.of(380L, 700L, 220L, 620L), sortedAppids(Sort.by(Sort.Direction.DESC, "priority")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void querySort_EqualPlaytime_BreaksTiesByAppid() {
|
||||
assertEquals(List.of(620L, 220L, 380L, 700L), sortedAppids(Sort.by(Sort.Direction.DESC, "playtimeMinutes")));
|
||||
}
|
||||
|
||||
// unrated LAST (nullsLast) and the querySort javadoc promises "then none" — one API, two opposite null rules.
|
||||
@Test
|
||||
void querySort_PriorityAsc_PutsNoneLast() {
|
||||
setBacklog(220L, null, GamePriority.LOW);
|
||||
setBacklog(380L, null, GamePriority.HIGH);
|
||||
setBacklog(700L, null, GamePriority.MEDIUM);
|
||||
|
||||
assertEquals(List.of(220L, 700L, 380L, 620L), sortedAppids(Sort.by(Sort.Direction.ASC, "priority")));
|
||||
}
|
||||
|
||||
private void setBacklog(long appid, Integer rating, GamePriority priority) {
|
||||
UserGame game = userGameRepository.findById(new UserGameId(USER_ID, appid)).orElseThrow();
|
||||
game.setRating(rating);
|
||||
game.setPriority(priority);
|
||||
userGameRepository.flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsPattern_BlankIsNoFilter() {
|
||||
assertNull(UserGameRepository.containsPattern(" "));
|
||||
|
||||
Reference in new issue
Block a user