Compare commits

...

2 Commits

Author SHA1 Message Date
Yoan.liu
faad7cb6d7 improve condition evaluate 2025-05-20 22:54:41 +08:00
Yoan.liu
97d692910b expression evaluate 2025-05-20 18:09:42 +08:00
17 changed files with 910 additions and 9 deletions

522
internal/domain/expr.go Normal file
View File

@ -0,0 +1,522 @@
package domain
import (
"encoding/json"
"fmt"
)
type Value any
type (
ComparisonOperator string
LogicalOperator string
)
const (
GreaterThan ComparisonOperator = ">"
LessThan ComparisonOperator = "<"
GreaterOrEqual ComparisonOperator = ">="
LessOrEqual ComparisonOperator = "<="
Equal ComparisonOperator = "=="
NotEqual ComparisonOperator = "!="
Is ComparisonOperator = "is"
And LogicalOperator = "and"
Or LogicalOperator = "or"
Not LogicalOperator = "not"
)
type EvalResult struct {
Type string
Value any
}
func (e *EvalResult) GetFloat64() (float64, error) {
if e.Type != "number" {
return 0, fmt.Errorf("type mismatch: %s", e.Type)
}
switch v := e.Value.(type) {
case int:
return float64(v), nil
case float64:
return v, nil
default:
return 0, fmt.Errorf("unsupported type: %T", v)
}
}
func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "number":
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: "boolean",
Value: left > right,
}, nil
case "string":
return &EvalResult{
Type: "boolean",
Value: e.Value.(string) > other.Value.(string),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "number":
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: "boolean",
Value: left >= right,
}, nil
case "string":
return &EvalResult{
Type: "boolean",
Value: e.Value.(string) >= other.Value.(string),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "number":
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: "boolean",
Value: left < right,
}, nil
case "string":
return &EvalResult{
Type: "boolean",
Value: e.Value.(string) < other.Value.(string),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "number":
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: "boolean",
Value: left <= right,
}, nil
case "string":
return &EvalResult{
Type: "boolean",
Value: e.Value.(string) <= other.Value.(string),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "number":
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: "boolean",
Value: left == right,
}, nil
case "string":
return &EvalResult{
Type: "boolean",
Value: e.Value.(string) == other.Value.(string),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "number":
left, err := e.GetFloat64()
if err != nil {
return nil, err
}
right, err := other.GetFloat64()
if err != nil {
return nil, err
}
return &EvalResult{
Type: "boolean",
Value: left != right,
}, nil
case "string":
return &EvalResult{
Type: "boolean",
Value: e.Value.(string) != other.Value.(string),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "boolean":
return &EvalResult{
Type: "boolean",
Value: e.Value.(bool) && other.Value.(bool),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "boolean":
return &EvalResult{
Type: "boolean",
Value: e.Value.(bool) || other.Value.(bool),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
func (e *EvalResult) Not() (*EvalResult, error) {
if e.Type != "boolean" {
return nil, fmt.Errorf("type mismatch: %s", e.Type)
}
return &EvalResult{
Type: "boolean",
Value: !e.Value.(bool),
}, nil
}
func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) {
if e.Type != other.Type {
return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type)
}
switch e.Type {
case "boolean":
return &EvalResult{
Type: "boolean",
Value: e.Value.(bool) == other.Value.(bool),
}, nil
default:
return nil, fmt.Errorf("unsupported type: %s", e.Type)
}
}
type Expr interface {
GetType() string
Eval(variables map[string]map[string]any) (*EvalResult, error)
}
type ConstExpr struct {
Type string `json:"type"`
Value Value `json:"value"`
ValueType string `json:"valueType"`
}
func (c ConstExpr) GetType() string { return c.Type }
func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
return &EvalResult{
Type: c.ValueType,
Value: c.Value,
}, nil
}
type VarExpr struct {
Type string `json:"type"`
Selector WorkflowNodeIOValueSelector `json:"selector"`
}
func (v VarExpr) GetType() string { return v.Type }
func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
if v.Selector.Id == "" {
return nil, fmt.Errorf("node id is empty")
}
if v.Selector.Name == "" {
return nil, fmt.Errorf("name is empty")
}
if _, ok := variables[v.Selector.Id]; !ok {
return nil, fmt.Errorf("node %s not found", v.Selector.Id)
}
if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok {
return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id)
}
return &EvalResult{
Type: v.Selector.Type,
Value: variables[v.Selector.Id][v.Selector.Name],
}, nil
}
type CompareExpr struct {
Type string `json:"type"` // compare
Op ComparisonOperator `json:"op"`
Left Expr `json:"left"`
Right Expr `json:"right"`
}
func (c CompareExpr) GetType() string { return c.Type }
func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
left, err := c.Left.Eval(variables)
if err != nil {
return nil, err
}
right, err := c.Right.Eval(variables)
if err != nil {
return nil, err
}
switch c.Op {
case GreaterThan:
return left.GreaterThan(right)
case LessThan:
return left.LessThan(right)
case GreaterOrEqual:
return left.GreaterOrEqual(right)
case LessOrEqual:
return left.LessOrEqual(right)
case Equal:
return left.Equal(right)
case NotEqual:
return left.NotEqual(right)
case Is:
return left.Is(right)
default:
return nil, fmt.Errorf("unknown operator: %s", c.Op)
}
}
type LogicalExpr struct {
Type string `json:"type"` // logical
Op LogicalOperator `json:"op"`
Left Expr `json:"left"`
Right Expr `json:"right"`
}
func (l LogicalExpr) GetType() string { return l.Type }
func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
left, err := l.Left.Eval(variables)
if err != nil {
return nil, err
}
right, err := l.Right.Eval(variables)
if err != nil {
return nil, err
}
switch l.Op {
case And:
return left.And(right)
case Or:
return left.Or(right)
default:
return nil, fmt.Errorf("unknown operator: %s", l.Op)
}
}
type NotExpr struct {
Type string `json:"type"` // not
Expr Expr `json:"expr"`
}
func (n NotExpr) GetType() string { return n.Type }
func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) {
inner, err := n.Expr.Eval(variables)
if err != nil {
return nil, err
}
return inner.Not()
}
type rawExpr struct {
Type string `json:"type"`
}
func MarshalExpr(e Expr) ([]byte, error) {
return json.Marshal(e)
}
func UnmarshalExpr(data []byte) (Expr, error) {
var typ rawExpr
if err := json.Unmarshal(data, &typ); err != nil {
return nil, err
}
switch typ.Type {
case "const":
var e ConstExpr
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e, nil
case "var":
var e VarExpr
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e, nil
case "compare":
var e CompareExprRaw
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e.ToCompareExpr()
case "logical":
var e LogicalExprRaw
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e.ToLogicalExpr()
case "not":
var e NotExprRaw
if err := json.Unmarshal(data, &e); err != nil {
return nil, err
}
return e.ToNotExpr()
default:
return nil, fmt.Errorf("unknown expr type: %s", typ.Type)
}
}
type CompareExprRaw struct {
Type string `json:"type"`
Op ComparisonOperator `json:"op"`
Left json.RawMessage `json:"left"`
Right json.RawMessage `json:"right"`
}
func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) {
leftExpr, err := UnmarshalExpr(r.Left)
if err != nil {
return CompareExpr{}, err
}
rightExpr, err := UnmarshalExpr(r.Right)
if err != nil {
return CompareExpr{}, err
}
return CompareExpr{
Type: r.Type,
Op: r.Op,
Left: leftExpr,
Right: rightExpr,
}, nil
}
type LogicalExprRaw struct {
Type string `json:"type"`
Op LogicalOperator `json:"op"`
Left json.RawMessage `json:"left"`
Right json.RawMessage `json:"right"`
}
func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) {
left, err := UnmarshalExpr(r.Left)
if err != nil {
return LogicalExpr{}, err
}
right, err := UnmarshalExpr(r.Right)
if err != nil {
return LogicalExpr{}, err
}
return LogicalExpr{
Type: r.Type,
Op: r.Op,
Left: left,
Right: right,
}, nil
}
type NotExprRaw struct {
Type string `json:"type"`
Expr json.RawMessage `json:"expr"`
}
func (r NotExprRaw) ToNotExpr() (NotExpr, error) {
inner, err := UnmarshalExpr(r.Expr)
if err != nil {
return NotExpr{}, err
}
return NotExpr{
Type: r.Type,
Expr: inner,
}, nil
}

