feat: Implement pagination and filtering for approval tasks and process instances

- Added query method to InMemoryApprovalTaskRepository for filtering and pagination of approval tasks.
- Enhanced InMemoryProcessInstanceRepository with query method for filtering and pagination of process instances.
- Introduced Page and PageRequest classes for handling paginated results.
- Updated JdbcApprovalTaskRepository and JdbcProcessInstanceRepository to support querying with pagination.
- Added tests for querying, filtering, and pagination in InMemory and JDBC repositories.
- Implemented dynamic WHERE clause building for SQL queries in PageSupport.
This commit is contained in:
0264408
2026-09-15 08:56:05 +08:00
parent e83234e98a
commit 77d1e5198b
26 changed files with 963 additions and 39 deletions
@@ -1,5 +1,10 @@
package com.jetlumen.ordo.api;
import com.jetlumen.ordo.api.query.InstanceQuery;
import com.jetlumen.ordo.api.query.Page;
import com.jetlumen.ordo.api.query.PageRequest;
import com.jetlumen.ordo.api.query.TaskQuery;
import java.util.List;
import java.util.Optional;
@@ -33,4 +38,13 @@ public interface OrdoEngine {
List<ApprovalTask> findTasks(String instanceId);
List<ApprovalTask> findPendingTasksByAssignee(String assignee);
List<ApprovalTask> findPendingTasksByInstanceId(String instanceId);
/** Paginated, filterable task query; see {@link com.jetlumen.ordo.api.repository.ApprovalTaskRepository#query}. */
Page<ApprovalTask> queryTasks(TaskQuery query, PageRequest pageRequest);
/** Paginated, filterable instance query; see {@link com.jetlumen.ordo.api.repository.ProcessInstanceRepository#query}. */
Page<ProcessInstance> queryInstances(InstanceQuery query, PageRequest pageRequest);
/** Paginated listing of all registered process definitions. */
Page<ProcessDefinition> listDefinitions(PageRequest pageRequest);
}
@@ -0,0 +1,29 @@
package com.jetlumen.ordo.api.query;
import com.jetlumen.ordo.api.ProcessStatus;
import java.time.Instant;
/** Optional filters for querying process instances; a null field means "no filter on that dimension". */
public record InstanceQuery(String definitionId, ProcessStatus status, String initiator,
Instant startedFrom, Instant startedTo) {
public static InstanceQuery any() {
return new InstanceQuery(null, null, null, null, null);
}
public InstanceQuery withDefinitionId(String definitionId) {
return new InstanceQuery(definitionId, status, initiator, startedFrom, startedTo);
}
public InstanceQuery withStatus(ProcessStatus status) {
return new InstanceQuery(definitionId, status, initiator, startedFrom, startedTo);
}
public InstanceQuery withInitiator(String initiator) {
return new InstanceQuery(definitionId, status, initiator, startedFrom, startedTo);
}
public InstanceQuery withStartedBetween(Instant startedFrom, Instant startedTo) {
return new InstanceQuery(definitionId, status, initiator, startedFrom, startedTo);
}
}
@@ -0,0 +1,18 @@
package com.jetlumen.ordo.api.query;
import java.util.List;
/** One page of results plus enough information to compute total pages / next-page availability. */
public record Page<T>(List<T> content, long totalElements, int page, int size) {
public Page {
content = List.copyOf(content);
}
public int totalPages() {
return size == 0 ? 0 : (int) Math.ceil((double) totalElements / size);
}
public boolean hasNext() {
return (long) (page + 1) * size < totalElements;
}
}
@@ -0,0 +1,21 @@
package com.jetlumen.ordo.api.query;
/** Page-number based pagination request. */
public record PageRequest(int page, int size) {
public PageRequest {
if (page < 0) {
throw new IllegalArgumentException("page must not be negative");
}
if (size <= 0) {
throw new IllegalArgumentException("size must be positive");
}
}
public static PageRequest of(int page, int size) {
return new PageRequest(page, size);
}
public int offset() {
return page * size;
}
}
@@ -0,0 +1,33 @@
package com.jetlumen.ordo.api.query;
import com.jetlumen.ordo.api.TaskStatus;
import java.time.Instant;
/** Optional filters for querying approval tasks; a null field means "no filter on that dimension". */
public record TaskQuery(String assignee, String instanceId, String definitionId, TaskStatus status,
Instant createdFrom, Instant createdTo) {
public static TaskQuery any() {
return new TaskQuery(null, null, null, null, null, null);
}
public TaskQuery withAssignee(String assignee) {
return new TaskQuery(assignee, instanceId, definitionId, status, createdFrom, createdTo);
}
public TaskQuery withInstanceId(String instanceId) {
return new TaskQuery(assignee, instanceId, definitionId, status, createdFrom, createdTo);
}
public TaskQuery withDefinitionId(String definitionId) {
return new TaskQuery(assignee, instanceId, definitionId, status, createdFrom, createdTo);
}
public TaskQuery withStatus(TaskStatus status) {
return new TaskQuery(assignee, instanceId, definitionId, status, createdFrom, createdTo);
}
public TaskQuery withCreatedBetween(Instant createdFrom, Instant createdTo) {
return new TaskQuery(assignee, instanceId, definitionId, status, createdFrom, createdTo);
}
}
@@ -1,6 +1,9 @@
package com.jetlumen.ordo.api.repository;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.query.Page;
import com.jetlumen.ordo.api.query.PageRequest;
import com.jetlumen.ordo.api.query.TaskQuery;
import java.util.List;
import java.util.Optional;
@@ -27,4 +30,7 @@ public interface ApprovalTaskRepository {
* @return true if the update was applied, false if the task had already been completed by a concurrent operation
*/
boolean completeIfPending(ApprovalTask completedTask);
/** Paginated, filterable query; results are ordered newest-first (created_at desc). */
Page<ApprovalTask> query(TaskQuery query, PageRequest pageRequest);
}
@@ -1,6 +1,8 @@
package com.jetlumen.ordo.api.repository;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.query.Page;
import com.jetlumen.ordo.api.query.PageRequest;
import java.util.Optional;
@@ -20,4 +22,7 @@ public interface ProcessDefinitionRepository {
void upsert(ProcessDefinition definition);
Optional<ProcessDefinition> findById(String definitionId);
/** Paginated listing of all registered definitions, ordered by id ascending. */
Page<ProcessDefinition> findAll(PageRequest pageRequest);
}
@@ -1,6 +1,9 @@
package com.jetlumen.ordo.api.repository;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.query.InstanceQuery;
import com.jetlumen.ordo.api.query.Page;
import com.jetlumen.ordo.api.query.PageRequest;
import java.util.Optional;
@@ -22,4 +25,7 @@ public interface ProcessInstanceRepository {
* @return true if the update was applied, false if the instance was missing or already terminal
*/
boolean completeIfRunning(ProcessInstance completed);
/** Paginated, filterable query; results are ordered newest-first (started_at desc). */
Page<ProcessInstance> query(InstanceQuery query, PageRequest pageRequest);
}
@@ -0,0 +1,60 @@
package com.jetlumen.ordo.api.query;
import com.jetlumen.ordo.api.TaskStatus;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class QueryTypesTest {
@Test
void pageRequestRejectsInvalidPageOrSize() {
assertThrows(IllegalArgumentException.class, () -> new PageRequest(-1, 10));
assertThrows(IllegalArgumentException.class, () -> new PageRequest(0, 0));
assertEquals(20, PageRequest.of(2, 10).offset());
}
@Test
void pageComputesTotalPagesAndHasNext() {
Page<String> page = new Page<>(List.of("a", "b"), 5, 0, 2);
assertEquals(3, page.totalPages());
assertTrue(page.hasNext());
Page<String> lastPage = new Page<>(List.of("e"), 5, 2, 2);
assertFalse(lastPage.hasNext());
}
@Test
void taskQueryAnyHasNoFiltersAndWithersAreImmutable() {
TaskQuery any = TaskQuery.any();
assertNull(any.assignee());
assertNull(any.definitionId());
TaskQuery filtered = any.withAssignee("maria").withDefinitionId("leave").withStatus(TaskStatus.PENDING);
assertEquals("maria", filtered.assignee());
assertEquals("leave", filtered.definitionId());
assertEquals(TaskStatus.PENDING, filtered.status());
assertNull(any.assignee());
}
@Test
void instanceQueryAnyHasNoFiltersAndWithersAreImmutable() {
InstanceQuery any = InstanceQuery.any();
assertNull(any.definitionId());
Instant from = Instant.parse("2026-01-01T00:00:00Z");
Instant to = Instant.parse("2026-01-02T00:00:00Z");
InstanceQuery filtered = any.withDefinitionId("leave").withInitiator("alice").withStartedBetween(from, to);
assertEquals("leave", filtered.definitionId());
assertEquals("alice", filtered.initiator());
assertEquals(from, filtered.startedFrom());
assertEquals(to, filtered.startedTo());
assertNull(any.definitionId());
}
}