feat: let initiators withdraw running process instances
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+16
@@ -19,6 +19,8 @@ public final class JdbcProcessInstanceRepository implements ProcessInstanceRepos
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
private static final String UPDATE_INSTANCE =
|
||||
"UPDATE ordo_process_instance SET status = ?, finished_at = ? WHERE id = ?";
|
||||
private static final String COMPLETE_IF_RUNNING =
|
||||
"UPDATE ordo_process_instance SET status = ?, finished_at = ? WHERE id = ? AND status = 'RUNNING'";
|
||||
private static final String SELECT_INSTANCE =
|
||||
"SELECT id, definition_id, initiator, status, context_json, started_at, finished_at"
|
||||
+ " FROM ordo_process_instance WHERE id = ?";
|
||||
@@ -91,4 +93,18 @@ public final class JdbcProcessInstanceRepository implements ProcessInstanceRepos
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean completeIfRunning(ProcessInstance completed) {
|
||||
Objects.requireNonNull(completed, "completed must not be null");
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try (PreparedStatement update = connection.prepareStatement(COMPLETE_IF_RUNNING)) {
|
||||
ProcessInstanceMapper.bindUpdate(update, completed);
|
||||
return update.executeUpdate() == 1;
|
||||
} catch (SQLException e) {
|
||||
throw new JdbcStorageException("failed to complete instance: " + completed.id(), e);
|
||||
} finally {
|
||||
connectionProvider.close(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -12,6 +12,8 @@ import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.api.exception.UnauthorizedInstanceOperationException;
|
||||
import com.jetlumen.ordo.api.exception.NoRouteFoundException;
|
||||
import com.jetlumen.ordo.api.exception.TaskAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.core.DefaultOrdoEngine;
|
||||
@@ -165,6 +167,34 @@ class JdbcOrdoEngineIntegrationTest {
|
||||
"leave", "Leave request v2", List.of(ApprovalStep.single("director", "Director approval", "diana")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void initiatorCanWithdrawWhileALaterStepIsPending() {
|
||||
ProcessInstance instance = engine.start("leave", "alice");
|
||||
engine.approve(engine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria");
|
||||
|
||||
ProcessInstance withdrawn = engine.withdraw(instance.id(), "alice", "changed plans");
|
||||
|
||||
assertEquals(ProcessStatus.WITHDRAWN, withdrawn.status());
|
||||
assertEquals(NOW, withdrawn.finishedAt());
|
||||
assertTrue(engine.findPendingTasksByInstanceId(instance.id()).isEmpty());
|
||||
ApprovalTask hrTask = engine.findTasks(instance.id()).stream()
|
||||
.filter(task -> task.stepId().equals("hr"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertEquals(TaskStatus.SKIPPED, hrTask.status());
|
||||
assertEquals("alice", hrTask.action().actor());
|
||||
assertEquals("changed plans", hrTask.action().comment());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withdrawIsRejectedForNonInitiatorAndCompletedInstances() {
|
||||
ProcessInstance instance = engine.start("leave", "alice");
|
||||
assertThrows(UnauthorizedInstanceOperationException.class,
|
||||
() -> engine.withdraw(instance.id(), "mallory"));
|
||||
engine.reject(engine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria");
|
||||
assertThrows(InstanceAlreadyCompletedException.class, () -> engine.withdraw(instance.id(), "alice"));
|
||||
}
|
||||
|
||||
private OrdoEngine newEngine(AssigneeResolver assigneeResolver) {
|
||||
return new DefaultOrdoEngine(Clock.fixed(NOW, ZoneOffset.UTC), assigneeResolver, RoutingCondition.always(),
|
||||
new JdbcTransactionExecutor(connectionProvider),
|
||||
|
||||
+19
@@ -81,6 +81,25 @@ class JdbcProcessInstanceRepositoryTest {
|
||||
assertFalse(repository.existsRunning("leave"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeIfRunningOnlyUpdatesARunningInstance() {
|
||||
ProcessInstance running = new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
|
||||
STARTED_AT, null, ProcessContext.empty());
|
||||
repository.insert(running);
|
||||
|
||||
Instant finishedAt = STARTED_AT.plusSeconds(30);
|
||||
ProcessInstance withdrawn = new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.WITHDRAWN,
|
||||
STARTED_AT, finishedAt, ProcessContext.empty());
|
||||
assertTrue(repository.completeIfRunning(withdrawn));
|
||||
assertEquals(ProcessStatus.WITHDRAWN, repository.findById("inst-1").orElseThrow().status());
|
||||
assertEquals(finishedAt, repository.findById("inst-1").orElseThrow().finishedAt());
|
||||
|
||||
assertFalse(repository.completeIfRunning(new ProcessInstance("inst-1", "leave", "alice",
|
||||
ProcessStatus.APPROVED, STARTED_AT, finishedAt, ProcessContext.empty())));
|
||||
assertFalse(repository.completeIfRunning(new ProcessInstance("missing", "leave", "alice",
|
||||
ProcessStatus.WITHDRAWN, STARTED_AT, finishedAt, ProcessContext.empty())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateOfAnUnknownInstanceFails() {
|
||||
assertThrows(IllegalStateException.class, () -> repository.update(new ProcessInstance("missing", "leave",
|
||||
|
||||
Reference in New Issue
Block a user