mirror of
https://github.com/oarkflow/mq.git
synced 2025-09-27 04:15:52 +08:00
962 lines
25 KiB
Go
962 lines
25 KiB
Go
package workflow
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SubDAGProcessor handles sub-workflow execution
|
|
type SubDAGProcessor struct{}
|
|
|
|
func (p *SubDAGProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
subWorkflowID := config.SubWorkflowID
|
|
if subWorkflowID == "" {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "sub_workflow_id not specified",
|
|
}, nil
|
|
}
|
|
|
|
// Apply input mapping
|
|
subInput := make(map[string]interface{})
|
|
for subKey, sourceKey := range config.InputMapping {
|
|
if value, exists := input.Data[sourceKey]; exists {
|
|
subInput[subKey] = value
|
|
}
|
|
}
|
|
|
|
// Simulate sub-workflow execution (in real implementation, this would trigger actual sub-workflow)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Mock sub-workflow output
|
|
subOutput := map[string]interface{}{
|
|
"sub_workflow_result": "completed",
|
|
"sub_workflow_id": subWorkflowID,
|
|
"processed_data": subInput,
|
|
}
|
|
|
|
// Apply output mapping
|
|
result := make(map[string]interface{})
|
|
for targetKey, subKey := range config.OutputMapping {
|
|
if value, exists := subOutput[subKey]; exists {
|
|
result[targetKey] = value
|
|
}
|
|
}
|
|
|
|
// If no output mapping specified, return all sub-workflow output
|
|
if len(config.OutputMapping) == 0 {
|
|
result = subOutput
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Sub-workflow %s completed successfully", subWorkflowID),
|
|
}, nil
|
|
}
|
|
|
|
// HTMLProcessor handles HTML page generation
|
|
type HTMLProcessor struct{}
|
|
|
|
func (p *HTMLProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
templateStr := config.Template
|
|
if templateStr == "" {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "template not specified",
|
|
}, nil
|
|
}
|
|
|
|
// Parse template
|
|
tmpl, err := template.New("html_page").Parse(templateStr)
|
|
if err != nil {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to parse template: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// Prepare template data
|
|
templateData := make(map[string]interface{})
|
|
|
|
// Add data from input
|
|
for key, value := range input.Data {
|
|
templateData[key] = value
|
|
}
|
|
|
|
// Add template-specific data from config
|
|
for key, value := range config.TemplateData {
|
|
templateData[key] = value
|
|
}
|
|
|
|
// Add current timestamp
|
|
templateData["timestamp"] = time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
// Execute template
|
|
var htmlBuffer strings.Builder
|
|
if err := tmpl.Execute(&htmlBuffer, templateData); err != nil {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to execute template: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
html := htmlBuffer.String()
|
|
|
|
result := map[string]interface{}{
|
|
"html_content": html,
|
|
"template": templateStr,
|
|
"data_used": templateData,
|
|
}
|
|
|
|
// If output path is specified, simulate file writing
|
|
if config.OutputPath != "" {
|
|
result["output_path"] = config.OutputPath
|
|
result["file_written"] = true
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: "HTML page generated successfully",
|
|
}, nil
|
|
}
|
|
|
|
// SMSProcessor handles SMS operations
|
|
type SMSProcessor struct{}
|
|
|
|
func (p *SMSProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
provider := config.Provider
|
|
if provider == "" {
|
|
provider = "default"
|
|
}
|
|
|
|
from := config.From
|
|
if from == "" {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "from number not specified",
|
|
}, nil
|
|
}
|
|
|
|
if len(config.SMSTo) == 0 {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "recipient numbers not specified",
|
|
}, nil
|
|
}
|
|
|
|
message := config.Message
|
|
if message == "" {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "message not specified",
|
|
}, nil
|
|
}
|
|
|
|
// Process message template with input data
|
|
processedMessage := p.processMessageTemplate(message, input.Data)
|
|
|
|
// Validate phone numbers
|
|
validRecipients := []string{}
|
|
invalidRecipients := []string{}
|
|
|
|
for _, recipient := range config.SMSTo {
|
|
if p.isValidPhoneNumber(recipient) {
|
|
validRecipients = append(validRecipients, recipient)
|
|
} else {
|
|
invalidRecipients = append(invalidRecipients, recipient)
|
|
}
|
|
}
|
|
|
|
if len(validRecipients) == 0 {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "no valid recipient numbers",
|
|
}, nil
|
|
}
|
|
|
|
// Simulate SMS sending
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Mock SMS sending results
|
|
results := []map[string]interface{}{}
|
|
for _, recipient := range validRecipients {
|
|
results = append(results, map[string]interface{}{
|
|
"recipient": recipient,
|
|
"status": "sent",
|
|
"message_id": fmt.Sprintf("msg_%d", time.Now().UnixNano()),
|
|
"provider": provider,
|
|
})
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"provider": provider,
|
|
"from": from,
|
|
"message": processedMessage,
|
|
"valid_recipients": validRecipients,
|
|
"invalid_recipients": invalidRecipients,
|
|
"sent_count": len(validRecipients),
|
|
"failed_count": len(invalidRecipients),
|
|
"results": results,
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("SMS sent to %d recipients via %s", len(validRecipients), provider),
|
|
}, nil
|
|
}
|
|
|
|
func (p *SMSProcessor) processMessageTemplate(message string, data map[string]interface{}) string {
|
|
result := message
|
|
for key, value := range data {
|
|
placeholder := fmt.Sprintf("{{%s}}", key)
|
|
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *SMSProcessor) isValidPhoneNumber(phone string) bool {
|
|
// Simple phone number validation (E.164 format)
|
|
phoneRegex := regexp.MustCompile(`^\+[1-9]\d{1,14}$`)
|
|
return phoneRegex.MatchString(phone)
|
|
}
|
|
|
|
// AuthProcessor handles authentication operations
|
|
type AuthProcessor struct{}
|
|
|
|
func (p *AuthProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
authType := config.AuthType
|
|
if authType == "" {
|
|
authType = "jwt"
|
|
}
|
|
|
|
credentials := config.Credentials
|
|
if credentials == nil {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "credentials not provided",
|
|
}, nil
|
|
}
|
|
|
|
switch authType {
|
|
case "jwt":
|
|
return p.processJWTAuth(input, credentials, config.TokenExpiry)
|
|
case "basic":
|
|
return p.processBasicAuth(input, credentials)
|
|
case "api_key":
|
|
return p.processAPIKeyAuth(input, credentials)
|
|
default:
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("unsupported auth type: %s", authType),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (p *AuthProcessor) processJWTAuth(input ProcessingContext, credentials map[string]string, expiry time.Duration) (*ProcessingResult, error) {
|
|
username, hasUsername := credentials["username"]
|
|
password, hasPassword := credentials["password"]
|
|
|
|
if !hasUsername || !hasPassword {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "username and password required for JWT auth",
|
|
}, nil
|
|
}
|
|
|
|
// Simulate authentication (in real implementation, verify against user store)
|
|
if username == "admin" && password == "password" {
|
|
// Generate mock JWT token
|
|
token := fmt.Sprintf("jwt.token.%d", time.Now().Unix())
|
|
expiresAt := time.Now().Add(expiry)
|
|
if expiry == 0 {
|
|
expiresAt = time.Now().Add(24 * time.Hour)
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"auth_type": "jwt",
|
|
"token": token,
|
|
"expires_at": expiresAt,
|
|
"username": username,
|
|
"permissions": []string{"read", "write", "admin"},
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: "JWT authentication successful",
|
|
}, nil
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "invalid credentials",
|
|
}, nil
|
|
}
|
|
|
|
func (p *AuthProcessor) processBasicAuth(input ProcessingContext, credentials map[string]string) (*ProcessingResult, error) {
|
|
username, hasUsername := credentials["username"]
|
|
password, hasPassword := credentials["password"]
|
|
|
|
if !hasUsername || !hasPassword {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "username and password required for basic auth",
|
|
}, nil
|
|
}
|
|
|
|
// Simulate basic auth
|
|
if username != "" && password != "" {
|
|
result := map[string]interface{}{
|
|
"auth_type": "basic",
|
|
"username": username,
|
|
"status": "authenticated",
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: "Basic authentication successful",
|
|
}, nil
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "invalid credentials",
|
|
}, nil
|
|
}
|
|
|
|
func (p *AuthProcessor) processAPIKeyAuth(input ProcessingContext, credentials map[string]string) (*ProcessingResult, error) {
|
|
apiKey, hasAPIKey := credentials["api_key"]
|
|
|
|
if !hasAPIKey {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "api_key required for API key auth",
|
|
}, nil
|
|
}
|
|
|
|
// Simulate API key validation
|
|
if apiKey != "" && len(apiKey) >= 10 {
|
|
result := map[string]interface{}{
|
|
"auth_type": "api_key",
|
|
"api_key": apiKey[:6] + "...", // Partially masked
|
|
"status": "authenticated",
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: "API key authentication successful",
|
|
}, nil
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "invalid API key",
|
|
}, nil
|
|
}
|
|
|
|
// ValidatorProcessor handles data validation
|
|
type ValidatorProcessor struct{}
|
|
|
|
func (p *ValidatorProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
validationType := config.ValidationType
|
|
if validationType == "" {
|
|
validationType = "rules"
|
|
}
|
|
|
|
validationRules := config.ValidationRules
|
|
if len(validationRules) == 0 {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "no validation rules specified",
|
|
}, nil
|
|
}
|
|
|
|
errors := []string{}
|
|
warnings := []string{}
|
|
validatedFields := []string{}
|
|
|
|
for _, rule := range validationRules {
|
|
fieldValue, exists := input.Data[rule.Field]
|
|
if !exists {
|
|
if rule.Required {
|
|
errors = append(errors, fmt.Sprintf("required field '%s' is missing", rule.Field))
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Validate based on rule type
|
|
switch rule.Type {
|
|
case "string":
|
|
if err := p.validateString(fieldValue, rule); err != nil {
|
|
errors = append(errors, fmt.Sprintf("field '%s': %s", rule.Field, err.Error()))
|
|
} else {
|
|
validatedFields = append(validatedFields, rule.Field)
|
|
}
|
|
case "number":
|
|
if err := p.validateNumber(fieldValue, rule); err != nil {
|
|
errors = append(errors, fmt.Sprintf("field '%s': %s", rule.Field, err.Error()))
|
|
} else {
|
|
validatedFields = append(validatedFields, rule.Field)
|
|
}
|
|
case "email":
|
|
if err := p.validateEmail(fieldValue); err != nil {
|
|
errors = append(errors, fmt.Sprintf("field '%s': %s", rule.Field, err.Error()))
|
|
} else {
|
|
validatedFields = append(validatedFields, rule.Field)
|
|
}
|
|
case "regex":
|
|
if err := p.validateRegex(fieldValue, rule.Pattern); err != nil {
|
|
errors = append(errors, fmt.Sprintf("field '%s': %s", rule.Field, err.Error()))
|
|
} else {
|
|
validatedFields = append(validatedFields, rule.Field)
|
|
}
|
|
default:
|
|
warnings = append(warnings, fmt.Sprintf("unknown validation type '%s' for field '%s'", rule.Type, rule.Field))
|
|
}
|
|
}
|
|
|
|
success := len(errors) == 0
|
|
result := map[string]interface{}{
|
|
"validation_type": validationType,
|
|
"validated_fields": validatedFields,
|
|
"errors": errors,
|
|
"warnings": warnings,
|
|
"error_count": len(errors),
|
|
"warning_count": len(warnings),
|
|
"is_valid": success,
|
|
}
|
|
|
|
message := fmt.Sprintf("Validation completed: %d fields validated, %d errors, %d warnings",
|
|
len(validatedFields), len(errors), len(warnings))
|
|
|
|
return &ProcessingResult{
|
|
Success: success,
|
|
Data: result,
|
|
Message: message,
|
|
}, nil
|
|
}
|
|
|
|
func (p *ValidatorProcessor) validateString(value interface{}, rule ValidationRule) error {
|
|
str, ok := value.(string)
|
|
if !ok {
|
|
return fmt.Errorf("expected string, got %T", value)
|
|
}
|
|
|
|
if rule.MinLength > 0 && len(str) < int(rule.MinLength) {
|
|
return fmt.Errorf("minimum length is %d, got %d", rule.MinLength, len(str))
|
|
}
|
|
|
|
if rule.MaxLength > 0 && len(str) > int(rule.MaxLength) {
|
|
return fmt.Errorf("maximum length is %d, got %d", rule.MaxLength, len(str))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ValidatorProcessor) validateNumber(value interface{}, rule ValidationRule) error {
|
|
var num float64
|
|
switch v := value.(type) {
|
|
case int:
|
|
num = float64(v)
|
|
case int64:
|
|
num = float64(v)
|
|
case float64:
|
|
num = v
|
|
case string:
|
|
parsed, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot parse as number: %s", v)
|
|
}
|
|
num = parsed
|
|
default:
|
|
return fmt.Errorf("expected number, got %T", value)
|
|
}
|
|
|
|
if rule.Min != nil && num < *rule.Min {
|
|
return fmt.Errorf("minimum value is %f, got %f", *rule.Min, num)
|
|
}
|
|
|
|
if rule.Max != nil && num > *rule.Max {
|
|
return fmt.Errorf("maximum value is %f, got %f", *rule.Max, num)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ValidatorProcessor) validateEmail(value interface{}) error {
|
|
email, ok := value.(string)
|
|
if !ok {
|
|
return fmt.Errorf("expected string, got %T", value)
|
|
}
|
|
|
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
|
if !emailRegex.MatchString(email) {
|
|
return fmt.Errorf("invalid email format")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ValidatorProcessor) validateRegex(value interface{}, pattern string) error {
|
|
str, ok := value.(string)
|
|
if !ok {
|
|
return fmt.Errorf("expected string, got %T", value)
|
|
}
|
|
|
|
regex, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid regex pattern: %s", err.Error())
|
|
}
|
|
|
|
if !regex.MatchString(str) {
|
|
return fmt.Errorf("does not match pattern %s", pattern)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RouterProcessor handles conditional routing
|
|
type RouterProcessor struct{}
|
|
|
|
func (p *RouterProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
routingRules := config.RoutingRules
|
|
if len(routingRules) == 0 {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "no routing rules specified",
|
|
}, nil
|
|
}
|
|
|
|
selectedRoutes := []RoutingRule{}
|
|
|
|
for _, rule := range routingRules {
|
|
if p.evaluateRoutingCondition(rule.Condition, input.Data) {
|
|
selectedRoutes = append(selectedRoutes, rule)
|
|
}
|
|
}
|
|
|
|
if len(selectedRoutes) == 0 {
|
|
// Check if there's a default route
|
|
for _, rule := range routingRules {
|
|
if rule.IsDefault {
|
|
selectedRoutes = append(selectedRoutes, rule)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"selected_routes": selectedRoutes,
|
|
"route_count": len(selectedRoutes),
|
|
"routing_data": input.Data,
|
|
}
|
|
|
|
if len(selectedRoutes) == 0 {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Data: result,
|
|
Error: "no matching routes found",
|
|
}, nil
|
|
}
|
|
|
|
message := fmt.Sprintf("Routing completed: %d routes selected", len(selectedRoutes))
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: message,
|
|
}, nil
|
|
}
|
|
|
|
func (p *RouterProcessor) evaluateRoutingCondition(condition string, data map[string]interface{}) bool {
|
|
// Simple condition evaluation - in real implementation, use expression parser
|
|
if condition == "" {
|
|
return false
|
|
}
|
|
|
|
// Support simple equality checks
|
|
if strings.Contains(condition, "==") {
|
|
parts := strings.Split(condition, "==")
|
|
if len(parts) == 2 {
|
|
field := strings.TrimSpace(parts[0])
|
|
expectedValue := strings.TrimSpace(strings.Trim(parts[1], "\"'"))
|
|
|
|
if value, exists := data[field]; exists {
|
|
return fmt.Sprintf("%v", value) == expectedValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Support simple greater than checks
|
|
if strings.Contains(condition, ">") {
|
|
parts := strings.Split(condition, ">")
|
|
if len(parts) == 2 {
|
|
field := strings.TrimSpace(parts[0])
|
|
threshold := strings.TrimSpace(parts[1])
|
|
|
|
if value, exists := data[field]; exists {
|
|
if numValue, ok := value.(float64); ok {
|
|
if thresholdValue, err := strconv.ParseFloat(threshold, 64); err == nil {
|
|
return numValue > thresholdValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// StorageProcessor handles data storage operations
|
|
type StorageProcessor struct{}
|
|
|
|
func (p *StorageProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
storageType := config.StorageType
|
|
if storageType == "" {
|
|
storageType = "memory"
|
|
}
|
|
|
|
operation := config.StorageOperation
|
|
if operation == "" {
|
|
operation = "store"
|
|
}
|
|
|
|
key := config.StorageKey
|
|
if key == "" {
|
|
key = fmt.Sprintf("data_%d", time.Now().UnixNano())
|
|
}
|
|
|
|
switch operation {
|
|
case "store":
|
|
return p.storeData(storageType, key, input.Data)
|
|
case "retrieve":
|
|
return p.retrieveData(storageType, key)
|
|
case "delete":
|
|
return p.deleteData(storageType, key)
|
|
default:
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("unsupported storage operation: %s", operation),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (p *StorageProcessor) storeData(storageType, key string, data map[string]interface{}) (*ProcessingResult, error) {
|
|
// Simulate data storage
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
result := map[string]interface{}{
|
|
"storage_type": storageType,
|
|
"operation": "store",
|
|
"key": key,
|
|
"stored_data": data,
|
|
"timestamp": time.Now(),
|
|
"size_bytes": len(fmt.Sprintf("%v", data)),
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Data stored successfully with key: %s", key),
|
|
}, nil
|
|
}
|
|
|
|
func (p *StorageProcessor) retrieveData(storageType, key string) (*ProcessingResult, error) {
|
|
// Simulate data retrieval
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
// Mock retrieved data
|
|
retrievedData := map[string]interface{}{
|
|
"key": key,
|
|
"value": "mock_stored_value",
|
|
"timestamp": time.Now().Add(-1 * time.Hour),
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"storage_type": storageType,
|
|
"operation": "retrieve",
|
|
"key": key,
|
|
"retrieved_data": retrievedData,
|
|
"found": true,
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Data retrieved successfully for key: %s", key),
|
|
}, nil
|
|
}
|
|
|
|
func (p *StorageProcessor) deleteData(storageType, key string) (*ProcessingResult, error) {
|
|
// Simulate data deletion
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
result := map[string]interface{}{
|
|
"storage_type": storageType,
|
|
"operation": "delete",
|
|
"key": key,
|
|
"deleted": true,
|
|
"timestamp": time.Now(),
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Data deleted successfully for key: %s", key),
|
|
}, nil
|
|
}
|
|
|
|
// NotifyProcessor handles notification operations
|
|
type NotifyProcessor struct{}
|
|
|
|
func (p *NotifyProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
notificationType := config.NotificationType
|
|
if notificationType == "" {
|
|
notificationType = "email"
|
|
}
|
|
|
|
recipients := config.NotificationRecipients
|
|
if len(recipients) == 0 {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "no notification recipients specified",
|
|
}, nil
|
|
}
|
|
|
|
message := config.NotificationMessage
|
|
if message == "" {
|
|
message = "Workflow notification"
|
|
}
|
|
|
|
// Process message template with input data
|
|
processedMessage := p.processNotificationTemplate(message, input.Data)
|
|
|
|
switch notificationType {
|
|
case "email":
|
|
return p.sendEmailNotification(recipients, processedMessage, config)
|
|
case "sms":
|
|
return p.sendSMSNotification(recipients, processedMessage, config)
|
|
case "webhook":
|
|
return p.sendWebhookNotification(recipients, processedMessage, input.Data, config)
|
|
default:
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("unsupported notification type: %s", notificationType),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (p *NotifyProcessor) processNotificationTemplate(message string, data map[string]interface{}) string {
|
|
result := message
|
|
for key, value := range data {
|
|
placeholder := fmt.Sprintf("{{%s}}", key)
|
|
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *NotifyProcessor) sendEmailNotification(recipients []string, message string, config NodeConfig) (*ProcessingResult, error) {
|
|
// Simulate email sending
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
results := []map[string]interface{}{}
|
|
for _, recipient := range recipients {
|
|
results = append(results, map[string]interface{}{
|
|
"recipient": recipient,
|
|
"status": "sent",
|
|
"type": "email",
|
|
"timestamp": time.Now(),
|
|
})
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"notification_type": "email",
|
|
"recipients": recipients,
|
|
"message": message,
|
|
"sent_count": len(recipients),
|
|
"results": results,
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Email notifications sent to %d recipients", len(recipients)),
|
|
}, nil
|
|
}
|
|
|
|
func (p *NotifyProcessor) sendSMSNotification(recipients []string, message string, config NodeConfig) (*ProcessingResult, error) {
|
|
// Simulate SMS sending
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
results := []map[string]interface{}{}
|
|
for _, recipient := range recipients {
|
|
results = append(results, map[string]interface{}{
|
|
"recipient": recipient,
|
|
"status": "sent",
|
|
"type": "sms",
|
|
"timestamp": time.Now(),
|
|
})
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"notification_type": "sms",
|
|
"recipients": recipients,
|
|
"message": message,
|
|
"sent_count": len(recipients),
|
|
"results": results,
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("SMS notifications sent to %d recipients", len(recipients)),
|
|
}, nil
|
|
}
|
|
|
|
func (p *NotifyProcessor) sendWebhookNotification(recipients []string, message string, data map[string]interface{}, config NodeConfig) (*ProcessingResult, error) {
|
|
// Simulate webhook sending
|
|
time.Sleep(25 * time.Millisecond)
|
|
|
|
results := []map[string]interface{}{}
|
|
for _, recipient := range recipients {
|
|
// Mock webhook response
|
|
results = append(results, map[string]interface{}{
|
|
"url": recipient,
|
|
"status": "sent",
|
|
"type": "webhook",
|
|
"response": map[string]interface{}{"status": "ok", "code": 200},
|
|
"timestamp": time.Now(),
|
|
})
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"notification_type": "webhook",
|
|
"urls": recipients,
|
|
"message": message,
|
|
"payload": data,
|
|
"sent_count": len(recipients),
|
|
"results": results,
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Webhook notifications sent to %d URLs", len(recipients)),
|
|
}, nil
|
|
}
|
|
|
|
// WebhookReceiverProcessor handles incoming webhook processing
|
|
type WebhookReceiverProcessor struct{}
|
|
|
|
func (p *WebhookReceiverProcessor) Process(ctx context.Context, input ProcessingContext) (*ProcessingResult, error) {
|
|
config := input.Node.Config
|
|
|
|
expectedSignature := config.WebhookSignature
|
|
secret := config.WebhookSecret
|
|
|
|
// Extract webhook data from input
|
|
webhookData, ok := input.Data["webhook_data"].(map[string]interface{})
|
|
if !ok {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "no webhook data found in input",
|
|
}, nil
|
|
}
|
|
|
|
// Verify webhook signature if provided
|
|
if expectedSignature != "" && secret != "" {
|
|
isValid := p.verifyWebhookSignature(webhookData, secret, expectedSignature)
|
|
if !isValid {
|
|
return &ProcessingResult{
|
|
Success: false,
|
|
Error: "webhook signature verification failed",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Process webhook data based on source
|
|
source, _ := webhookData["source"].(string)
|
|
if source == "" {
|
|
source = "unknown"
|
|
}
|
|
|
|
processedData := map[string]interface{}{
|
|
"source": source,
|
|
"original_data": webhookData,
|
|
"processed_at": time.Now(),
|
|
"signature_valid": expectedSignature == "" || secret == "",
|
|
}
|
|
|
|
// Apply any data transformations specified in config
|
|
if transformRules, exists := config.WebhookTransforms["transforms"]; exists {
|
|
if rules, ok := transformRules.(map[string]interface{}); ok {
|
|
for key, rule := range rules {
|
|
if sourceField, ok := rule.(string); ok {
|
|
if value, exists := webhookData[sourceField]; exists {
|
|
processedData[key] = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"webhook_source": source,
|
|
"processed_data": processedData,
|
|
"original_payload": webhookData,
|
|
"processing_time": time.Now(),
|
|
}
|
|
|
|
return &ProcessingResult{
|
|
Success: true,
|
|
Data: result,
|
|
Message: fmt.Sprintf("Webhook from %s processed successfully", source),
|
|
}, nil
|
|
}
|
|
|
|
func (p *WebhookReceiverProcessor) verifyWebhookSignature(data map[string]interface{}, secret, expectedSignature string) bool {
|
|
// Convert data to JSON for signature verification
|
|
payload, err := json.Marshal(data)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Create HMAC signature
|
|
h := hmac.New(sha256.New, []byte(secret))
|
|
h.Write(payload)
|
|
computedSignature := hex.EncodeToString(h.Sum(nil))
|
|
|
|
// Compare signatures (constant time comparison for security)
|
|
return hmac.Equal([]byte(computedSignature), []byte(expectedSignature))
|
|
}
|