feat: add optional Spring REST adapter and catalog SPI
Expose OrdoEngine over conditionally registered HTTP endpoints in autoconfigure, keep JDBC wiring in spring.jdbc, and share definition JSON via ProcessDefinitionParser.toJson. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -66,6 +66,11 @@
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
||||
+28
@@ -12,6 +12,7 @@ public class OrdoProperties {
|
||||
private final Definitions definitions = new Definitions();
|
||||
private final Due due = new Due();
|
||||
private final Jdbc jdbc = new Jdbc();
|
||||
private final Rest rest = new Rest();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@@ -33,6 +34,10 @@ public class OrdoProperties {
|
||||
return jdbc;
|
||||
}
|
||||
|
||||
public Rest getRest() {
|
||||
return rest;
|
||||
}
|
||||
|
||||
public static class Definitions {
|
||||
private String location = "classpath*:ordo/*.json";
|
||||
|
||||
@@ -70,4 +75,27 @@ public class OrdoProperties {
|
||||
this.dialect = dialect;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Rest {
|
||||
/** When false, REST beans are not registered even if the web stack is present. */
|
||||
private boolean enabled;
|
||||
|
||||
private String basePath = "/ordo";
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getBasePath() {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
public void setBasePath(String basePath) {
|
||||
this.basePath = basePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
|
||||
import org.flywaydb.core.Flyway;
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
@@ -12,6 +12,7 @@ import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.api.repository.ProcessHistoryRepository;
|
||||
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
|
||||
import com.jetlumen.ordo.core.DefaultOrdoEngine;
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcConnectionProvider;
|
||||
@@ -46,13 +47,11 @@ import java.time.Clock;
|
||||
@ConditionalOnClass(DataSource.class)
|
||||
@ConditionalOnBean(DataSource.class)
|
||||
@AutoConfigureAfter(name = {
|
||||
// Spring Boot 3.x locations
|
||||
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration",
|
||||
// Spring Boot 4.x locations (JDBC/Flyway autoconfiguration moved to dedicated modules)
|
||||
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
|
||||
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration",
|
||||
"com.jetlumen.ordo.spring.OrdoFlywayAutoConfiguration"
|
||||
"com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration"
|
||||
})
|
||||
@EnableConfigurationProperties(OrdoProperties.class)
|
||||
public class OrdoJdbcAutoConfiguration {
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
record ActorCommentRequest(String actor, String comment) {
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessDefinitionParser;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
final class DefinitionJson {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final TypeReference<Map<String, Object>> MAP = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private DefinitionJson() {
|
||||
}
|
||||
|
||||
static Map<String, Object> map(ProcessDefinition definition) {
|
||||
try {
|
||||
return MAPPER.readValue(ProcessDefinitionParser.toJson(definition), MAP);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static String write(Map<String, Object> body) {
|
||||
try {
|
||||
return MAPPER.writeValueAsString(body);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("invalid process definition json", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
record ErrorBody(String error, String message) {
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${ordo.rest.base-path:/ordo}")
|
||||
public class OrdoCatalogController {
|
||||
private final OrdoCatalog catalog;
|
||||
|
||||
public OrdoCatalogController(OrdoCatalog catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
|
||||
@GetMapping("/catalog/conditions")
|
||||
public List<OrdoCatalog.Item> conditions() {
|
||||
return catalog.conditions();
|
||||
}
|
||||
|
||||
@GetMapping("/catalog/actions")
|
||||
public List<OrdoCatalog.Item> actions() {
|
||||
return catalog.actions();
|
||||
}
|
||||
|
||||
@GetMapping("/catalog/assignees")
|
||||
public List<OrdoCatalog.Item> assignees() {
|
||||
return catalog.assignees();
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessDefinitionParser;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${ordo.rest.base-path:/ordo}")
|
||||
public class OrdoDefinitionController {
|
||||
private final OrdoEngine engine;
|
||||
|
||||
public OrdoDefinitionController(OrdoEngine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@PostMapping("/definitions/parse")
|
||||
public Map<String, Object> parse(@RequestBody Map<String, Object> body) {
|
||||
return DefinitionJson.map(ProcessDefinitionParser.fromJson(DefinitionJson.write(body)));
|
||||
}
|
||||
|
||||
@PostMapping("/definitions")
|
||||
public ResponseEntity<Map<String, Object>> publish(@RequestBody Map<String, Object> body) {
|
||||
ProcessDefinition published = engine.publish(ProcessDefinitionParser.fromJson(DefinitionJson.write(body)));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(DefinitionJson.map(published));
|
||||
}
|
||||
|
||||
@GetMapping("/definitions")
|
||||
public PageResponse<Map<String, Object>> query(
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
var result = engine.queryDefinitions(PageRequest.of(page, size));
|
||||
List<Map<String, Object>> content = result.content().stream()
|
||||
.map(DefinitionJson::map)
|
||||
.toList();
|
||||
return new PageResponse<>(content, result.totalElements(), result.page(), result.size(),
|
||||
result.totalPages(), result.hasNext());
|
||||
}
|
||||
|
||||
@GetMapping("/definitions/{definitionId}")
|
||||
public ResponseEntity<Map<String, Object>> findLatest(@PathVariable("definitionId") String definitionId) {
|
||||
return engine.findDefinition(definitionId)
|
||||
.map(definition -> ResponseEntity.ok(DefinitionJson.map(definition)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/definitions/{definitionId}/versions")
|
||||
public PageResponse<Map<String, Object>> versions(
|
||||
@PathVariable("definitionId") String definitionId,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
var result = engine.queryDefinitionVersions(definitionId, PageRequest.of(page, size));
|
||||
List<Map<String, Object>> content = result.content().stream()
|
||||
.map(DefinitionJson::map)
|
||||
.toList();
|
||||
return new PageResponse<>(content, result.totalElements(), result.page(), result.size(),
|
||||
result.totalPages(), result.hasNext());
|
||||
}
|
||||
|
||||
@GetMapping("/definitions/{definitionId}/versions/{version}")
|
||||
public ResponseEntity<Map<String, Object>> findVersion(@PathVariable("definitionId") String definitionId,
|
||||
@PathVariable("version") int version) {
|
||||
return engine.findDefinition(definitionId, version)
|
||||
.map(definition -> ResponseEntity.ok(DefinitionJson.map(definition)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${ordo.rest.base-path:/ordo}")
|
||||
public class OrdoDueController {
|
||||
static final int DEFAULT_LIMIT = 100;
|
||||
|
||||
private final OrdoEngine engine;
|
||||
|
||||
public OrdoDueController(OrdoEngine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@PostMapping("/due")
|
||||
public Map<String, Integer> processDue(
|
||||
@RequestParam(name = "limit", required = false) Integer limit,
|
||||
@RequestBody(required = false) ProcessDueRequest body) {
|
||||
int resolved = limit != null ? limit
|
||||
: body != null && body.limit() != null ? body.limit()
|
||||
: DEFAULT_LIMIT;
|
||||
return Map.of("processed", engine.processDue(resolved));
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionExecution;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.query.InstanceQuery;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${ordo.rest.base-path:/ordo}")
|
||||
public class OrdoInstanceController {
|
||||
private final OrdoEngine engine;
|
||||
|
||||
public OrdoInstanceController(OrdoEngine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@PostMapping("/instances")
|
||||
public ProcessInstance start(@RequestBody StartInstanceRequest request) {
|
||||
Map<String, Object> variables = request.context() == null || request.context().variables() == null
|
||||
? Map.of()
|
||||
: request.context().variables();
|
||||
return engine.start(request.definitionId(), request.initiator(), new ProcessContext(variables));
|
||||
}
|
||||
|
||||
@GetMapping("/instances/{instanceId}")
|
||||
public ResponseEntity<ProcessInstance> find(@PathVariable("instanceId") String instanceId) {
|
||||
return engine.findInstance(instanceId)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/instances")
|
||||
public PageResponse<ProcessInstance> query(
|
||||
@RequestParam(name = "definitionId", required = false) String definitionId,
|
||||
@RequestParam(name = "status", required = false) ProcessStatus status,
|
||||
@RequestParam(name = "initiator", required = false) String initiator,
|
||||
@RequestParam(name = "startedFrom", required = false) Instant startedFrom,
|
||||
@RequestParam(name = "startedTo", required = false) Instant startedTo,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
InstanceQuery query = InstanceQuery.any()
|
||||
.withDefinitionId(definitionId)
|
||||
.withStatus(status)
|
||||
.withInitiator(initiator)
|
||||
.withStartedBetween(startedFrom, startedTo);
|
||||
return PageResponse.of(engine.queryInstances(query, PageRequest.of(page, size)));
|
||||
}
|
||||
|
||||
@PostMapping("/instances/{instanceId}/withdraw")
|
||||
public ProcessInstance withdraw(@PathVariable("instanceId") String instanceId,
|
||||
@RequestBody ActorCommentRequest request) {
|
||||
return engine.withdraw(instanceId, request.actor(), request.comment());
|
||||
}
|
||||
|
||||
@PostMapping("/instances/{instanceId}/cancel")
|
||||
public ProcessInstance cancel(@PathVariable("instanceId") String instanceId,
|
||||
@RequestBody ActorCommentRequest request) {
|
||||
return engine.cancel(instanceId, request.actor(), request.comment());
|
||||
}
|
||||
|
||||
@GetMapping("/instances/{instanceId}/tasks")
|
||||
public List<ApprovalTask> tasks(@PathVariable("instanceId") String instanceId) {
|
||||
return engine.findTasks(instanceId);
|
||||
}
|
||||
|
||||
@GetMapping("/instances/{instanceId}/tasks/pending")
|
||||
public List<ApprovalTask> pendingTasks(@PathVariable("instanceId") String instanceId) {
|
||||
return engine.findPendingTasksByInstanceId(instanceId);
|
||||
}
|
||||
|
||||
@GetMapping("/instances/{instanceId}/history")
|
||||
public PageResponse<ProcessEvent> history(
|
||||
@PathVariable("instanceId") String instanceId,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
return PageResponse.of(engine.queryHistory(instanceId, PageRequest.of(page, size)));
|
||||
}
|
||||
|
||||
@GetMapping("/instances/{instanceId}/action-executions")
|
||||
public PageResponse<ActionExecution> actionExecutions(
|
||||
@PathVariable("instanceId") String instanceId,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
return PageResponse.of(engine.queryActionExecutions(instanceId, PageRequest.of(page, size)));
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(DispatcherServlet.class)
|
||||
@ConditionalOnProperty(prefix = "ordo.rest", name = "enabled", havingValue = "true")
|
||||
@ConditionalOnBean(OrdoEngine.class)
|
||||
@AutoConfigureAfter(OrdoJdbcAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(OrdoProperties.class)
|
||||
@Import({
|
||||
OrdoDefinitionController.class,
|
||||
OrdoInstanceController.class,
|
||||
OrdoTaskController.class,
|
||||
OrdoDueController.class,
|
||||
OrdoCatalogController.class,
|
||||
OrdoRestExceptionHandler.class
|
||||
})
|
||||
public class OrdoRestAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OrdoCatalog ordoCatalog() {
|
||||
return OrdoCatalog.empty();
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.NoRouteFoundException;
|
||||
import com.jetlumen.ordo.api.exception.OrdoException;
|
||||
import com.jetlumen.ordo.api.exception.TaskAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.api.exception.TaskNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.UnauthorizedInstanceOperationException;
|
||||
import com.jetlumen.ordo.api.exception.UnauthorizedTaskOperationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice(basePackageClasses = OrdoRestAutoConfiguration.class)
|
||||
public class OrdoRestExceptionHandler {
|
||||
|
||||
@ExceptionHandler({
|
||||
DefinitionNotFoundException.class,
|
||||
InstanceNotFoundException.class,
|
||||
TaskNotFoundException.class
|
||||
})
|
||||
public ResponseEntity<ErrorBody> notFound(OrdoException exception) {
|
||||
return respond(HttpStatus.NOT_FOUND, exception);
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
UnauthorizedTaskOperationException.class,
|
||||
UnauthorizedInstanceOperationException.class
|
||||
})
|
||||
public ResponseEntity<ErrorBody> forbidden(OrdoException exception) {
|
||||
return respond(HttpStatus.FORBIDDEN, exception);
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
TaskAlreadyCompletedException.class,
|
||||
InstanceAlreadyCompletedException.class,
|
||||
NoRouteFoundException.class
|
||||
})
|
||||
public ResponseEntity<ErrorBody> conflict(OrdoException exception) {
|
||||
return respond(HttpStatus.CONFLICT, exception);
|
||||
}
|
||||
|
||||
@ExceptionHandler({IllegalArgumentException.class, HttpMessageNotReadableException.class})
|
||||
public ResponseEntity<ErrorBody> badRequest(Exception exception) {
|
||||
return respond(HttpStatus.BAD_REQUEST, exception);
|
||||
}
|
||||
|
||||
@ExceptionHandler({OrdoException.class, IllegalStateException.class})
|
||||
public ResponseEntity<ErrorBody> serverError(RuntimeException exception) {
|
||||
return respond(HttpStatus.INTERNAL_SERVER_ERROR, exception);
|
||||
}
|
||||
|
||||
private static ResponseEntity<ErrorBody> respond(HttpStatus status, Exception exception) {
|
||||
return ResponseEntity.status(status)
|
||||
.body(new ErrorBody(exception.getClass().getSimpleName(), exception.getMessage()));
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${ordo.rest.base-path:/ordo}")
|
||||
public class OrdoTaskController {
|
||||
private final OrdoEngine engine;
|
||||
|
||||
public OrdoTaskController(OrdoEngine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/pending")
|
||||
public List<ApprovalTask> pending(@RequestParam("assignee") String assignee) {
|
||||
return engine.findPendingTasksByAssignee(assignee);
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}")
|
||||
public ResponseEntity<ApprovalTask> find(@PathVariable("taskId") String taskId) {
|
||||
return engine.findTask(taskId)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/tasks")
|
||||
public PageResponse<ApprovalTask> query(
|
||||
@RequestParam(name = "assignee", required = false) String assignee,
|
||||
@RequestParam(name = "instanceId", required = false) String instanceId,
|
||||
@RequestParam(name = "definitionId", required = false) String definitionId,
|
||||
@RequestParam(name = "status", required = false) TaskStatus status,
|
||||
@RequestParam(name = "createdFrom", required = false) Instant createdFrom,
|
||||
@RequestParam(name = "createdTo", required = false) Instant createdTo,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size) {
|
||||
TaskQuery query = TaskQuery.any()
|
||||
.withAssignee(assignee)
|
||||
.withInstanceId(instanceId)
|
||||
.withDefinitionId(definitionId)
|
||||
.withStatus(status)
|
||||
.withCreatedBetween(createdFrom, createdTo);
|
||||
return PageResponse.of(engine.queryTasks(query, PageRequest.of(page, size)));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/approve")
|
||||
public ApprovalTask approve(@PathVariable("taskId") String taskId, @RequestBody ActorCommentRequest request) {
|
||||
return engine.approve(taskId, request.actor(), request.comment());
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/reject")
|
||||
public ApprovalTask reject(@PathVariable("taskId") String taskId, @RequestBody ActorCommentRequest request) {
|
||||
return engine.reject(taskId, request.actor(), request.comment());
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/reassign")
|
||||
public ApprovalTask reassign(@PathVariable("taskId") String taskId, @RequestBody ReassignTaskRequest request) {
|
||||
return engine.reassign(taskId, request.actor(), request.newAssignee());
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.query.Page;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record PageResponse<T>(List<T> content, long totalElements, int page, int size, int totalPages, boolean hasNext) {
|
||||
static <T> PageResponse<T> of(Page<T> page) {
|
||||
return new PageResponse<>(page.content(), page.totalElements(), page.page(), page.size(),
|
||||
page.totalPages(), page.hasNext());
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
record ProcessDueRequest(Integer limit) {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
record ReassignTaskRequest(String actor, String newAssignee) {
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
record StartInstanceRequest(String definitionId, String initiator, ContextBody context) {
|
||||
record ContextBody(Map<String, Object> variables) {
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,2 +1,3 @@
|
||||
com.jetlumen.ordo.spring.OrdoFlywayAutoConfiguration
|
||||
com.jetlumen.ordo.spring.OrdoJdbcAutoConfiguration
|
||||
com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration
|
||||
com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration
|
||||
com.jetlumen.ordo.spring.rest.OrdoRestAutoConfiguration
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.jetlumen.ordo.spring;
|
||||
package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class OrdoRestAutoConfigurationTest {
|
||||
|
||||
private static final String LINEAR_JSON = """
|
||||
{
|
||||
"id": "leave-request",
|
||||
"name": "Leave request",
|
||||
"steps": [
|
||||
{ "id": "manager", "name": "Manager approval", "candidates": ["maria"] }
|
||||
],
|
||||
"transitions": [
|
||||
{ "from": "manager", "to": null }
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(
|
||||
DataSourceAutoConfiguration.class,
|
||||
OrdoFlywayAutoConfiguration.class,
|
||||
FlywayAutoConfiguration.class,
|
||||
OrdoJdbcAutoConfiguration.class,
|
||||
JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class,
|
||||
DispatcherServletAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class,
|
||||
OrdoRestAutoConfiguration.class))
|
||||
.withPropertyValues(
|
||||
"spring.datasource.url=jdbc:h2:mem:ordo_rest_" + UUID.randomUUID()
|
||||
+ ";MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1",
|
||||
"spring.datasource.username=sa");
|
||||
|
||||
@Test
|
||||
void doesNotRegisterControllersWhenRestDisabled() {
|
||||
runner.run(context -> {
|
||||
assertThat(context).hasSingleBean(OrdoEngine.class);
|
||||
assertThat(context).doesNotHaveBean(OrdoDefinitionController.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registersControllersWhenEnabled() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true").run(context ->
|
||||
assertThat(context).hasSingleBean(OrdoDefinitionController.class)
|
||||
.hasSingleBean(OrdoInstanceController.class)
|
||||
.hasSingleBean(OrdoTaskController.class)
|
||||
.hasSingleBean(OrdoDueController.class)
|
||||
.hasSingleBean(OrdoCatalogController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePublishStartApproveAndErrorMappings() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true").run(context -> {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
|
||||
mvc.perform(post("/ordo/definitions/parse")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LINEAR_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value("leave-request"))
|
||||
.andExpect(jsonPath("$.steps[0].id").value("manager"));
|
||||
|
||||
mvc.perform(post("/ordo/definitions")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(LINEAR_JSON))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.version").value(1));
|
||||
|
||||
String startBody = mvc.perform(post("/ordo/instances")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"definitionId":"leave-request","initiator":"alice"}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
assertThat(startBody).contains("\"initiator\":\"alice\"");
|
||||
|
||||
MvcResult pending = mvc.perform(get("/ordo/tasks/pending").param("assignee", "maria"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].id").exists())
|
||||
.andReturn();
|
||||
String id = JsonPath.read(pending.getResponse().getContentAsString(), "$[0].id");
|
||||
|
||||
mvc.perform(post("/ordo/tasks/" + id + "/approve")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"actor":"bob"}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error").value("UnauthorizedTaskOperationException"));
|
||||
|
||||
mvc.perform(post("/ordo/tasks/" + id + "/approve")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"actor":"maria","comment":"ok"}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("APPROVED"));
|
||||
|
||||
mvc.perform(post("/ordo/tasks/" + id + "/approve")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"actor":"maria"}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error").value("TaskAlreadyCompletedException"));
|
||||
|
||||
mvc.perform(post("/ordo/instances")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"definitionId":"missing","initiator":"alice"}
|
||||
"""))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.error").value("DefinitionNotFoundException"));
|
||||
|
||||
mvc.perform(post("/ordo/definitions/parse")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{"))
|
||||
.andExpect(status().isBadRequest());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user