View File

@ -0,0 +1,127 @@
package domain
import (
"testing"
)
func TestLogicalEval(t *testing.T) {
// 测试逻辑表达式 and
logicalExpr := LogicalExpr{
Left: ConstExpr{
Type: "const",
Value: true,
ValueType: "boolean",
},
Op: And,
Right: ConstExpr{
Type: "const",
Value: true,
ValueType: "boolean",
},
}
result, err := logicalExpr.Eval(nil)
if err != nil {
t.Errorf("failed to evaluate logical expression: %v", err)
}
if result.Value != true {
t.Errorf("expected true, got %v", result)
}
// 测试逻辑表达式 or
orExpr := LogicalExpr{
Left: ConstExpr{
Type: "const",
Value: true,
ValueType: "boolean",
},
Op: Or,
Right: ConstExpr{
Type: "const",
Value: true,
ValueType: "boolean",
},
}
result, err = orExpr.Eval(nil)
if err != nil {
t.Errorf("failed to evaluate logical expression: %v", err)
}
if result.Value != true {
t.Errorf("expected true, got %v", result)
}
}
func TestUnmarshalExpr(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
want Expr
wantErr bool
}{
{
name: "test1",
args: args{
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := UnmarshalExpr(tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got == nil {
t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want)
return
}
})
}
}
func TestExpr_Eval(t *testing.T) {
type args struct {
variables map[string]map[string]any
data []byte
}
tests := []struct {
name string
args args
want *EvalResult
wantErr bool
}{
{
name: "test1",
args: args{
variables: map[string]map[string]any{
"ODnYSOXB6HQP2_vz6JcZE": {
"certificate.validated": true,
"certificate.daysLeft": 2,
},
},
data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := UnmarshalExpr(tt.args.data)
if err != nil {
t.Errorf("UnmarshalExpr() error = %v", err)
return
}
got, err := c.Eval(tt.args.variables)
t.Log("got:", got)
if (err != nil) != tt.wantErr {
t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got.Value != true {
t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true)
}
})
}
}

