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:
@@ -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);
|
||||
}
|
||||
|
||||
+5
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user