Repository: DataBiosphere/terra-scientific-pipelines-service
Pull request #221: [TSPS-506] Add `totalResults` and `quotaConsumed` fields to getAllPipelineRuns response
Author: login: MatthewBemis
Created: 2025-05-21T18:46:26Z | Merged: 2025-05-30T17:52:31Z

### Description 

Adds a couple of new fields to the getAllPipelineRuns response, which will help populate the Job History page in the Imputation UI.

Related Imputation UI PR: https://github.com/DataBiosphere/terra-ui/pull/5327

### Jira Ticket

https://broadworkbench.atlassian.net/browse/TSPS-506

### Checklist (provide links to changes)

- [ ] Updated external documentation (if applicable)
- [ ] Updated internal documentation (if applicable)
- [ ] Planned non patch version bump (if applicable)
- [ ] Updated CLI PR (if applicable)



Diff:
diff --git a/common/openapi.yml b/common/openapi.yml
index cb21f1fbe..e607b07c6 100644
--- a/common/openapi.yml
+++ b/common/openapi.yml
@@ -658,6 +658,8 @@ components:
       properties:
         pageToken:
           type: string
+        totalResults:
+          type: integer
         results:
           description: List of pipeline runs
           type: array
@@ -769,6 +771,8 @@ components:
           $ref: "#/components/schemas/JobTimeSubmitted"
         timeCompleted:
           $ref: "#/components/schemas/JobTimeCompleted"
+        quotaConsumed:
+          $ref: "#/components/schemas/QuotaConsumed"
 
     PipelineRunDescription:
       description: |
diff --git a/service/src/main/java/bio/terra/pipelines/app/controller/PipelineRunsApiController.java b/service/src/main/java/bio/terra/pipelines/app/controller/PipelineRunsApiController.java
index cbce922e5..c3ee14c6e 100644
--- a/service/src/main/java/bio/terra/pipelines/app/controller/PipelineRunsApiController.java
+++ b/service/src/main/java/bio/terra/pipelines/app/controller/PipelineRunsApiController.java
@@ -217,6 +217,8 @@ public ResponseEntity<ApiGetPipelineRunsResponse> getAllPipelineRuns(
         pipelinesService.getPipelines().stream()
             .collect(Collectors.toMap(Pipeline::getId, pipeline -> pipeline.getName().getValue()));
 
+    int totalResults = Math.toIntExact(pipelineRunsService.getPipelineRunCount(userId));
+
     // convert PageResponse object to list of ApiPipelineRun objects for response
     List<ApiPipelineRun> apiPipelineRuns =
         pageResults.content().stream()
@@ -226,6 +228,7 @@ public ResponseEntity<ApiGetPipelineRunsResponse> getAllPipelineRuns(
                         .jobId(pipelineRun.getJobId())
                         .pipelineName(pipelineIdToNameMap.get(pipelineRun.getPipelineId()))
                         .status(pipelineRun.getStatus().name())
+                        .quotaConsumed(pipelineRun.getQuotaConsumed())
                         .description(pipelineRun.getDescription())
                         .timeSubmitted(pipelineRun.getCreated().toString())
                         .timeCompleted(
@@ -237,6 +240,7 @@ public ResponseEntity<ApiGetPipelineRunsResponse> getAllPipelineRuns(
     ApiGetPipelineRunsResponse apiGetPipelineRunsResponse =
         new ApiGetPipelineRunsResponse()
             .results(apiPipelineRuns)
+            .totalResults(totalResults)
             .pageToken(pageResults.nextPageCursor());
     return new ResponseEntity<>(apiGetPipelineRunsResponse, HttpStatus.OK);
   }
diff --git a/service/src/main/java/bio/terra/pipelines/db/repositories/PipelineRunsRepository.java b/service/src/main/java/bio/terra/pipelines/db/repositories/PipelineRunsRepository.java
index 2d57d1eaa..5c960da30 100644
--- a/service/src/main/java/bio/terra/pipelines/db/repositories/PipelineRunsRepository.java
+++ b/service/src/main/java/bio/terra/pipelines/db/repositories/PipelineRunsRepository.java
@@ -16,4 +16,6 @@ public interface PipelineRunsRepository
   Optional<PipelineRun> findByJobIdAndUserId(UUID jobId, String userId);
 
   boolean existsByIdGreaterThan(Long id);
+
+  int countByUserId(String userId);
 }
diff --git a/service/src/main/java/bio/terra/pipelines/service/PipelineRunsService.java b/service/src/main/java/bio/terra/pipelines/service/PipelineRunsService.java
index 0e9cae7b4..21795652c 100644
--- a/service/src/main/java/bio/terra/pipelines/service/PipelineRunsService.java
+++ b/service/src/main/java/bio/terra/pipelines/service/PipelineRunsService.java
@@ -353,4 +353,8 @@ public PageResponse<List<PipelineRun>> findPipelineRunsPaginated(
         CursorBasedPageable.getEncodedCursor(
             pipelineRuns.get(pipelineRuns.size() - 1).getId().toString(), postSlice.hasNext()));
   }
+
+  public long getPipelineRunCount(String userId) {
+    return pipelineRunsRepository.countByUserId(userId);
+  }
 }
