diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyDailyUsageRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyDailyUsageRepository.java index 2f6e599ebe..4db1dd34b8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyDailyUsageRepository.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyDailyUsageRepository.java @@ -1,5 +1,6 @@ package stirling.software.proprietary.security.repository; +import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -33,6 +34,21 @@ public interface ApiKeyDailyUsageRepository + "WHERE u.apiKeyId = :apiKeyId AND u.epochDay = :epochDay") Long countForDay(@Param("apiKeyId") Long apiKeyId, @Param("epochDay") long epochDay); + /** Batched today-count for many keys in one query (avoids N+1 when listing keys). */ + @Query( + "SELECT u.apiKeyId AS apiKeyId, u.count AS total FROM ApiKeyDailyUsage u " + + "WHERE u.apiKeyId IN :ids AND u.epochDay = :epochDay") + List countForDayByIds( + @Param("ids") Collection ids, @Param("epochDay") long epochDay); + + /** Batched trailing-window sum for many keys in one query. */ + @Query( + "SELECT u.apiKeyId AS apiKeyId, SUM(u.count) AS total FROM ApiKeyDailyUsage u " + + "WHERE u.apiKeyId IN :ids AND u.epochDay >= :fromDayInclusive " + + "GROUP BY u.apiKeyId") + List sumSinceByIds( + @Param("ids") Collection ids, @Param("fromDayInclusive") long fromDayInclusive); + void deleteByApiKeyId(Long apiKeyId); List findByApiKeyId(Long apiKeyId); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyUsageSum.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyUsageSum.java new file mode 100644 index 0000000000..bff5b9304c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/repository/ApiKeyUsageSum.java @@ -0,0 +1,8 @@ +package stirling.software.proprietary.security.repository; + +/** Projection: a key id and a usage total, for batching per-key usage into one query. */ +public interface ApiKeyUsageSum { + Long getApiKeyId(); + + Long getTotal(); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java index e1cbd5b854..3a33255f33 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/ApiKeyManagementService.java @@ -4,8 +4,10 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; @@ -86,8 +88,31 @@ public class ApiKeyManagementService { .forEach(visible::add); } + // Batch usage for all visible keys into two queries rather than two-per-key (avoids N+1). + long today = Instant.now().atZone(ZoneOffset.UTC).toLocalDate().toEpochDay(); + List ids = visible.stream().map(ApiKey::getId).toList(); + Map todayById = new HashMap<>(); + Map monthById = new HashMap<>(); + if (!ids.isEmpty()) { + usageRepository + .countForDayByIds(ids, today) + .forEach(r -> todayById.put(r.getApiKeyId(), r.getTotal())); + usageRepository + .sumSinceByIds(ids, today - (MONTH_WINDOW_DAYS - 1)) + .forEach(r -> monthById.put(r.getApiKeyId(), r.getTotal())); + } + List keys = - visible.stream().map(k -> toDto(caller, k, isManager, teamName)).toList(); + visible.stream() + .map( + k -> + toDto( + caller, + k, + teamName, + zeroIfNull(todayById.get(k.getId())), + zeroIfNull(monthById.get(k.getId())))) + .toList(); return PortalApiKeysResponse.builder() .keys(keys) .canCreateTeamKeys(isManager && teamId != null) @@ -95,6 +120,10 @@ public class ApiKeyManagementService { .build(); } + private static long zeroIfNull(Long value) { + return value == null ? 0L : value; + } + /** Create a key and return its one-time secret. */ @Transactional public CreatedApiKeyDto createKey(CreateApiKeyRequest request) { @@ -151,7 +180,7 @@ public class ApiKeyManagementService { .build()); return CreatedApiKeyDto.builder() - .key(toDto(caller, saved, policyAuthority.canEditPolicies(), teamName)) + .key(toDto(caller, saved, teamName, 0L, 0L)) .secret(rawKey) .build(); } @@ -230,10 +259,8 @@ public class ApiKeyManagementService { }); } - private PortalApiKeyDto toDto(User caller, ApiKey key, boolean isManager, String teamName) { - long today = Instant.now().atZone(ZoneOffset.UTC).toLocalDate().toEpochDay(); - Long todayCount = usageRepository.countForDay(key.getId(), today); - long usageMonth = usageRepository.sumSince(key.getId(), today - (MONTH_WINDOW_DAYS - 1)); + private PortalApiKeyDto toDto( + User caller, ApiKey key, String teamName, long usageToday, long usageMonth) { return PortalApiKeyDto.builder() .id(String.valueOf(key.getId())) .name(key.getName()) @@ -247,7 +274,7 @@ public class ApiKeyManagementService { ? "Never" : LAST_USED_FORMAT.format(key.getLastUsedAt())) .status(key.isActive() ? "active" : "revoked") - .usageToday(todayCount == null ? 0 : todayCount) + .usageToday(usageToday) .usageMonth(usageMonth) .canManage(canManage(caller, key)) .build();