From cbdb5e48d09d9cd76002772e7129ccca43d5020b Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Mon, 9 Mar 2026 14:33:39 +0100 Subject: [PATCH] Expose draft and executable status in flow views --- .../workflow/manager/flows/FlowMapper.java | 4 +- .../workflow/manager/flows/FlowService.java | 18 +++++++-- .../manager/flows/model/FlowView.java | 1 + .../manager/flows/model/FlowViewStatus.java | 6 +++ .../validation/FlowExecutionValidator.java | 16 ++++++-- .../controllers/FlowControllerTest.java | 38 +++++++++++++++++++ 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowViewStatus.java diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/FlowMapper.java b/src/main/java/it/cnr/isti/workflow/manager/flows/FlowMapper.java index af1408f..df19a7d 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/FlowMapper.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/FlowMapper.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest; import it.cnr.isti.workflow.manager.flows.model.FlowView; +import it.cnr.isti.workflow.manager.flows.model.FlowViewStatus; import it.cnr.isti.workflow.manager.flows.repo.FlowEntity; public final class FlowMapper { @@ -30,7 +31,7 @@ public final class FlowMapper { entity.setLastUpdateAt(LocalDateTime.now()); } - public static FlowView toView(FlowEntity entity) { + public static FlowView toView(FlowEntity entity, FlowViewStatus status) { return new FlowView( entity.getId(), entity.getName(), @@ -38,6 +39,7 @@ public final class FlowMapper { entity.getCreatedAt(), entity.getLastUpdateAt(), entity.getOwner(), + status, entity.isPublished(), entity.isFinalized(), entity.getFlow()); diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/FlowService.java b/src/main/java/it/cnr/isti/workflow/manager/flows/FlowService.java index 7878913..9213746 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/FlowService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/FlowService.java @@ -9,8 +9,10 @@ import org.springframework.web.server.ResponseStatusException; import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest; import it.cnr.isti.workflow.manager.flows.model.FlowView; +import it.cnr.isti.workflow.manager.flows.model.FlowViewStatus; import it.cnr.isti.workflow.manager.flows.repo.FlowEntity; import it.cnr.isti.workflow.manager.flows.repo.FlowRepository; +import it.cnr.isti.workflow.manager.flows.validation.FlowExecutionValidator; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; @@ -28,11 +30,14 @@ public class FlowService { @Autowired Validator validator; + @Autowired + FlowExecutionValidator flowExecutionValidator; + public FlowView createFlow(String owner, FlowCreateRequest request) { validateFlow(request); FlowEntity flowEntity = FlowMapper.toNewEntity(owner, request); FlowEntity savedEntity = flowRepository.save(flowEntity); - return FlowMapper.toView(savedEntity); + return toView(savedEntity); } public FlowView updateFlow(String id, String owner, FlowCreateRequest request) { @@ -47,7 +52,7 @@ public class FlowService { validateFlow(request); FlowMapper.updateEntity(entity, request); FlowEntity savedEntity = flowRepository.save(entity); - return FlowMapper.toView(savedEntity); + return toView(savedEntity); } public void deleteFlow(String id, String owner) { @@ -63,7 +68,7 @@ public class FlowService { } public List getAllFlows() { - return flowRepository.findAll().stream().map(FlowMapper::toView).toList(); + return flowRepository.findAll().stream().map(this::toView).toList(); } private void validateFlow(FlowCreateRequest request) { @@ -78,4 +83,11 @@ public class FlowService { logger.warn("Flow validation failed: {}", message); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); } + + private FlowView toView(FlowEntity entity) { + FlowViewStatus status = flowExecutionValidator.isExecutable(entity.getFlow()) + ? FlowViewStatus.EXECUTABLE + : FlowViewStatus.DRAFT; + return FlowMapper.toView(entity, status); + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowView.java b/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowView.java index 5078f37..9a3ed69 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowView.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowView.java @@ -9,6 +9,7 @@ public record FlowView( LocalDateTime createdAt, LocalDateTime lastUpdateAt, String owner, + FlowViewStatus status, boolean published, boolean finalized, FlowData flow) { diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowViewStatus.java b/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowViewStatus.java new file mode 100644 index 0000000..f03c921 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/model/FlowViewStatus.java @@ -0,0 +1,6 @@ +package it.cnr.isti.workflow.manager.flows.model; + +public enum FlowViewStatus { + DRAFT, + EXECUTABLE +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowExecutionValidator.java b/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowExecutionValidator.java index f5d6a1c..6eb6208 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowExecutionValidator.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/validation/FlowExecutionValidator.java @@ -21,6 +21,18 @@ public class FlowExecutionValidator { Validator validator; public void validate(FlowData flowData) { + List errors = collectErrors(flowData); + + if (!errors.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ValidationErrorCodec.encode(errors)); + } + } + + public boolean isExecutable(FlowData flowData) { + return collectErrors(flowData).isEmpty(); + } + + private List collectErrors(FlowData flowData) { List errors = new ArrayList<>(); errors.addAll(validator.validate(flowData).stream() @@ -43,8 +55,6 @@ public class FlowExecutionValidator { } } - if (!errors.isEmpty()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ValidationErrorCodec.encode(errors)); - } + return errors; } } diff --git a/src/test/java/it/cnr/isti/workflow/manager/controllers/FlowControllerTest.java b/src/test/java/it/cnr/isti/workflow/manager/controllers/FlowControllerTest.java index 1ab0f51..262a309 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/controllers/FlowControllerTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/controllers/FlowControllerTest.java @@ -25,11 +25,15 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import it.cnr.isti.workflow.manager.app.ObjectMapperHolder; import it.cnr.isti.workflow.manager.auth.config.JwtUtil; import it.cnr.isti.workflow.manager.auth.repo.LoginEntity; +import it.cnr.isti.workflow.manager.blocks.Block; +import it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration; +import it.cnr.isti.workflow.manager.blocks.types.LLMBlockType; import it.cnr.isti.workflow.manager.flows.FlowTestCreator; import it.cnr.isti.workflow.manager.flows.model.Flow; import it.cnr.isti.workflow.manager.flows.model.FlowCreateRequest; import it.cnr.isti.workflow.manager.flows.model.FlowData; import it.cnr.isti.workflow.manager.flows.model.FlowView; +import it.cnr.isti.workflow.manager.flows.model.FlowViewStatus; import it.cnr.isti.workflow.manager.llms.LLMDescriptor; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -44,6 +48,9 @@ public class FlowControllerTest { @Autowired private FlowController flowController; + @Autowired + private BlocksController blocksController; + @Autowired private MockMvc mockMvc; @@ -77,6 +84,7 @@ public class FlowControllerTest { assertNotNull(createResponse.getBody()); FlowView created = createResponse.getBody(); assertNotNull(created.id()); + assertEquals(FlowViewStatus.EXECUTABLE, created.status()); List flows = flowController.getAllFlows(); assertNotNull(flows); @@ -97,6 +105,7 @@ public class FlowControllerTest { assertEquals("testuser", retrieved.owner()); assertEquals(flow.getName(), retrieved.name()); assertEquals(flow.getDescription(), retrieved.description()); + assertEquals(FlowViewStatus.EXECUTABLE, retrieved.status()); assertNotNull(retrieved.flow()); assertEquals(flow.getBlocks().size(), retrieved.flow().getBlocks().size()); assertEquals(flow.getConnections().size(), retrieved.flow().getConnections().size()); @@ -150,6 +159,7 @@ public class FlowControllerTest { || updated.lastUpdateAt().isEqual(created.lastUpdateAt())); assertEquals(updatedFlowDefinition.getName(), updated.name()); assertEquals(updatedFlowDefinition.getDescription(), updated.description()); + assertEquals(FlowViewStatus.EXECUTABLE, updated.status()); assertNotNull(updated.flow()); assertEquals(updatedFlowDefinition.getBlocks().size(), updated.flow().getBlocks().size()); assertEquals(updatedFlowDefinition.getConnections().size(), updated.flow().getConnections().size()); @@ -312,4 +322,32 @@ public class FlowControllerTest { .andExpect(jsonPath("$.errors[0].field").value("inputs")) .andExpect(jsonPath("$.errors[0].message").value("Inputs do not match its configuration")); } + + @Test + public void createDraftFlowReturnsDraftStatus() { + Block draftBlock = blocksController.create(LLMBlockConfiguration.empty()); + draftBlock.asSink(); + + Flow draftFlow = Flow.builder() + .name("Draft Flow") + .description("Incomplete but structurally valid flow") + .block(draftBlock) + .build(); + + FlowCreateRequest request = new FlowCreateRequest( + draftFlow.getName(), + draftFlow.getDescription(), + FlowData.builder() + .blocks(draftFlow.getBlocks()) + .connections(draftFlow.getConnections()) + .build()); + + ResponseEntity createResponse = flowController.createFlow( + request, + new LoginEntity("testuser", "testpassword")); + + assertTrue(createResponse.getStatusCode().is2xxSuccessful()); + assertNotNull(createResponse.getBody()); + assertEquals(FlowViewStatus.DRAFT, createResponse.getBody().status()); + } }