mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-06 16:36:53 +08:00
update
This commit is contained in:
@@ -44,7 +44,7 @@ func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var conditionStatus string
|
||||
_, ok := e.conditions[defaultKey]
|
||||
for status, condition := range e.conditions {
|
||||
if status != defaultKey {
|
||||
if status != defaultKey && condition != nil {
|
||||
if condition.Match(data) {
|
||||
conditionStatus = status
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"html/template"
|
||||
"os"
|
||||
|
||||
"github.com/oarkflow/jet"
|
||||
"github.com/oarkflow/mq"
|
||||
"github.com/oarkflow/mq/consts"
|
||||
"github.com/oarkflow/mq/dag"
|
||||
@@ -26,24 +27,30 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu
|
||||
templateStr, _ = data["template"].(string)
|
||||
templateFile, _ = data["template_file"].(string)
|
||||
)
|
||||
|
||||
var templateData map[string]any
|
||||
if len(task.Payload) > 0 {
|
||||
if err := json.Unmarshal(task.Payload, &templateData); err != nil {
|
||||
return mq.Result{Payload: task.Payload, Error: err, Ctx: ctx, ConditionStatus: "invalid"}
|
||||
return mq.Result{Payload: task.Payload, Error: err, Ctx: ctx}
|
||||
}
|
||||
}
|
||||
if templateData == nil {
|
||||
templateData = make(map[string]any)
|
||||
}
|
||||
delete(templateData, "html_content")
|
||||
if c.Payload.Mapping != nil {
|
||||
for k, v := range c.Payload.Mapping {
|
||||
_, val := dag.GetVal(ctx, v, templateData)
|
||||
templateData[k] = val
|
||||
}
|
||||
}
|
||||
templateData["task_id"] = ctx.Value("task_id")
|
||||
|
||||
var renderedHTML string
|
||||
var err error
|
||||
|
||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||
switch {
|
||||
// 1. JSONSchema + HTML Template
|
||||
case schemaFile != "" && templateStr != "":
|
||||
case schemaFile != "" && templateStr != "" && templateFile == "":
|
||||
fmt.Println("Using JSONSchema and inline HTML template", c.ID)
|
||||
if c.renderer == nil {
|
||||
renderer, err := renderer.GetFromFile(schemaFile, templateStr)
|
||||
if err != nil {
|
||||
@@ -53,7 +60,8 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu
|
||||
}
|
||||
renderedHTML, err = c.renderer.RenderFields(templateData)
|
||||
// 2. JSONSchema + HTML File
|
||||
case schemaFile != "" && templateFile != "":
|
||||
case schemaFile != "" && templateFile != "" && templateStr == "":
|
||||
fmt.Println("Using JSONSchema and HTML file", c.ID)
|
||||
if c.renderer == nil {
|
||||
renderer, err := renderer.GetFromFile(schemaFile, "", templateFile)
|
||||
if err != nil {
|
||||
@@ -63,7 +71,8 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu
|
||||
}
|
||||
renderedHTML, err = c.renderer.RenderFields(templateData)
|
||||
// 3. Only JSONSchema
|
||||
case schemaFile != "" || c.renderer != nil:
|
||||
case (schemaFile != "" || c.renderer != nil) && templateStr == "" && templateFile == "":
|
||||
fmt.Println("Using only JSONSchema", c.ID)
|
||||
if c.renderer == nil {
|
||||
renderer, err := renderer.GetFromFile(schemaFile, "")
|
||||
if err != nil {
|
||||
@@ -73,7 +82,8 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu
|
||||
}
|
||||
renderedHTML, err = c.renderer.RenderFields(templateData)
|
||||
// 4. Only HTML Template
|
||||
case templateStr != "":
|
||||
case templateStr != "" && templateFile == "" && schemaFile == "":
|
||||
fmt.Println("Using inline HTML template", c.ID)
|
||||
tmpl, err := template.New("inline").Parse(templateStr)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to parse template: %v", err), Ctx: ctx}
|
||||
@@ -85,21 +95,16 @@ func (c *RenderHTMLNode) ProcessTask(ctx context.Context, task *mq.Task) mq.Resu
|
||||
}
|
||||
renderedHTML = buf.String()
|
||||
// 5. Only HTML File
|
||||
case templateFile != "":
|
||||
case templateFile != "" && templateStr == "" && schemaFile == "":
|
||||
fmt.Println("Using HTML file", c.ID)
|
||||
fileContent, err := os.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to read template file: %v", err), Ctx: ctx}
|
||||
}
|
||||
tmpl, err := template.New("file").Parse(string(fileContent))
|
||||
renderedHTML, err = parser.ParseTemplate(string(fileContent), templateData)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to parse template file: %v", err), Ctx: ctx}
|
||||
return mq.Result{Error: err, Ctx: ctx}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, templateData)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to execute template file: %v", err), Ctx: ctx}
|
||||
}
|
||||
renderedHTML = buf.String()
|
||||
default:
|
||||
return mq.Result{Error: fmt.Errorf("no valid rendering approach found"), Ctx: ctx}
|
||||
}
|
||||
|
42
services/examples/app/templates/basic.html
Normal file
42
services/examples/app/templates/basic.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Basic Template</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="form.css">
|
||||
<style>
|
||||
.required {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
font-weight: bold;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #0d6efd;
|
||||
border-bottom: 2px solid #0d6efd;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group-fields>div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100">
|
||||
<form {{form_attributes}}>
|
||||
<div class="form-container p-4 bg-white shadow-md rounded">
|
||||
{{form_groups}}
|
||||
<div class="mt-4 flex gap-2">
|
||||
{{form_buttons}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
134
services/examples/app/templates/error.html
Normal file
134
services/examples/app/templates/error.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Email Error</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 700px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF5722 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(15px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
animation: shake 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 30px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin: 25px 0;
|
||||
font-size: 18px;
|
||||
border-left: 6px solid #FFB6B6;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 25px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(45deg, #4ECDC4, #44A08D);
|
||||
color: white;
|
||||
padding: 15px 30px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin: 0 15px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: linear-gradient(45deg, #FFA726, #FF9800);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-icon">❌</div>
|
||||
<h1>Email Processing Error</h1>
|
||||
|
||||
<div class="error-message">
|
||||
{{error_message}}
|
||||
</div>
|
||||
|
||||
{{if error_field}}
|
||||
<div class="error-details">
|
||||
<strong>🎯 Error Field:</strong> {{error_field}}<br>
|
||||
<strong>⚡ Action Required:</strong> Please correct the highlighted field and try again.<br>
|
||||
<strong>💡 Tip:</strong> Make sure all required fields are properly filled out.
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if retry_suggested}}
|
||||
<div class="error-details">
|
||||
<strong>⚠️ Temporary Issue:</strong> This appears to be a temporary system issue.
|
||||
Please try sending your message again in a few moments.<br>
|
||||
<strong>🔄 Auto-Retry:</strong> Our system will automatically retry failed deliveries.
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="actions">
|
||||
<a href="/" class="btn retry-btn">🔄 Try Again</a>
|
||||
<a href="/api/status" class="btn">📊 Check Status</a>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px; font-size: 14px; opacity: 0.8;">
|
||||
🔄 DAG Error Handler | Email Notification Workflow Failed<br>
|
||||
Our advanced routing system ensures reliable message delivery.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
71
services/examples/json/login.json
Normal file
71
services/examples/json/login.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "Login Flow",
|
||||
"key": "login:flow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "LoginForm",
|
||||
"first_node": true,
|
||||
"node": "render-html",
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"schema_file": "login.json",
|
||||
"template_file": "app/templates/basic.html"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ValidateLogin",
|
||||
"node": "condition",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
"additional_data": {
|
||||
"conditions": {
|
||||
"invalid": {
|
||||
"id": "condition:invalid_login",
|
||||
"node": "error-page",
|
||||
"group": {
|
||||
"reverse": true,
|
||||
"filters": [
|
||||
{
|
||||
"field": "username",
|
||||
"operator": "eq",
|
||||
"value": "admin"
|
||||
},
|
||||
{
|
||||
"field": "password",
|
||||
"operator": "eq",
|
||||
"value": "password"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "error-page",
|
||||
"node": "render-html",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"error_message": "eval.{{'Invalid login credentials.'}}",
|
||||
"error_field": "eval.{{'username'}}",
|
||||
"retry_suggested": "eval.{{true}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"template_file": "app/templates/error.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "LoginForm",
|
||||
"target": [ "ValidateLogin" ]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
37
services/examples/json_email.go
Normal file
37
services/examples/json_email.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/oarkflow/mq"
|
||||
"github.com/oarkflow/mq/dag"
|
||||
"github.com/oarkflow/mq/handlers"
|
||||
"github.com/oarkflow/mq/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
handlerBytes, err := os.ReadFile("json/login.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var handler services.Handler
|
||||
err = json.Unmarshal(handlerBytes, &handler)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
brokerAddr := ":8081"
|
||||
flow := services.SetupHandler(handler, brokerAddr)
|
||||
if flow.Error != nil {
|
||||
fmt.Println("Error setting up handler:", flow.Error)
|
||||
return
|
||||
}
|
||||
flow.Start(context.Background(), ":5000")
|
||||
}
|
||||
|
||||
func init() {
|
||||
dag.AddHandler("render-html", func(id string) mq.Processor { return handlers.NewRenderHTMLNode(id) })
|
||||
dag.AddHandler("condition", func(id string) mq.Processor { return handlers.NewCondition(id) })
|
||||
}
|
63
services/examples/login.json
Normal file
63
services/examples/login.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Username or Email",
|
||||
"order": 1,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "text",
|
||||
"class": "form-group",
|
||||
"name": "username",
|
||||
"placeholder": "Enter your username or email"
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"title": "Password",
|
||||
"order": 2,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "password",
|
||||
"class": "form-group",
|
||||
"name": "password",
|
||||
"placeholder": "Enter your password"
|
||||
}
|
||||
},
|
||||
"remember_me": {
|
||||
"type": "boolean",
|
||||
"title": "Remember Me",
|
||||
"order": 3,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "checkbox",
|
||||
"class": "form-check",
|
||||
"name": "remember_me"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "username", "password" ],
|
||||
"form": {
|
||||
"class": "form-horizontal",
|
||||
"action": "/process?task_id={{task_id}}&next=true",
|
||||
"method": "POST",
|
||||
"enctype": "application/x-www-form-urlencoded",
|
||||
"groups": [
|
||||
{
|
||||
"title": "Login Credentials",
|
||||
"fields": [ "username", "password", "remember_me" ]
|
||||
}
|
||||
],
|
||||
"submit": {
|
||||
"type": "submit",
|
||||
"label": "Log In",
|
||||
"class": "btn btn-primary"
|
||||
},
|
||||
"reset": {
|
||||
"type": "reset",
|
||||
"label": "Clear",
|
||||
"class": "btn btn-secondary"
|
||||
}
|
||||
}
|
||||
}
|
105
services/examples/schema.json
Normal file
105
services/examples/schema.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string",
|
||||
"title": "First Name",
|
||||
"order": 1,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"class": "form-group",
|
||||
"name": "first_name"
|
||||
}
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"title": "Last Name",
|
||||
"order": 2,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"class": "form-group",
|
||||
"name": "last_name"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"type": "email",
|
||||
"title": "Email Address",
|
||||
"order": 3,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"type": "email",
|
||||
"class": "form-group",
|
||||
"name": "email"
|
||||
}
|
||||
},
|
||||
"user_type": {
|
||||
"type": "string",
|
||||
"title": "User Type",
|
||||
"order": 4,
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"name": "user_type",
|
||||
"options": [ "new", "premium", "standard" ]
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"title": "Priority Level",
|
||||
"order": 5,
|
||||
"ui": {
|
||||
"element": "select",
|
||||
"class": "form-group",
|
||||
"name": "priority",
|
||||
"options": [ "low", "medium", "high", "urgent" ]
|
||||
}
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"title": "Subject",
|
||||
"order": 6,
|
||||
"ui": {
|
||||
"element": "input",
|
||||
"class": "form-group",
|
||||
"name": "subject"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "textarea",
|
||||
"title": "Message",
|
||||
"order": 7,
|
||||
"ui": {
|
||||
"element": "textarea",
|
||||
"class": "form-group",
|
||||
"name": "message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ],
|
||||
"form": {
|
||||
"class": "form-horizontal",
|
||||
"action": "/process?task_id={{task_id}}&next=true",
|
||||
"method": "POST",
|
||||
"enctype": "application/x-www-form-urlencoded",
|
||||
"groups": [
|
||||
{
|
||||
"title": "User Information",
|
||||
"fields": [ "first_name", "last_name", "email" ]
|
||||
},
|
||||
{
|
||||
"title": "Ticket Details",
|
||||
"fields": [ "user_type", "priority", "subject", "message" ]
|
||||
}
|
||||
],
|
||||
"submit": {
|
||||
"type": "submit",
|
||||
"label": "Submit",
|
||||
"class": "btn btn-primary"
|
||||
},
|
||||
"reset": {
|
||||
"type": "reset",
|
||||
"label": "Reset",
|
||||
"class": "btn btn-secondary"
|
||||
}
|
||||
}
|
||||
}
|
@@ -77,10 +77,17 @@ func SetupHandler(handler Handler, brokerAddr string, async ...bool) *dag.DAG {
|
||||
return flow
|
||||
}
|
||||
|
||||
type FilterGroup struct {
|
||||
Operator string `json:"operator"`
|
||||
Reverse bool `json:"reverse"`
|
||||
Filters []*filters.Filter `json:"filters"`
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
Filter *filters.Filter `json:"condition"`
|
||||
Node string `json:"node"`
|
||||
ID string `json:"id"`
|
||||
Filter *filters.Filter `json:"condition"`
|
||||
FilterGroup *FilterGroup `json:"group"`
|
||||
Node string `json:"node"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func prepareNode(flow *dag.DAG, node Node) error {
|
||||
@@ -108,7 +115,25 @@ func prepareNode(flow *dag.DAG, node Node) error {
|
||||
conditions := make(map[string]dag.Condition)
|
||||
for key, cond := range fil {
|
||||
condition[key] = cond.Node
|
||||
conditions[key] = cond.Filter
|
||||
if cond.Filter != nil {
|
||||
conditions[key] = cond.Filter
|
||||
} else if cond.FilterGroup != nil {
|
||||
cond.FilterGroup.Operator = strings.ToUpper(cond.FilterGroup.Operator)
|
||||
if !slices.Contains([]string{"AND", "OR"}, cond.FilterGroup.Operator) {
|
||||
cond.FilterGroup.Operator = "AND"
|
||||
}
|
||||
var fillers []filters.Condition
|
||||
for _, f := range cond.FilterGroup.Filters {
|
||||
if f != nil {
|
||||
fillers = append(fillers, f)
|
||||
}
|
||||
}
|
||||
conditions[key] = &filters.FilterGroup{
|
||||
Operator: filters.Boolean(cond.FilterGroup.Operator),
|
||||
Reverse: cond.FilterGroup.Reverse,
|
||||
Filters: fillers,
|
||||
}
|
||||
}
|
||||
}
|
||||
flow.AddCondition(node.ID, condition)
|
||||
nodeHandler.SetConditions(conditions)
|
||||
|
Reference in New Issue
Block a user