View File

@ -1,6 +1,7 @@
package domain
import (
"encoding/json"
"time"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
@ -81,6 +82,10 @@ type WorkflowNodeConfigForApply struct {
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30
}
type WorkflowNodeConfigForCondition struct {
Expression Expr `json:"expression"` // 条件表达式
}
type WorkflowNodeConfigForUpload struct {
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
@ -104,6 +109,24 @@ type WorkflowNodeConfigForNotify struct {
Message string `json:"message"` // 通知内容
}
func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
expression := n.Config["expression"]
if expression == nil {
return WorkflowNodeConfigForCondition{}
}
raw, _ := json.Marshal(expression)
expr, err := UnmarshalExpr([]byte(raw))
if err != nil {
return WorkflowNodeConfigForCondition{}
}
return WorkflowNodeConfigForCondition{
Expression: expr,
}
}
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays")
if skipBeforeExpiryDays == 0 {
@ -171,6 +194,7 @@ type WorkflowNodeIO struct {
type WorkflowNodeIOValueSelector struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
const WorkflowNodeIONameCertificate string = "certificate"

View File

@ -98,16 +98,26 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow
procErr = processor.Process(ctx)
if procErr != nil {
processor.GetLogger().Error(procErr.Error())
if current.Type != domain.WorkflowNodeTypeCondition {
processor.GetLogger().Error(procErr.Error())
}
break
}
nodeOutputs := processor.GetOutputs()
if len(nodeOutputs) > 0 {
ctx = nodes.AddNodeOutput(ctx, current.Id, nodeOutputs)
}
}
break
}
// TODO: 优化可读性
if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
current = nil
procErr = nil
return nil
} else if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch {
return procErr
} else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch {
current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure)

View File