diff --git a/service/src/test/java/bio/terra/pipelines/controller/PipelineRunsApiControllerTest.java b/service/src/test/java/bio/terra/pipelines/controller/PipelineRunsApiControllerTest.java
index 25901b66e..00dd0b6fb 100644
--- a/service/src/test/java/bio/terra/pipelines/controller/PipelineRunsApiControllerTest.java
+++ b/service/src/test/java/bio/terra/pipelines/controller/PipelineRunsApiControllerTest.java
@@ -768,6 +768,7 @@ void getAllPipelineRunsWithNoPageToken() throws Exception {
     assertEquals(pipelineRunPreparing.getStatus().name(), responsePipelineRun1.getStatus());
     assertEquals(pipelineRunPreparing.getDescription(), responsePipelineRun1.getDescription());
     assertEquals(pipelineRunPreparing.getJobId(), responsePipelineRun1.getJobId());
+    assertEquals(pipelineRunPreparing.getQuotaConsumed(), responsePipelineRun1.getQuotaConsumed());
     assertEquals(getTestPipeline().getName().getValue(), responsePipelineRun1.getPipelineName());
     assertEquals(
         pipelineRunPreparing.getCreated().toString(), responsePipelineRun1.getTimeSubmitted());
@@ -780,6 +781,7 @@ void getAllPipelineRunsWithNoPageToken() throws Exception {
     assertEquals(pipelineRunSucceeded.getStatus().name(), responsePipelineRun2.getStatus());
     assertEquals(pipelineRunSucceeded.getDescription(), responsePipelineRun2.getDescription());
     assertEquals(pipelineRunSucceeded.getJobId(), responsePipelineRun2.getJobId());
+    assertEquals(pipelineRunSucceeded.getQuotaConsumed(), responsePipelineRun2.getQuotaConsumed());
     assertEquals(getTestPipeline().getName().getValue(), responsePipelineRun2.getPipelineName());
     assertEquals(
         pipelineRunSucceeded.getCreated().toString(), responsePipelineRun2.getTimeSubmitted());
@@ -795,6 +797,7 @@ void getAllPipelineRunsWithNoPageToken() throws Exception {
     assertEquals(pipelineRunFailed.getStatus().name(), responsePipelineRun3.getStatus());
     assertEquals(pipelineRunFailed.getDescription(), responsePipelineRun3.getDescription());
     assertEquals(pipelineRunFailed.getJobId(), responsePipelineRun3.getJobId());
+    assertEquals(pipelineRunFailed.getQuotaConsumed(), responsePipelineRun3.getQuotaConsumed());
     assertEquals(getTestPipeline().getName().getValue(), responsePipelineRun3.getPipelineName());
     assertEquals(
         pipelineRunFailed.getCreated().toString(), responsePipelineRun3.getTimeSubmitted());
@@ -847,6 +850,46 @@ void getAllPipelineRunsWithPageTokenWithNextPageOverMaxLimit() throws Exception
     assertEquals(pipelineRun.getJobId(), responsePipelineRun.getJobId());
   }
 
+  @Test
+  void getAllPipelineRunsWithTotalResults() throws Exception {
+    int limit = 10;
+    String pageToken = null;
+    PipelineRun pipelineRunPreparing1 = getPipelineRunPreparing(null);
+    PipelineRun pipelineRunPreparing2 = getPipelineRunPreparing(null);
+    PipelineRun pipelineRunPreparing3 = getPipelineRunPreparing(null);
+    PipelineRun pipelineRunPreparing4 = getPipelineRunPreparing(null);
+    PageResponse<List<PipelineRun>> pageResponse =
+        new PageResponse<>(
+            List.of(
+                pipelineRunPreparing1,
+                pipelineRunPreparing2,
+                pipelineRunPreparing3,
+                pipelineRunPreparing4),
+            null,
+            null);
+
+    when(pipelineRunsServiceMock.findPipelineRunsPaginated(
+            limit, pageToken, testUser.getSubjectId()))
+        .thenReturn(pageResponse);
+
+    when(pipelineRunsServiceMock.getPipelineRunCount(testUser.getSubjectId())).thenReturn(4L);
+
+    MvcResult result =
+        mockMvc
+            .perform(get(String.format("/api/pipelineruns/v1/pipelineruns?limit=%s", limit)))
+            .andExpect(status().isOk())
+            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+            .andReturn();
+
+    ApiGetPipelineRunsResponse response =
+        new ObjectMapper()
+            .readValue(result.getResponse().getContentAsString(), ApiGetPipelineRunsResponse.class);
+
+    assertNull(response.getPageToken());
+    assertEquals(4, response.getResults().size());
+    assertEquals(4, response.getTotalResults());
+  }
+
   // support methods
 
   private String testPreparePipelineRunPostBody(
diff --git a/service/src/test/java/bio/terra/pipelines/service/PipelineRunsServiceTest.java b/service/src/test/java/bio/terra/pipelines/service/PipelineRunsServiceTest.java
index 2bf5898db..f52e1f46a 100644
--- a/service/src/test/java/bio/terra/pipelines/service/PipelineRunsServiceTest.java
+++ b/service/src/test/java/bio/terra/pipelines/service/PipelineRunsServiceTest.java
@@ -604,4 +604,18 @@ void findPageResultsResultsUseNextPage() {
     // test that results are coming with most recent first
     assertTrue(firstResultTime.isAfter(thirdResultTime));
   }
+
+  @Test
+  void getPipelineRunCountForUser() {
+    // A test row already exists for this user. Confirm the initial count is 1.
+    long initialResultsCount = pipelineRunsService.getPipelineRunCount(testUserId);
+    assertEquals(1, initialResultsCount);
+
+    // Add a new pipeline run for the same user and confirm the count is now 2.
+    PipelineRun pipelineRun = createNewPipelineRunWithJobId(testJobId);
+    pipelineRunsRepository.save(pipelineRun);
+
+    long totalResultsCount = pipelineRunsService.getPipelineRunCount(testUserId);
+    assertEquals(2, totalResultsCount);
+  }
 }


Reviews:
user: login: jsotobroad
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T00:46:20Z
user: login: jsotobroad
body: one small question about long -> int conversion otherwise looks good, thanks for the changes!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-29T18:59:28Z
user: login: MatthewBemis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T19:06:32Z
user: login: gbggrant
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:50:31Z

Review comments:
diff_hunk: @@ -353,4 +353,8 @@ public PageResponse<List<PipelineRun>> findPipelineRunsPaginated(
         CursorBasedPageable.getEncodedCursor(
             pipelineRuns.get(pipelineRuns.size() - 1).getId().toString(), postSlice.hasNext()));
   }
+
+  public long getPipelineRunCount(String userId) {
path: service/src/main/java/bio/terra/pipelines/service/PipelineRunsService.java
user: login: jsotobroad
body: can we add a test for this and where its used to return "totalResults" above.  Also is there any reason to put this function in the [pipelineRepository](https://github.com/DataBiosphere/terra-scientific-pipelines-service/blob/main/service/src/main/java/bio/terra/pipelines/db/repositories/PipelineRunsRepository.java) class instead?  the function name woudl be something like `countByUserId`
created_at: 2025-05-29T00:45:19Z
author_association: COLLABORATOR
diff_hunk: @@ -217,6 +217,8 @@ public ResponseEntity<ApiGetPipelineRunsResponse> getAllPipelineRuns(
         pipelinesService.getPipelines().stream()
             .collect(Collectors.toMap(Pipeline::getId, pipeline -> pipeline.getName().getValue()));
 
+    int totalResults = Math.toIntExact(pipelineRunsService.getPipelineRunCount(userId));
path: service/src/main/java/bio/terra/pipelines/app/controller/PipelineRunsApiController.java
user: login: jsotobroad
body: do you get some kind of weird typing error if you just pass this along as a long?
created_at: 2025-05-29T18:58:31Z
author_association: COLLABORATOR
diff_hunk: @@ -217,6 +217,8 @@ public ResponseEntity<ApiGetPipelineRunsResponse> getAllPipelineRuns(
         pipelinesService.getPipelines().stream()
             .collect(Collectors.toMap(Pipeline::getId, pipeline -> pipeline.getName().getValue()));
 
+    int totalResults = Math.toIntExact(pipelineRunsService.getPipelineRunCount(userId));
path: service/src/main/java/bio/terra/pipelines/app/controller/PipelineRunsApiController.java
user: login: MatthewBemis
body: yes- that's exactly what's happening
created_at: 2025-05-29T19:06:32Z
author_association: MEMBER

PR comments:
user: login: jsotobroad
created_at: 2025-05-29T00:56:43Z
body: we should make a ticket for the CLI to also show quota consumed too
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-29T17:31:26Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DataBiosphere_terra-scientific-pipelines-service&pullRequest=221)


author_association: NONE