Expose draft and executable status in flow views

This commit is contained in:
Lucio Lelii 2026-03-09 14:33:39 +01:00
parent 3d02bec226
commit cbdb5e48d0
6 changed files with 76 additions and 7 deletions

View File

@ -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());

View File

@ -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<FlowView> 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);
}
}

View File

@ -9,6 +9,7 @@ public record FlowView(
LocalDateTime createdAt,
LocalDateTime lastUpdateAt,
String owner,
FlowViewStatus status,
boolean published,
boolean finalized,
FlowData flow) {

View File

@ -0,0 +1,6 @@
package it.cnr.isti.workflow.manager.flows.model;
public enum FlowViewStatus {
DRAFT,
EXECUTABLE
}

View File

@ -21,6 +21,18 @@ public class FlowExecutionValidator {
Validator validator;
public void validate(FlowData flowData) {
List<ValidationError> 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<ValidationError> collectErrors(FlowData flowData) {
List<ValidationError> 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;
}
}

View File

@ -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<FlowView> 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<LLMBlockType> 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<FlowView> createResponse = flowController.createFlow(
request,
new LoginEntity("testuser", "testpassword"));
assertTrue(createResponse.getStatusCode().is2xxSuccessful());
assertNotNull(createResponse.getBody());
assertEquals(FlowViewStatus.DRAFT, createResponse.getBody().status());
}
}