feat: add ACTION steps that run after the approval transaction commits
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+26
-4
@@ -25,7 +25,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
private static final String INSERT_DEFINITION =
|
||||
"INSERT INTO ordo_process_definition (id, name) VALUES (?, ?)";
|
||||
private static final String INSERT_STEP =
|
||||
"INSERT INTO ordo_approval_step (definition_id, step_id, step_name, policy, step_order) VALUES (?, ?, ?, ?, ?)";
|
||||
"INSERT INTO ordo_approval_step (definition_id, step_id, step_name, policy, step_order, kind, action_key)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
private static final String INSERT_CANDIDATE =
|
||||
"INSERT INTO ordo_step_candidate (definition_id, step_id, candidate, candidate_order) VALUES (?, ?, ?, ?)";
|
||||
private static final String INSERT_TRANSITION =
|
||||
@@ -41,7 +42,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
private static final String SELECT_DEFINITION =
|
||||
"SELECT id, name FROM ordo_process_definition WHERE id = ?";
|
||||
private static final String SELECT_STEPS =
|
||||
"SELECT step_id, step_name, policy FROM ordo_approval_step WHERE definition_id = ? ORDER BY step_order";
|
||||
"SELECT step_id, step_name, policy, kind, action_key FROM ordo_approval_step"
|
||||
+ " WHERE definition_id = ? ORDER BY step_order";
|
||||
private static final String SELECT_CANDIDATES =
|
||||
"SELECT step_id, candidate FROM ordo_step_candidate WHERE definition_id = ? ORDER BY step_id, candidate_order";
|
||||
private static final String SELECT_TRANSITIONS =
|
||||
@@ -76,9 +78,17 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
Objects.requireNonNull(definition, "definition must not be null");
|
||||
Connection connection = connectionProvider.getConnection();
|
||||
try {
|
||||
if (!insertDefinitionRow(connection, definition)) {
|
||||
// PostgreSQL aborts the current transaction on unique-constraint violations,
|
||||
// so upsert must not probe existence via a failing INSERT.
|
||||
if (definitionExists(connection, definition.id())) {
|
||||
updateDefinitionName(connection, definition);
|
||||
deleteGraph(connection, definition.id());
|
||||
} else {
|
||||
try (PreparedStatement insert = connection.prepareStatement(INSERT_DEFINITION)) {
|
||||
insert.setString(1, definition.id());
|
||||
insert.setString(2, definition.name());
|
||||
insert.executeUpdate();
|
||||
}
|
||||
}
|
||||
insertGraph(connection, definition);
|
||||
} catch (SQLException e) {
|
||||
@@ -124,7 +134,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
List<ApprovalStep> steps = new ArrayList<>();
|
||||
for (StepRow row : stepRows) {
|
||||
List<String> candidates = candidatesByStep.getOrDefault(row.stepId(), List.of());
|
||||
steps.add(new ApprovalStep(row.stepId(), row.stepName(), candidates, row.policy()));
|
||||
steps.add(new ApprovalStep(row.stepId(), row.stepName(), candidates, row.policy(), row.kind(),
|
||||
row.actionKey()));
|
||||
}
|
||||
List<StepTransition> transitions = new ArrayList<>();
|
||||
try (PreparedStatement selectTransitions = connection.prepareStatement(SELECT_TRANSITIONS)) {
|
||||
@@ -145,6 +156,15 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean definitionExists(Connection connection, String definitionId) throws SQLException {
|
||||
try (PreparedStatement select = connection.prepareStatement(SELECT_DEFINITION)) {
|
||||
select.setString(1, definitionId);
|
||||
try (ResultSet resultSet = select.executeQuery()) {
|
||||
return resultSet.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean insertDefinitionRow(Connection connection, ProcessDefinition definition) throws SQLException {
|
||||
try (PreparedStatement insertDefinition = connection.prepareStatement(INSERT_DEFINITION)) {
|
||||
insertDefinition.setString(1, definition.id());
|
||||
@@ -191,6 +211,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
|
||||
insertStep.setString(3, step.name());
|
||||
insertStep.setString(4, step.policy().name());
|
||||
insertStep.setInt(5, stepOrder++);
|
||||
insertStep.setString(6, step.kind().name());
|
||||
insertStep.setString(7, step.actionKey());
|
||||
insertStep.executeUpdate();
|
||||
}
|
||||
int candidateOrder = 0;
|
||||
|
||||
+5
-2
@@ -1,6 +1,7 @@
|
||||
package com.jetlumen.ordo.storage.jdbc.mapper;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalPolicy;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -16,9 +17,11 @@ public final class ApprovalStepMapper {
|
||||
|
||||
public static StepRow readRow(ResultSet resultSet) throws SQLException {
|
||||
return new StepRow(resultSet.getString("step_id"), resultSet.getString("step_name"),
|
||||
ApprovalPolicy.valueOf(resultSet.getString("policy")));
|
||||
ApprovalPolicy.valueOf(resultSet.getString("policy")),
|
||||
StepKind.valueOf(resultSet.getString("kind")),
|
||||
resultSet.getString("action_key"));
|
||||
}
|
||||
|
||||
public record StepRow(String stepId, String stepName, ApprovalPolicy policy) {
|
||||
public record StepRow(String stepId, String stepName, ApprovalPolicy policy, StepKind kind, String actionKey) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Adds ACTION step kind and an optional action_key (host ActionHandler lookup).
|
||||
ALTER TABLE ordo_approval_step ADD COLUMN kind VARCHAR(16) NOT NULL DEFAULT 'APPROVAL';
|
||||
ALTER TABLE ordo_approval_step ADD COLUMN action_key VARCHAR(255);
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
@@ -119,7 +120,7 @@ class JdbcOrdoEngineIntegrationTest {
|
||||
@Test
|
||||
void rollsBackTheWholeApprovalWhenNoRouteMatches() {
|
||||
OrdoEngine failingEngine = new DefaultOrdoEngine(Clock.fixed(NOW, ZoneOffset.UTC),
|
||||
AssigneeResolver.direct(), (key, context) -> false,
|
||||
AssigneeResolver.direct(), (key, context) -> false, ActionHandler.noop(),
|
||||
new JdbcTransactionExecutor(connectionProvider),
|
||||
new JdbcProcessDefinitionRepository(connectionProvider),
|
||||
new JdbcProcessInstanceRepository(connectionProvider),
|
||||
@@ -197,6 +198,7 @@ class JdbcOrdoEngineIntegrationTest {
|
||||
|
||||
private OrdoEngine newEngine(AssigneeResolver assigneeResolver) {
|
||||
return new DefaultOrdoEngine(Clock.fixed(NOW, ZoneOffset.UTC), assigneeResolver, RoutingCondition.always(),
|
||||
ActionHandler.noop(),
|
||||
new JdbcTransactionExecutor(connectionProvider),
|
||||
new JdbcProcessDefinitionRepository(connectionProvider),
|
||||
new JdbcProcessInstanceRepository(connectionProvider),
|
||||
|
||||
+2
@@ -1,5 +1,6 @@
|
||||
package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
@@ -219,6 +220,7 @@ class JdbcPostgresIntegrationTest {
|
||||
|
||||
private OrdoEngine newEngine(AssigneeResolver assigneeResolver) {
|
||||
return new DefaultOrdoEngine(Clock.fixed(NOW, ZoneOffset.UTC), assigneeResolver, RoutingCondition.always(),
|
||||
ActionHandler.noop(),
|
||||
new JdbcTransactionExecutor(connectionProvider),
|
||||
new JdbcProcessDefinitionRepository(connectionProvider),
|
||||
new JdbcProcessInstanceRepository(connectionProvider),
|
||||
|
||||
+16
@@ -2,6 +2,7 @@ package com.jetlumen.ordo.storage.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -72,6 +73,21 @@ class JdbcProcessDefinitionRepositoryTest {
|
||||
assertEquals(definition, repository.findById("expense").orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertsAndReadsBackActionStepsWithoutCandidates() {
|
||||
ProcessDefinition definition = new ProcessDefinition("notify", "Notify", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.action("mail", "Send mail", "leave-approved-mail")),
|
||||
List.of(
|
||||
StepTransition.always("manager", "mail"),
|
||||
StepTransition.end("mail")));
|
||||
|
||||
assertTrue(repository.insertIfAbsent(definition));
|
||||
assertEquals(definition, repository.findById("notify").orElseThrow());
|
||||
assertEquals(StepKind.ACTION, repository.findById("notify").orElseThrow().steps().get(1).kind());
|
||||
assertEquals("leave-approved-mail", repository.findById("notify").orElseThrow().steps().get(1).actionKey());
|
||||
}
|
||||
|
||||
private static ProcessDefinition definition(String id, String name) {
|
||||
return ProcessDefinition.linear(id, name, List.of(ApprovalStep.single("lead", "Lead approval", "lee")));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ final class JdbcTestSupport {
|
||||
private static final String[] MIGRATIONS = {
|
||||
"/db/migration/V1__create_ordo_tables.sql",
|
||||
"/db/migration/V2__add_step_candidates_and_policy.sql",
|
||||
"/db/migration/V3__add_step_transitions.sql"
|
||||
"/db/migration/V3__add_step_transitions.sql",
|
||||
"/db/migration/V4__add_step_kind_and_action_key.sql"
|
||||
};
|
||||
private static final String[] SCHEMA_SQL = loadSchemas();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user