Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE workflow_edges
ADD COLUMN branch_type VARCHAR(20) NOT NULL DEFAULT 'DEFAULT';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE task_executions
ADD COLUMN output jsonb;
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ services:
SLACK_BOT_TOKEN : ${SLACK_BOT_TOKEN}
volumes:
- ./database/migrations:/app/database/migrations
environment:
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}

kafka:
image: apache/kafka:latest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.flowforge.workflowservice.application.execution.condtion;

import com.fasterxml.jackson.databind.JsonNode;
import com.flowforge.workflowservice.common.exception.BusinessException;
import com.flowforge.workflowservice.common.exception.ErrorCode;
import com.flowforge.workflowservice.presentation.dto.request.ConditionConfig;
import org.springframework.stereotype.Component;

@Component
public class ConditionEvaluator {

public boolean evaluate(
JsonNode output,
ConditionConfig config
) {
String field = config.field();
ConditionOperator operator = config.operator();
JsonNode expectedValue = config.value();

JsonNode actualValue = output.get(field);

if (actualValue == null || actualValue.isNull()){
throw new BusinessException(ErrorCode.CONDITION_FIELD_NOT_FOUND);
}

return switch(operator) {
case EQUALS -> actualValue.equals(expectedValue);
case NOT_EQUALS -> !actualValue.equals(expectedValue);
case GREATER_THAN -> {
validateNumeric(actualValue, expectedValue);
yield actualValue.asDouble() > expectedValue.asDouble();
}
case LESS_THAN -> {
validateNumeric(actualValue, expectedValue);
yield actualValue.asDouble() < expectedValue.asDouble();
}
case GREATER_OR_EQUAL -> {
validateNumeric(actualValue, expectedValue);
yield actualValue.asDouble() >= expectedValue.asDouble();
}
case LESS_OR_EQUAL -> {
validateNumeric(actualValue, expectedValue);
yield actualValue.asDouble() <= expectedValue.asDouble();
}
};
}

private void validateNumeric(JsonNode actual, JsonNode expected) {
if (!actual.isNumber() || !expected.isNumber()) {
throw new BusinessException(ErrorCode.INVALID_CONDITION_COMPARISON);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.flowforge.workflowservice.application.execution.condtion;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flowforge.workflowservice.domain.edge.BranchType;
import com.flowforge.workflowservice.domain.node.WorkflowNode;
import com.flowforge.workflowservice.presentation.dto.request.ConditionConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ConditionExecutionService {
private final ObjectMapper objectMapper;
private final ConditionEvaluator conditionEvaluator;

public BranchType execute(
JsonNode previousTaskOutput,
WorkflowNode conditionNode
){
ConditionConfig config = objectMapper.convertValue(
conditionNode.getConfiguration(),
ConditionConfig.class
);

boolean result = conditionEvaluator.evaluate(previousTaskOutput,config);
return result
? BranchType.TRUE
: BranchType.FALSE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.flowforge.workflowservice.application.execution.condtion;

public enum ConditionOperator {
EQUALS,
NOT_EQUALS,
GREATER_THAN,
LESS_THAN,
GREATER_OR_EQUAL,
LESS_OR_EQUAL
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ public void handleTaskCompleted(TaskExecution taskExecution) {
);

List<WorkflowNode> runnableNodes = dependencyResolver.resolveNextNodes(
taskExecution.getWorkflowExecution(),
taskExecution.getNode());
taskExecution);

log.info(
"Resolved next nodes {}",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
package com.flowforge.workflowservice.application.execution.resolver;

import com.flowforge.workflowservice.application.execution.scheduler.NodeScheduler;
import com.flowforge.workflowservice.application.execution.condtion.ConditionExecutionService;
import com.flowforge.workflowservice.common.exception.BusinessException;
import com.flowforge.workflowservice.common.exception.ErrorCode;
import com.flowforge.workflowservice.domain.edge.BranchType;
import com.flowforge.workflowservice.domain.edge.WorkflowEdge;
import com.flowforge.workflowservice.domain.execution.WorkflowExecution;
import com.flowforge.workflowservice.domain.execution.TaskExecution;
import com.flowforge.workflowservice.domain.node.NodeType;
import com.flowforge.workflowservice.domain.node.WorkflowNode;
import com.flowforge.workflowservice.infrastructure.persistence.WorkflowEdgeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class DefaultDependencyResolver implements DependencyResolver {

private final WorkflowEdgeRepository workflowEdgeRepository;
private final ConditionExecutionService conditionExecutionService;

@Override
public List<WorkflowNode> resolveNextNodes(
WorkflowExecution execution,
WorkflowNode completedNode
TaskExecution completedTask
) {
List<WorkflowEdge> edges = workflowEdgeRepository.findBySourceNode(completedNode);
return edges.stream()
.map(WorkflowEdge::getTargetNode)
.toList();
WorkflowNode completedNode = completedTask.getNode();

List<WorkflowNode> nextNodes = new ArrayList<>();

List<WorkflowEdge> edges = workflowEdgeRepository.findBySourceNode(completedNode);

for (WorkflowEdge edge : edges){
WorkflowNode targetNode = edge.getTargetNode();
if (targetNode.getNodeType() != NodeType.CONDITION){
nextNodes.add(targetNode);
continue;
}
BranchType branch = conditionExecutionService.execute(completedTask.getOutput(),targetNode);

WorkflowEdge selectedEdge = workflowEdgeRepository
.findBySourceNodeAndBranchType(targetNode,branch)
.orElseThrow(() ->
new BusinessException(
ErrorCode.CONDITION_BRANCH_NOT_FOUND
));

nextNodes.add(selectedEdge.getTargetNode());

}
return nextNodes;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.flowforge.workflowservice.application.execution.resolver;

import com.flowforge.workflowservice.domain.execution.TaskExecution;
import com.flowforge.workflowservice.domain.execution.WorkflowExecution;
import com.flowforge.workflowservice.domain.node.WorkflowNode;

Expand All @@ -8,8 +9,7 @@
public interface DependencyResolver {

List<WorkflowNode> resolveNextNodes(
WorkflowExecution execution,
WorkflowNode completedNode
TaskExecution taskExecution
);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.flowforge.workflowservice.common.exception.ErrorCode;
import com.flowforge.workflowservice.domain.execution.TaskExecution;
import com.flowforge.workflowservice.domain.execution.WorkflowExecution;
import com.flowforge.workflowservice.domain.node.NodeType;
import com.flowforge.workflowservice.domain.node.WorkflowNode;
import com.flowforge.workflowservice.infrastructure.persistence.TaskExecutionRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -30,6 +31,7 @@ public void schedule(
WorkflowNode node
) {
log.info("Entered NodeScheduler");

TaskExecution taskExecution =
taskExecutionRepository
.findByWorkflowExecutionIdAndNodeId(
Expand All @@ -44,6 +46,7 @@ public void schedule(
node.getNodeType()
);


taskStateMachine.startTaskExecution(taskExecution);
taskExecutionRepository.save(taskExecution);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ public enum ErrorCode {
INVALID_WORKFLOW_STATE_TRANSITION(HttpStatus.BAD_REQUEST, "Invalid workflow execution state transition"),
INVALID_TASK_STATE_TRANSITION(HttpStatus.BAD_REQUEST, "Invalid task execution state transition"),
TASK_EXECUTION_NOT_FOUND(HttpStatus.NOT_FOUND, "Task execution not found"),
INVALID_EMAIL_CONFIGURATION(HttpStatus.BAD_REQUEST,"Invalid Email Configuration" );
INVALID_EMAIL_CONFIGURATION(HttpStatus.BAD_REQUEST,"Invalid Email Configuration" ),
CONDITION_FIELD_NOT_FOUND(HttpStatus.NOT_FOUND,"Condition field not found in task output"),
CONDITION_BRANCH_NOT_FOUND(HttpStatus.NOT_FOUND,"condition branch not found"),
INVALID_CONDITION_COMPARISON(HttpStatus.BAD_REQUEST, "Invalid condition comparison"),
INVALID_CONDITION_OPERATOR(HttpStatus.BAD_REQUEST, "Unsupported condition operator");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.flowforge.workflowservice.domain.edge;

public enum BranchType {
TRUE,
FALSE,
DEFAULT
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ public class WorkflowEdge {

@CreationTimestamp
private Instant createdAt;

@Builder.Default
@Enumerated(EnumType.STRING)
@Column(name = "branch_type", nullable = false)
private BranchType branchType = BranchType.DEFAULT;

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.flowforge.workflowservice.domain.execution;
import com.fasterxml.jackson.databind.JsonNode;
import com.flowforge.workflowservice.domain.node.WorkflowNode;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import java.time.Instant;
import java.util.UUID;

Expand Down Expand Up @@ -45,4 +49,9 @@ public class TaskExecution {
private Integer retryCount = 0;

private String errorMessage;

@JdbcTypeCode(SqlTypes.JSON)
@Column(columnDefinition = "jsonb")
private JsonNode output;

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum NodeType {
TASK,
EMAIL,
HTTP_REQUEST,
SLACK,

CONDITION,
APPROVAL,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package com.flowforge.workflowservice.infrastructure.persistence;

import com.flowforge.workflowservice.domain.edge.BranchType;
import com.flowforge.workflowservice.domain.edge.WorkflowEdge;
import com.flowforge.workflowservice.domain.node.WorkflowNode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface WorkflowEdgeRepository extends JpaRepository<WorkflowEdge, UUID> {
List<WorkflowEdge> findByWorkflow_Id(UUID workflowId);
void deleteByWorkflow_Id(UUID workflowId);
List<WorkflowEdge> findBySourceNode(WorkflowNode sourceNode);
Optional<WorkflowEdge> findBySourceNodeAndBranchType(
WorkflowNode sourceNode,
BranchType branchType
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.flowforge.workflowservice.presentation.dto.request;

import com.fasterxml.jackson.databind.JsonNode;
import com.flowforge.workflowservice.application.execution.condtion.ConditionOperator;

public record ConditionConfig(
String field,
ConditionOperator operator,
JsonNode value

) {}
3 changes: 0 additions & 3 deletions workflow-service/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=*
spring.kafka.consumer.properties.spring.json.use.type.headers=false
spring.kafka.consumer.properties.spring.json.value.default.type=com.flowforge.workflowservice.application.execution.event.TaskCompletedEvent
spring.kafka.consumer.properties.spring.deserializer.value.delegate.class=org.springframework.kafka.support.serializer.JsonDeserializer

# Listener
spring.kafka.listener.ack-mode=record
Expand Down