@ -16,6 +16,7 @@ import (
type applyNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
certRepo certificateRepository
outputRepo workflowOutputRepository
@ -25,6 +26,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
return &applyNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(),
@ -71,6 +73,7 @@ func (n *applyNode) Process(ctx context.Context) error {
n.logger.Warn("failed to parse certificate, may be the CA responded error")
return err
}
certificate := &domain.Certificate{
Source: domain.CertificateSourceTypeWorkflow,
Certificate: applyResult.CertificateFullChain,
@ -96,6 +99,10 @@ func (n *applyNode) Process(ctx context.Context) error {
return err
}
// 添加中间结果
n.outputs["certificate.validated"] = true
n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24)
n.logger.Info("apply completed")
return nil
@ -139,6 +146,10 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
expirationTime := time.Until(lastCertificate.ExpireAt)
if expirationTime > renewalInterval {
n.outputs["certificate.validated"] = true
n.outputs["certificate.daysLeft"] = int(expirationTime.Hours() / 24)
return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
}
}

View File

@ -2,6 +2,7 @@ package nodeprocessor
import (
"context"
"errors"
"github.com/usual2970/certimate/internal/domain"
)
@ -9,16 +10,42 @@ import (
type conditionNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
}
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
return &conditionNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
}
}
func (n *conditionNode) Process(ctx context.Context) error {
// 此类型节点不需要执行任何操作,直接返回
n.logger.Info("enter condition node: " + n.node.Name)
nodeConfig := n.node.GetConfigForCondition()
if nodeConfig.Expression == nil {
n.logger.Info("no condition found, continue to next node")
return nil
}
rs, err := n.eval(ctx, nodeConfig.Expression)
if err != nil {
n.logger.Warn("failed to eval expression: " + err.Error())
return err
}
if rs.Value == false {
n.logger.Info("condition not met, skip this branch")
return errors.New("condition not met")
}
n.logger.Info("condition met, continue to next node")
return nil
}
func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (*domain.EvalResult, error) {
variables := GetNodeOutputs(ctx)
return expression.Eval(variables)
}

View File

@ -0,0 +1,126 @@
package nodeprocessor
import (
"context"
"sync"
)
// 定义上下文键类型,避免键冲突
type workflowContextKey string
const (
nodeOutputsKey workflowContextKey = "node_outputs"
)
// 带互斥锁的节点输出容器
type nodeOutputsContainer struct {
sync.RWMutex
outputs map[string]map[string]any
}
// 创建新的并发安全的节点输出容器
func newNodeOutputsContainer() *nodeOutputsContainer {
return &nodeOutputsContainer{
outputs: make(map[string]map[string]any),
}
}
// 添加节点输出到上下文
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
container := getNodeOutputsContainer(ctx)
if container == nil {
container = newNodeOutputsContainer()
}
container.Lock()
defer container.Unlock()
// 创建输出的深拷贝以避免后续修改
outputCopy := make(map[string]any, len(output))
for k, v := range output {
outputCopy[k] = v
}
container.outputs[nodeId] = outputCopy
return context.WithValue(ctx, nodeOutputsKey, container)
}
// 从上下文获取节点输出
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
container := getNodeOutputsContainer(ctx)
if container == nil {
return nil
}
container.RLock()
defer container.RUnlock()
output, exists := container.outputs[nodeId]
if !exists {
return nil
}
outputCopy := make(map[string]any, len(output))
for k, v := range output {
outputCopy[k] = v
}
return outputCopy
}
// 获取特定节点的特定输出项
func GetNodeOutputValue(ctx context.Context, nodeId string, key string) (any, bool) {
output := GetNodeOutput(ctx, nodeId)
if output == nil {
return nil, false
}
value, exists := output[key]
return value, exists
}
// 获取所有节点输出
func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
container := getNodeOutputsContainer(ctx)
if container == nil {
return nil
}
container.RLock()
defer container.RUnlock()
// 创建所有输出的深拷贝
allOutputs := make(map[string]map[string]any, len(container.outputs))
for nodeId, output := range container.outputs {
nodeCopy := make(map[string]any, len(output))
for k, v := range output {
nodeCopy[k] = v
}
allOutputs[nodeId] = nodeCopy
}
return allOutputs
}
// 获取节点输出容器
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
value := ctx.Value(nodeOutputsKey)
if value == nil {
return nil
}
return value.(*nodeOutputsContainer)
}
// 检查节点是否有输出
func HasNodeOutput(ctx context.Context, nodeId string) bool {
container := getNodeOutputsContainer(ctx)
if container == nil {
return false
}
container.RLock()
defer container.RUnlock()
_, exists := container.outputs[nodeId]
return exists
}

