diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/PushStore.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/PushStore.java index 4dd94a6..bd7a59e 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/PushStore.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/PushStore.java @@ -8,6 +8,7 @@ import org.finos.gitproxy.db.model.PushQuery; import org.finos.gitproxy.db.model.PushRecord; import org.finos.gitproxy.db.model.PushStatus; +import org.finos.gitproxy.db.model.PushSummary; /** * Storage abstraction for push records. Implementations exist for in-memory, JDBC (H2, SQLite, Postgres), and MongoDB. @@ -28,6 +29,30 @@ public interface PushStore { /** Query pushes with optional filters. */ List find(PushQuery query); + /** + * Return lightweight summary projections for the list view. Omits all child collections (steps, commits, + * attestation). Default implementation delegates to {@link #find} and projects in memory; JDBC override uses a lean + * SELECT. + */ + default List findSummaries(PushQuery query) { + return find(query).stream() + .map(r -> PushSummary.builder() + .id(r.getId()) + .status(r.getStatus()) + .url(r.getUrl()) + .upstreamUrl(r.getUpstreamUrl()) + .project(r.getProject()) + .repoName(r.getRepoName()) + .branch(r.getBranch()) + .commitTo(r.getCommitTo()) + .author(r.getAuthor()) + .user(r.getUser()) + .resolvedUser(r.getResolvedUser()) + .timestamp(r.getTimestamp()) + .build()) + .toList(); + } + /** Delete a push record and all associated data. */ void delete(String id); diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcPushStore.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcPushStore.java index c61e503..d7d5baa 100644 --- a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcPushStore.java +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/jdbc/JdbcPushStore.java @@ -96,6 +96,39 @@ public List find(PushQuery query) { return results; } + @Override + public List findSummaries(PushQuery query) { + MapSqlParameterSource params = new MapSqlParameterSource(); + String where = buildWhere(query, params); + String sql = + "SELECT id, status, url, upstream_url, project, repo_name, branch, commit_to, author, push_user, resolved_user, timestamp" + + " FROM push_records" + where + + " ORDER BY timestamp " + (query.isNewestFirst() ? "DESC" : "ASC") + + " LIMIT :limit OFFSET :offset"; + params.addValue("limit", query.getLimit()); + params.addValue("offset", query.getOffset()); + return jdbc.query( + sql, + params, + (rs, rowNum) -> PushSummary.builder() + .id(rs.getString("id")) + .status(PushStatus.valueOf(rs.getString("status"))) + .url(rs.getString("url")) + .upstreamUrl(rs.getString("upstream_url")) + .project(rs.getString("project")) + .repoName(rs.getString("repo_name")) + .branch(rs.getString("branch")) + .commitTo(rs.getString("commit_to")) + .author(rs.getString("author")) + .user(rs.getString("push_user")) + .resolvedUser(rs.getString("resolved_user")) + .timestamp( + rs.getTimestamp("timestamp") != null + ? rs.getTimestamp("timestamp").toInstant() + : null) + .build()); + } + @Override public List summarizeByRepo() { return jdbc.query( diff --git a/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/PushSummary.java b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/PushSummary.java new file mode 100644 index 0000000..a5ba833 --- /dev/null +++ b/git-proxy-java-core/src/main/java/org/finos/gitproxy/db/model/PushSummary.java @@ -0,0 +1,26 @@ +package org.finos.gitproxy.db.model; + +import java.time.Instant; +import lombok.Builder; +import lombok.Value; + +/** + * Lightweight projection of a push record for list views. Omits all {@link PushStep}, {@link PushCommit}, and + * {@link Attestation} data to keep list-endpoint responses small. + */ +@Value +@Builder +public class PushSummary { + String id; + PushStatus status; + String url; + String upstreamUrl; + String project; + String repoName; + String branch; + String commitTo; + String author; + String user; + String resolvedUser; + Instant timestamp; +} diff --git a/git-proxy-java-core/src/test/java/org/finos/gitproxy/db/jdbc/JdbcPushStoreIntegrationTest.java b/git-proxy-java-core/src/test/java/org/finos/gitproxy/db/jdbc/JdbcPushStoreIntegrationTest.java index 36bc29f..cf7cb84 100644 --- a/git-proxy-java-core/src/test/java/org/finos/gitproxy/db/jdbc/JdbcPushStoreIntegrationTest.java +++ b/git-proxy-java-core/src/test/java/org/finos/gitproxy/db/jdbc/JdbcPushStoreIntegrationTest.java @@ -12,6 +12,7 @@ import org.finos.gitproxy.db.model.PushRecord; import org.finos.gitproxy.db.model.PushStatus; import org.finos.gitproxy.db.model.PushStep; +import org.finos.gitproxy.db.model.PushSummary; import org.finos.gitproxy.db.model.StepStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -349,6 +350,50 @@ void find_bySearch_matchesProjectAndRepoName() { assertEquals("widget-service", byRepo.get(0).getRepoName()); } + // ---- findSummaries ---- + + @Test + void findSummaries_returnsProjectionWithoutChildCollections() { + PushRecord saved = PushRecord.builder() + .commitTo("abc123") + .branch("refs/heads/main") + .repoName("repo") + .project("acme") + .upstreamUrl("https://github.com/acme/repo.git") + .author("Alice") + .user("alice") + .resolvedUser("alice") + .status(PushStatus.FORWARDED) + .build(); + store.save(saved); + + List summaries = store.findSummaries(PushQuery.builder().build()); + + assertEquals(1, summaries.size()); + PushSummary s = summaries.get(0); + assertEquals(saved.getId(), s.getId()); + assertEquals(PushStatus.FORWARDED, s.getStatus()); + assertEquals("https://github.com/acme/repo.git", s.getUpstreamUrl()); + assertEquals("refs/heads/main", s.getBranch()); + assertEquals("abc123", s.getCommitTo()); + assertEquals("Alice", s.getAuthor()); + assertEquals("alice", s.getUser()); + assertEquals("alice", s.getResolvedUser()); + assertNotNull(s.getTimestamp()); + } + + @Test + void findSummaries_filtersApplied() { + store.save(record("a", "refs/heads/main", "repoA")); + store.save(record("b", "refs/heads/main", "repoB")); + + List results = + store.findSummaries(PushQuery.builder().repoName("repoA").build()); + + assertEquals(1, results.size()); + assertEquals("repoA", results.get(0).getRepoName()); + } + // ---- initialize idempotency ---- @Test diff --git a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/PushController.java b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/PushController.java index d6ae0c9..40ce7d0 100644 --- a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/PushController.java +++ b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/controller/PushController.java @@ -11,6 +11,7 @@ import org.finos.gitproxy.db.model.PushRecord; import org.finos.gitproxy.db.model.PushStatus; import org.finos.gitproxy.db.model.PushStep; +import org.finos.gitproxy.db.model.PushSummary; import org.finos.gitproxy.jetty.config.AttestationQuestion; import org.finos.gitproxy.jetty.config.GitProxyConfig; import org.finos.gitproxy.jetty.reload.ConfigHolder; @@ -55,7 +56,7 @@ private static String resolveReviewer(Map body) { */ @Operation(operationId = "listPushes", summary = "List push records") @GetMapping - public List list( + public List list( @RequestParam(required = false) String status, @RequestParam(required = false) String project, @RequestParam(required = false) String repo, @@ -82,7 +83,7 @@ public List list( if (user != null && !user.isBlank()) query.user(user); if (search != null && !search.isBlank()) query.search(search); - return pushStore.find(query.build()); + return pushStore.findSummaries(query.build()); } /** diff --git a/git-proxy-java-dashboard/src/test/java/org/finos/gitproxy/dashboard/controller/PushControllerTest.java b/git-proxy-java-dashboard/src/test/java/org/finos/gitproxy/dashboard/controller/PushControllerTest.java index a4307e9..3f26822 100644 --- a/git-proxy-java-dashboard/src/test/java/org/finos/gitproxy/dashboard/controller/PushControllerTest.java +++ b/git-proxy-java-dashboard/src/test/java/org/finos/gitproxy/dashboard/controller/PushControllerTest.java @@ -99,10 +99,10 @@ private void loginAs(String username, boolean admin, boolean selfCertify) { class List_ { @Test void noFilters_delegatesToStore() { - when(pushStore.find(any())).thenReturn(java.util.List.of()); + when(pushStore.findSummaries(any())).thenReturn(java.util.List.of()); var result = controller.list(null, null, null, null, null, 50, 0, true); assertEquals(0, result.size()); - verify(pushStore).find(argThat(q -> q.getLimit() == 50 && q.getOffset() == 0)); + verify(pushStore).findSummaries(argThat(q -> q.getLimit() == 50 && q.getOffset() == 0)); } @Test @@ -115,9 +115,9 @@ void invalidStatus_throws400() { @Test void validStatus_passedToQuery() { - when(pushStore.find(any())).thenReturn(java.util.List.of()); + when(pushStore.findSummaries(any())).thenReturn(java.util.List.of()); controller.list("PENDING", null, null, null, null, 50, 0, true); - verify(pushStore).find(argThat(q -> q.getStatus() == PushStatus.PENDING)); + verify(pushStore).findSummaries(argThat(q -> q.getStatus() == PushStatus.PENDING)); } }