View File

@ -15,6 +15,7 @@ import (
type deployNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
certRepo certificateRepository
outputRepo workflowOutputRepository
@ -24,6 +25,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
return &deployNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(),

View File

@ -9,12 +9,14 @@ import (
type executeFailureNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
}
func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode {
return &executeFailureNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
}
}

View File

@ -9,12 +9,14 @@ import (
type executeSuccessNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
}
func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode {
return &executeSuccessNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
}
}

View File

@ -12,6 +12,7 @@ import (
type notifyNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
settingsRepo settingsRepository
}
@ -20,6 +21,7 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
return &notifyNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
settingsRepo: repository.NewSettingsRepository(),
}

View File

@ -14,6 +14,8 @@ type NodeProcessor interface {
SetLogger(*slog.Logger)
Process(ctx context.Context) error
GetOutputs() map[string]any
}
type nodeProcessor struct {
@ -32,6 +34,20 @@ func (n *nodeProcessor) SetLogger(logger *slog.Logger) {
n.logger = logger
}
type nodeOutputer struct {
outputs map[string]any
}
func newNodeOutputer() *nodeOutputer {
return &nodeOutputer{
outputs: make(map[string]any),
}
}
func (n *nodeOutputer) GetOutputs() map[string]any {
return n.outputs
}
type certificateRepository interface {
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
}

View File

@ -9,12 +9,14 @@ import (
type startNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
}
func NewStartNode(node *domain.WorkflowNode) *startNode {
return &startNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/repository"
@ -12,6 +13,7 @@ import (
type uploadNode struct {
node *domain.WorkflowNode
*nodeProcessor
*nodeOutputer
certRepo certificateRepository
outputRepo workflowOutputRepository
@ -21,6 +23,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
return &uploadNode{
node: node,
nodeProcessor: newNodeProcessor(node),
nodeOutputer: newNodeOutputer(),
certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(),
@ -66,6 +69,9 @@ func (n *uploadNode) Process(ctx context.Context) error {
return err
}
n.outputs["certificate.validated"] = true
n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24)
n.logger.Info("upload completed")
return nil
@ -85,6 +91,8 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl
lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id)
if lastCertificate != nil {
n.outputs["certificate.validated"] = true
n.outputs["certificate.daysLeft"] = int(time.Until(lastCertificate.ExpireAt).Hours() / 24)
return true, "the certificate has already been uploaded"
}
}

View File

@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd";
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
import AddNode from "./AddNode";
import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
import { Expr, WorkflowNodeIoValueType } from "@/domain/workflow";
import { Expr, WorkflowNodeIoValueType, Value } from "@/domain/workflow";
import { produce } from "immer";
import { useWorkflowStore } from "@/stores/workflow";
import { useZustandShallowSelector } from "@/hooks";
@ -30,15 +30,34 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
// 创建单个条件的表达式
const createComparisonExpr = (condition: ConditionItem): Expr => {
const selectors = condition.leftSelector.split("#");
const t = selectors[2] as WorkflowNodeIoValueType;
const left: Expr = {
type: "var",
selector: {
id: selectors[0],
name: selectors[1],
type: selectors[2] as WorkflowNodeIoValueType,
type: t,
},
};
const right: Expr = { type: "const", value: condition.rightValue || "" };
let value: Value = condition.rightValue;
switch (t) {
case "boolean":
if (value === "true") {
value = true;
} else if (value === "false") {
value = false;
}
break;
case "number":
value = parseInt(value as string);
break;
case "string":
value = value as string;
break;
}
const right: Expr = { type: "const", value: value, valueType: t };
return {
type: "compare",

View File

@ -318,6 +318,7 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => {
const right: Expr = {
type: "const",
value: rightValue,
valueType: type,
};
return {

View File

@ -232,13 +232,13 @@ export const workflowNodeIOOptions = (node: WorkflowNode) => {
// #region Condition expression
type Value = string | number | boolean;
export type Value = string | number | boolean;
export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is";
export type LogicalOperator = "and" | "or" | "not";
export type ConstExpr = { type: "const"; value: Value };
export type ConstExpr = { type: "const"; value: Value; valueType: WorkflowNodeIoValueType };
export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector };
export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr };
export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr };