mirror of
https://github.com/oarkflow/mq.git
synced 2025-09-26 20:11:16 +08:00
update
This commit is contained in:
@@ -20,10 +20,12 @@ type DataHandler struct {
|
||||
}
|
||||
|
||||
func (h *DataHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err)}
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
if data == nil {
|
||||
data = make(map[string]any)
|
||||
}
|
||||
|
||||
operation, ok := h.Payload.Data["operation"].(string)
|
||||
@@ -34,6 +36,8 @@ func (h *DataHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result
|
||||
var result map[string]any
|
||||
var conditionStatus string
|
||||
switch operation {
|
||||
case "extract":
|
||||
result = h.extractData(ctx, data)
|
||||
case "sort":
|
||||
result = h.sortData(data)
|
||||
case "deduplicate":
|
||||
@@ -73,6 +77,34 @@ func (h *DataHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DataHandler) extractData(ctx context.Context, data map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
|
||||
// Copy existing data
|
||||
for k, v := range data {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Extract data based on mapping
|
||||
if h.Payload.Mapping != nil {
|
||||
for targetField, sourcePath := range h.Payload.Mapping {
|
||||
_, val := dag.GetVal(ctx, sourcePath, data)
|
||||
if val != nil {
|
||||
result[targetField] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle default values
|
||||
if defaultPath, ok := h.Payload.Data["default_path"].(string); ok {
|
||||
if path, exists := result["path"]; !exists || path == "" {
|
||||
result["path"] = defaultPath
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *DataHandler) sortData(data map[string]any) map[string]any {
|
||||
result := make(map[string]any)
|
||||
|
||||
|
@@ -16,10 +16,9 @@ type FieldHandler struct {
|
||||
}
|
||||
|
||||
func (h *FieldHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err)}
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
|
||||
operation, ok := h.Payload.Data["operation"].(string)
|
||||
|
@@ -15,8 +15,7 @@ type FlattenHandler struct {
|
||||
}
|
||||
|
||||
func (h *FlattenHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
|
@@ -18,35 +18,60 @@ type FormatHandler struct {
|
||||
}
|
||||
|
||||
func (h *FormatHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
if data == nil {
|
||||
data = make(map[string]any)
|
||||
}
|
||||
|
||||
// Handle mapping first
|
||||
if h.Payload.Mapping != nil {
|
||||
for k, v := range h.Payload.Mapping {
|
||||
_, val := dag.GetVal(ctx, v, data)
|
||||
data[k] = val
|
||||
}
|
||||
}
|
||||
|
||||
formatType, ok := h.Payload.Data["format_type"].(string)
|
||||
if !ok {
|
||||
return mq.Result{Error: fmt.Errorf("format_type not specified"), Ctx: ctx}
|
||||
// If no format_type specified, just return the data with mapping applied
|
||||
resultPayload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to marshal result: %w", err), Ctx: ctx}
|
||||
}
|
||||
return mq.Result{Payload: resultPayload, Ctx: ctx}
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
// Copy data to result
|
||||
if data != nil {
|
||||
result = make(map[string]any)
|
||||
for k, v := range data {
|
||||
result[k] = v
|
||||
}
|
||||
} else {
|
||||
result = make(map[string]any)
|
||||
}
|
||||
|
||||
switch formatType {
|
||||
case "string":
|
||||
result = h.formatToString(data)
|
||||
result = h.formatToString(result)
|
||||
case "number":
|
||||
result = h.formatToNumber(data)
|
||||
result = h.formatToNumber(result)
|
||||
case "date":
|
||||
result = h.formatDate(data)
|
||||
result = h.formatDate(result)
|
||||
case "currency":
|
||||
result = h.formatCurrency(data)
|
||||
result = h.formatCurrency(result)
|
||||
case "uppercase":
|
||||
result = h.formatUppercase(data)
|
||||
result = h.formatUppercase(result)
|
||||
case "lowercase":
|
||||
result = h.formatLowercase(data)
|
||||
result = h.formatLowercase(result)
|
||||
case "capitalize":
|
||||
result = h.formatCapitalize(data)
|
||||
result = h.formatCapitalize(result)
|
||||
case "trim":
|
||||
result = h.formatTrim(data)
|
||||
result = h.formatTrim(result)
|
||||
default:
|
||||
return mq.Result{Error: fmt.Errorf("unsupported format_type: %s", formatType), Ctx: ctx}
|
||||
}
|
||||
|
@@ -16,10 +16,9 @@ type GroupHandler struct {
|
||||
}
|
||||
|
||||
func (h *GroupHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err)}
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
|
||||
// Extract the data array
|
||||
|
@@ -15,10 +15,9 @@ type JSONHandler struct {
|
||||
}
|
||||
|
||||
func (h *JSONHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err)}
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
|
||||
operation, ok := h.Payload.Data["operation"].(string)
|
||||
|
@@ -16,10 +16,9 @@ type SplitHandler struct {
|
||||
}
|
||||
|
||||
func (h *SplitHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err)}
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
|
||||
operation, ok := h.Payload.Data["operation"].(string)
|
||||
@@ -149,10 +148,9 @@ type JoinHandler struct {
|
||||
}
|
||||
|
||||
func (h *JoinHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
var data map[string]any
|
||||
err := json.Unmarshal(task.Payload, &data)
|
||||
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||
if err != nil {
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err)}
|
||||
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||
}
|
||||
|
||||
operation, ok := h.Payload.Data["operation"].(string)
|
||||
|
@@ -85,39 +85,97 @@ func (receiver *RunHandler) Extend() contracts.Extend {
|
||||
Aliases: []string{"hf"},
|
||||
Usage: "Header to be passed to the handler",
|
||||
},
|
||||
{
|
||||
Name: "enhanced",
|
||||
Value: "false",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Run as enhanced handler with workflow engine support",
|
||||
},
|
||||
{
|
||||
Name: "workflow",
|
||||
Value: "false",
|
||||
Aliases: []string{"w"},
|
||||
Usage: "Enable workflow engine features",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
} // Handle Execute the console command.
|
||||
func (receiver *RunHandler) Handle(ctx contracts.Context) error {
|
||||
name := ctx.Option("name")
|
||||
serve := ctx.Option("serve")
|
||||
enhanced := ctx.Option("enhanced")
|
||||
workflow := ctx.Option("workflow")
|
||||
|
||||
if serve == "" {
|
||||
serve = "false"
|
||||
}
|
||||
if enhanced == "" {
|
||||
enhanced = "false"
|
||||
}
|
||||
if workflow == "" {
|
||||
workflow = "false"
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return errors.New("Handler name has to be provided")
|
||||
}
|
||||
handler := receiver.userConfig.GetHandler(name)
|
||||
if handler == nil {
|
||||
return errors.New("Handler not found")
|
||||
|
||||
// Check if enhanced handler is requested or if handler is configured as enhanced
|
||||
isEnhanced := enhanced == "true" || receiver.userConfig.IsEnhancedHandler(name)
|
||||
|
||||
var flow *dag.DAG
|
||||
var err error
|
||||
|
||||
if isEnhanced {
|
||||
// Try to get enhanced handler first
|
||||
enhancedHandler := receiver.userConfig.GetEnhancedHandler(name)
|
||||
if enhancedHandler != nil {
|
||||
fmt.Printf("Setting up enhanced handler: %s\n", name)
|
||||
flow, err = services.SetupEnhancedHandler(*enhancedHandler, receiver.brokerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup enhanced handler: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Fallback to traditional handler
|
||||
handler := receiver.userConfig.GetHandler(name)
|
||||
if handler == nil {
|
||||
return errors.New("Handler not found")
|
||||
}
|
||||
flow = services.SetupHandler(*handler, receiver.brokerAddr)
|
||||
}
|
||||
} else {
|
||||
// Traditional handler
|
||||
handler := receiver.userConfig.GetHandler(name)
|
||||
if handler == nil {
|
||||
return errors.New("Handler not found")
|
||||
}
|
||||
flow = services.SetupHandler(*handler, receiver.brokerAddr)
|
||||
}
|
||||
flow := services.SetupHandler(*handler, receiver.brokerAddr)
|
||||
|
||||
if flow.Error != nil {
|
||||
panic(flow.Error)
|
||||
}
|
||||
|
||||
port := ctx.Option("port")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
if serve != "false" {
|
||||
fmt.Printf("Starting %s handler server on port %s\n",
|
||||
func() string {
|
||||
if isEnhanced {
|
||||
return "enhanced"
|
||||
} else {
|
||||
return "traditional"
|
||||
}
|
||||
}(), port)
|
||||
if err := flow.Start(context.Background(), ":"+port); err != nil {
|
||||
return fmt.Errorf("error starting handler: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := receiver.getData(ctx, "data", "data-file", "test/data", false)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -130,8 +188,31 @@ func (receiver *RunHandler) Handle(ctx contracts.Context) error {
|
||||
if headerData == nil {
|
||||
headerData = make(map[string]any)
|
||||
}
|
||||
c = context.WithValue(c, "header", headerData)
|
||||
fmt.Println("Running Handler: ", name)
|
||||
|
||||
// Convert headerData to map[string]any if it's not already
|
||||
var headerMap map[string]any
|
||||
switch h := headerData.(type) {
|
||||
case map[string]any:
|
||||
headerMap = h
|
||||
default:
|
||||
headerMap = make(map[string]any)
|
||||
}
|
||||
|
||||
// Add enhanced context information if workflow is enabled
|
||||
if workflow == "true" || isEnhanced {
|
||||
headerMap["workflow_enabled"] = true
|
||||
headerMap["enhanced_mode"] = isEnhanced
|
||||
}
|
||||
|
||||
c = context.WithValue(c, "header", headerMap)
|
||||
fmt.Printf("Running %s Handler: %s\n",
|
||||
func() string {
|
||||
if isEnhanced {
|
||||
return "Enhanced"
|
||||
} else {
|
||||
return "Traditional"
|
||||
}
|
||||
}(), name)
|
||||
rs := send(c, flow, data)
|
||||
if rs.Error == nil {
|
||||
fmt.Println("Handler response", string(rs.Payload))
|
||||
@@ -197,3 +278,39 @@ func Unmarshal(data any, dst any) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enhanced helper functions
|
||||
|
||||
// getHandlerInfo returns information about the handler (traditional or enhanced)
|
||||
func (receiver *RunHandler) getHandlerInfo(name string) (interface{}, bool) {
|
||||
// Check enhanced handlers first
|
||||
if enhancedHandler := receiver.userConfig.GetEnhancedHandler(name); enhancedHandler != nil {
|
||||
return *enhancedHandler, true
|
||||
}
|
||||
|
||||
// Check traditional handlers
|
||||
if handler := receiver.userConfig.GetHandler(name); handler != nil {
|
||||
return *handler, false
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// listAvailableHandlers lists all available handlers (both traditional and enhanced)
|
||||
func (receiver *RunHandler) listAvailableHandlers() {
|
||||
fmt.Println("Available Traditional Handlers:")
|
||||
for _, handler := range receiver.userConfig.Policy.Handlers {
|
||||
fmt.Printf(" - %s (%s)\n", handler.Name, handler.Key)
|
||||
}
|
||||
|
||||
if len(receiver.userConfig.Policy.EnhancedHandlers) > 0 {
|
||||
fmt.Println("\nAvailable Enhanced Handlers:")
|
||||
for _, handler := range receiver.userConfig.Policy.EnhancedHandlers {
|
||||
status := "disabled"
|
||||
if handler.WorkflowEnabled {
|
||||
status = "workflow enabled"
|
||||
}
|
||||
fmt.Printf(" - %s (%s) [%s]\n", handler.Name, handler.Key, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"name": "SMS Workflow",
|
||||
"route_uri": "/api/v1/sms/send",
|
||||
"route_method": "POST",
|
||||
"handler_key": "sms:workflow",
|
||||
"operation": "custom",
|
||||
"schema_file": "sms-send.json"
|
||||
},
|
||||
{
|
||||
"name": "Email Workflow",
|
||||
"route_uri": "/api/v1/email/send",
|
||||
"route_method": "POST",
|
||||
"handler_key": "email:workflow",
|
||||
"operation": "custom",
|
||||
"schema_file": "email-send.json"
|
||||
},
|
||||
{
|
||||
"name": "Blog Engine",
|
||||
"route_uri": "/api/v1/blog/*",
|
||||
"route_method": "GET",
|
||||
"handler_key": "blog:engine",
|
||||
"operation": "custom"
|
||||
},
|
||||
{
|
||||
"name": "SMS Workflow DAG",
|
||||
"route_uri": "/api/v1/sms/dag",
|
||||
"route_method": "GET",
|
||||
"handler_key": "sms:workflow",
|
||||
"operation": "custom"
|
||||
},
|
||||
{
|
||||
"name": "Email Workflow DAG",
|
||||
"route_uri": "/api/v1/email/dag",
|
||||
"route_method": "GET",
|
||||
"handler_key": "email:workflow",
|
||||
"operation": "custom"
|
||||
},
|
||||
{
|
||||
"name": "SMS Page",
|
||||
"route_uri": "/sms",
|
||||
"route_method": "GET",
|
||||
"handler_key": "sms:workflow",
|
||||
"operation": "custom"
|
||||
},
|
||||
{
|
||||
"name": "Email Page",
|
||||
"route_uri": "/email",
|
||||
"route_method": "GET",
|
||||
"handler_key": "email:workflow",
|
||||
"operation": "custom"
|
||||
},
|
||||
{
|
||||
"name": "Blog Page",
|
||||
"route_uri": "/blog/*",
|
||||
"route_method": "GET",
|
||||
"handler_key": "blog:engine",
|
||||
"operation": "custom"
|
||||
},
|
||||
{
|
||||
"name": "Home Page",
|
||||
"route_uri": "/",
|
||||
"route_method": "GET",
|
||||
"handler_key": "blog:engine",
|
||||
"operation": "custom"
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"name": "Blog Engine",
|
||||
"key": "blog:engine",
|
||||
"debug": false,
|
||||
"disable_log": false,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"name": "Start Blog Engine",
|
||||
"node": "start",
|
||||
"first_node": true,
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"workflow": "blog_rendering",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "parse_route",
|
||||
"name": "Parse Blog Route",
|
||||
"node": "data",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"path": "header.param.path",
|
||||
"category": "header.query.category",
|
||||
"tag": "header.query.tag"
|
||||
},
|
||||
"additional_data": {
|
||||
"operation": "extract",
|
||||
"default_path": "index"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_content",
|
||||
"name": "Load Blog Content",
|
||||
"node": "condition",
|
||||
"data": {
|
||||
"conditions": {
|
||||
"post": {
|
||||
"filter": {
|
||||
"key": "path",
|
||||
"operator": "startsWith",
|
||||
"value": "post/"
|
||||
},
|
||||
"node": "load_post"
|
||||
},
|
||||
"category": {
|
||||
"filter": {
|
||||
"key": "category",
|
||||
"operator": "exists"
|
||||
},
|
||||
"node": "load_category"
|
||||
},
|
||||
"index": {
|
||||
"filter": {
|
||||
"key": "path",
|
||||
"operator": "eq",
|
||||
"value": "index"
|
||||
},
|
||||
"node": "load_index"
|
||||
}
|
||||
},
|
||||
"additional_data": {
|
||||
"default_action": "load_index"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_post",
|
||||
"name": "Load Blog Post",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"content_type": "eval.{{'post'}}",
|
||||
"title": "eval.{{'Sample Blog Post'}}",
|
||||
"author": "eval.{{'John Doe'}}",
|
||||
"date": "eval.{{now()}}",
|
||||
"content": "eval.{{'This is a sample blog post content. It demonstrates the blog engine workflow.'}}",
|
||||
"tags": "eval.{{['technology', 'workflow', 'automation']}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_category",
|
||||
"name": "Load Category Posts",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"content_type": "eval.{{'category'}}",
|
||||
"category": "category",
|
||||
"posts": "eval.{{[{'title': 'Post 1', 'slug': 'post-1'}, {'title': 'Post 2', 'slug': 'post-2'}]}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "load_index",
|
||||
"name": "Load Blog Index",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"content_type": "eval.{{'index'}}",
|
||||
"recent_posts": "eval.{{[{'title': 'Latest Post', 'slug': 'latest-post', 'excerpt': 'This is the latest blog post...'}]}}",
|
||||
"categories": "eval.{{['Technology', 'Tutorials', 'News']}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "render_blog",
|
||||
"name": "Render Blog Page",
|
||||
"node": "render-html",
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"template_file": "templates/blog.html"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"name": "Blog Output",
|
||||
"node": "output",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"content_type": "eval.{{'text/html'}}",
|
||||
"rendered": "html_content"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "start",
|
||||
"label": "initialize",
|
||||
"target": [ "parse_route" ]
|
||||
},
|
||||
{
|
||||
"source": "parse_route",
|
||||
"label": "parsed",
|
||||
"target": [ "load_content" ]
|
||||
},
|
||||
{
|
||||
"source": "load_content.post",
|
||||
"label": "load_post_content",
|
||||
"target": [ "load_post" ]
|
||||
},
|
||||
{
|
||||
"source": "load_content.category",
|
||||
"label": "load_category_content",
|
||||
"target": [ "load_category" ]
|
||||
},
|
||||
{
|
||||
"source": "load_content.index",
|
||||
"label": "load_index_content",
|
||||
"target": [ "load_index" ]
|
||||
},
|
||||
{
|
||||
"source": "load_post",
|
||||
"label": "post_loaded",
|
||||
"target": [ "render_blog" ]
|
||||
},
|
||||
{
|
||||
"source": "load_category",
|
||||
"label": "category_loaded",
|
||||
"target": [ "render_blog" ]
|
||||
},
|
||||
{
|
||||
"source": "load_index",
|
||||
"label": "index_loaded",
|
||||
"target": [ "render_blog" ]
|
||||
},
|
||||
{
|
||||
"source": "render_blog",
|
||||
"label": "rendered",
|
||||
"target": [ "output" ]
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"name": "Email Workflow Engine",
|
||||
"key": "email:workflow",
|
||||
"debug": false,
|
||||
"disable_log": false,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"name": "Start Email Workflow",
|
||||
"node": "start",
|
||||
"first_node": true,
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"workflow": "email_sending",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "validate_email",
|
||||
"name": "Validate Email Input",
|
||||
"node": "data",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"to": "body.to",
|
||||
"from": "body.from",
|
||||
"subject": "body.subject",
|
||||
"body": "body.body",
|
||||
"html": "body.html"
|
||||
},
|
||||
"additional_data": {
|
||||
"operation": "validate_fields",
|
||||
"validation_rules": {
|
||||
"to": { "required": true, "type": "email" },
|
||||
"from": { "required": true, "type": "email" },
|
||||
"subject": { "required": true, "max_length": 255 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "prepare_template",
|
||||
"name": "Prepare Email Template",
|
||||
"node": "render-html",
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"template_file": "templates/email-template.html",
|
||||
"engine": "handlebars"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "send_email",
|
||||
"name": "Send Email",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"provider": "eval.{{'smtp'}}",
|
||||
"status": "eval.{{'sent'}}",
|
||||
"message_id": "eval.{{'email_' + generateID()}}",
|
||||
"cost": "eval.{{0.001}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"format_type": "string",
|
||||
"smtp_config": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "log_email_result",
|
||||
"name": "Log Email Result",
|
||||
"node": "log",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"event": "eval.{{'email_sent'}}",
|
||||
"timestamp": "eval.{{now()}}",
|
||||
"result": "eval.{{{'to': to, 'subject': subject, 'status': status, 'message_id': message_id}}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"operation": "info",
|
||||
"log_level": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"name": "Email Response",
|
||||
"node": "output",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"success": "eval.{{true}}",
|
||||
"message": "eval.{{'Email sent successfully'}}",
|
||||
"to": "to",
|
||||
"subject": "subject",
|
||||
"message_id": "message_id",
|
||||
"timestamp": "eval.{{now()}}",
|
||||
"status": "eval.{{'delivered'}}"
|
||||
},
|
||||
"templates": {
|
||||
"html": "email-template.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "start",
|
||||
"label": "initialize",
|
||||
"target": [ "validate_email" ]
|
||||
},
|
||||
{
|
||||
"source": "validate_email",
|
||||
"label": "validated",
|
||||
"target": [ "prepare_template" ]
|
||||
},
|
||||
{
|
||||
"source": "prepare_template",
|
||||
"label": "template_ready",
|
||||
"target": [ "send_email" ]
|
||||
},
|
||||
{
|
||||
"source": "send_email",
|
||||
"label": "sent",
|
||||
"target": [ "log_email_result" ]
|
||||
},
|
||||
{
|
||||
"source": "log_email_result",
|
||||
"label": "logged",
|
||||
"target": [ "output" ]
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"name": "SMS Workflow Engine",
|
||||
"key": "sms:workflow",
|
||||
"debug": false,
|
||||
"disable_log": false,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"name": "Start SMS Workflow",
|
||||
"node": "start",
|
||||
"first_node": true,
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"workflow": "sms_sending",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "validate_input",
|
||||
"name": "Validate SMS Input",
|
||||
"node": "data",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"message": "body.message",
|
||||
"recipients": "body.recipients",
|
||||
"sender": "body.sender",
|
||||
"priority": "body.priority"
|
||||
},
|
||||
"additional_data": {
|
||||
"operation": "validate_fields",
|
||||
"validation_rules": {
|
||||
"message": { "required": true, "max_length": 1000 },
|
||||
"recipients": { "required": true, "type": "array" },
|
||||
"sender": { "required": true, "max_length": 255 },
|
||||
"priority": { "required": false, "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "select_provider",
|
||||
"name": "Select SMS Provider",
|
||||
"node": "condition",
|
||||
"data": {
|
||||
"conditions": {
|
||||
"premium": {
|
||||
"filter": {
|
||||
"key": "priority",
|
||||
"operator": "eq",
|
||||
"value": "high"
|
||||
},
|
||||
"node": "use_twilio"
|
||||
},
|
||||
"standard": {
|
||||
"filter": {
|
||||
"key": "priority",
|
||||
"operator": "eq",
|
||||
"value": "medium"
|
||||
},
|
||||
"node": "use_nexmo"
|
||||
},
|
||||
"bulk": {
|
||||
"filter": {
|
||||
"key": "priority",
|
||||
"operator": "eq",
|
||||
"value": "low"
|
||||
},
|
||||
"node": "use_aws"
|
||||
}
|
||||
},
|
||||
"additional_data": {
|
||||
"default_provider": "nexmo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "use_twilio",
|
||||
"name": "Send via Twilio",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"provider": "eval.{{'twilio'}}",
|
||||
"cost": "eval.{{0.0075}}",
|
||||
"status": "eval.{{'sent'}}",
|
||||
"message_id": "eval.{{'twilio_' + generateID()}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"format_type": "string",
|
||||
"provider_config": {
|
||||
"name": "Twilio",
|
||||
"type": "premium",
|
||||
"reliability": 0.99
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "use_nexmo",
|
||||
"name": "Send via Nexmo",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"provider": "eval.{{'nexmo'}}",
|
||||
"cost": "eval.{{0.0065}}",
|
||||
"status": "eval.{{'sent'}}",
|
||||
"message_id": "eval.{{'nexmo_' + generateID()}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"format_type": "string",
|
||||
"provider_config": {
|
||||
"name": "Vonage (Nexmo)",
|
||||
"type": "standard",
|
||||
"reliability": 0.97
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "use_aws",
|
||||
"name": "Send via AWS SNS",
|
||||
"node": "format",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"provider": "eval.{{'aws'}}",
|
||||
"cost": "eval.{{0.0055}}",
|
||||
"status": "eval.{{'sent'}}",
|
||||
"message_id": "eval.{{'aws_' + generateID()}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"format_type": "string",
|
||||
"provider_config": {
|
||||
"name": "AWS SNS",
|
||||
"type": "bulk",
|
||||
"reliability": 0.95
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "log_result",
|
||||
"name": "Log SMS Result",
|
||||
"node": "log",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"event": "eval.{{'sms_sent'}}",
|
||||
"timestamp": "eval.{{now()}}",
|
||||
"result": "eval.{{{'provider': provider, 'cost': cost, 'status': status, 'message_id': message_id}}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"operation": "info",
|
||||
"log_level": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"name": "SMS Response",
|
||||
"node": "output",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"success": "eval.{{true}}",
|
||||
"message": "eval.{{'SMS sent successfully'}}",
|
||||
"provider_used": "provider",
|
||||
"cost": "cost",
|
||||
"message_id": "message_id",
|
||||
"timestamp": "eval.{{now()}}",
|
||||
"status": "eval.{{'delivered'}}"
|
||||
},
|
||||
"templates": {
|
||||
"html": "sms-template.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "start",
|
||||
"label": "initialize",
|
||||
"target": [ "validate_input" ]
|
||||
},
|
||||
{
|
||||
"source": "validate_input",
|
||||
"label": "validated",
|
||||
"target": [ "select_provider" ]
|
||||
},
|
||||
{
|
||||
"source": "select_provider.premium",
|
||||
"label": "use_premium",
|
||||
"target": [ "use_twilio" ]
|
||||
},
|
||||
{
|
||||
"source": "select_provider.standard",
|
||||
"label": "use_standard",
|
||||
"target": [ "use_nexmo" ]
|
||||
},
|
||||
{
|
||||
"source": "select_provider.bulk",
|
||||
"label": "use_bulk",
|
||||
"target": [ "use_aws" ]
|
||||
},
|
||||
{
|
||||
"source": "use_twilio",
|
||||
"label": "sent",
|
||||
"target": [ "log_result" ]
|
||||
},
|
||||
{
|
||||
"source": "use_nexmo",
|
||||
"label": "sent",
|
||||
"target": [ "log_result" ]
|
||||
},
|
||||
{
|
||||
"source": "use_aws",
|
||||
"label": "sent",
|
||||
"target": [ "log_result" ]
|
||||
},
|
||||
{
|
||||
"source": "log_result",
|
||||
"label": "logged",
|
||||
"target": [ "output" ]
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Recipient email address"
|
||||
},
|
||||
"from": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Sender email address"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"description": "Email subject"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Plain text email body"
|
||||
},
|
||||
"html": {
|
||||
"type": "string",
|
||||
"description": "HTML email body"
|
||||
},
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filename": { "type": "string" },
|
||||
"content": { "type": "string" },
|
||||
"contentType": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"description": "Email attachments"
|
||||
}
|
||||
},
|
||||
"required": [ "to", "from", "subject" ],
|
||||
"additionalProperties": false
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"maxLength": 160,
|
||||
"minLength": 1,
|
||||
"description": "SMS message content"
|
||||
},
|
||||
"recipients": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^\\+[1-9]\\d{1,14}$"
|
||||
},
|
||||
"minItems": 1,
|
||||
"description": "Array of phone numbers in E.164 format"
|
||||
},
|
||||
"sender": {
|
||||
"type": "string",
|
||||
"description": "Sender identifier"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": [ "high", "medium", "low" ],
|
||||
"default": "medium",
|
||||
"description": "SMS priority level"
|
||||
}
|
||||
},
|
||||
"required": [ "message", "recipients", "sender" ],
|
||||
"additionalProperties": false
|
||||
}
|
16
services/examples/json-engine/config/policies/web.json
Normal file
16
services/examples/json-engine/config/policies/web.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"prefix": "/",
|
||||
"middlewares": [
|
||||
{ "name": "cors" }
|
||||
],
|
||||
"static": {
|
||||
"dir": "./public",
|
||||
"prefix": "/static",
|
||||
"options": {
|
||||
"byte_range": true,
|
||||
"browse": true,
|
||||
"compress": true,
|
||||
"index_file": "index.html"
|
||||
}
|
||||
}
|
||||
}
|
BIN
services/examples/json-engine/main
Executable file
BIN
services/examples/json-engine/main
Executable file
Binary file not shown.
@@ -1,54 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/oarkflow/cli"
|
||||
"github.com/oarkflow/cli/console"
|
||||
"github.com/oarkflow/cli/contracts"
|
||||
|
||||
"github.com/oarkflow/mq"
|
||||
"github.com/oarkflow/mq/dag"
|
||||
"github.com/oarkflow/mq/handlers"
|
||||
"github.com/oarkflow/mq/services"
|
||||
dagConsole "github.com/oarkflow/mq/services/console"
|
||||
)
|
||||
|
||||
func loadConfiguration(configPath string) (*AppConfiguration, error) {
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
var config AppConfiguration
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON config: %v", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
configPath := flag.String("config", "sms-app.json", "Path to JSON configuration file")
|
||||
flag.Parse()
|
||||
|
||||
// If positional args provided, use the first one
|
||||
if len(os.Args) > 1 && !flag.Parsed() {
|
||||
*configPath = os.Args[1]
|
||||
}
|
||||
|
||||
// Load configuration first
|
||||
config, err := loadConfiguration(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
// Create JSON engine with configuration
|
||||
engine := NewJSONEngine(config)
|
||||
|
||||
// Compile configuration
|
||||
if err := engine.Compile(); err != nil {
|
||||
log.Fatalf("Failed to compile configuration: %v", err)
|
||||
}
|
||||
|
||||
// Start server
|
||||
if err := engine.Start(); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
handlers.Init()
|
||||
brokerAddr := ":5051"
|
||||
loader := services.NewLoader("config")
|
||||
loader.Load()
|
||||
serverApp := fiber.New(fiber.Config{EnablePrintRoutes: true})
|
||||
services.Setup(loader, serverApp, brokerAddr)
|
||||
cli.Run("json-engine", "v1.0.0", func(client contracts.Cli) []contracts.Command {
|
||||
return []contracts.Command{
|
||||
console.NewListCommand(client),
|
||||
dagConsole.NewRunHandler(loader.UserConfig, loader.ParsedPath, brokerAddr),
|
||||
dagConsole.NewRunServer(serverApp),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register standard handlers
|
||||
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) })
|
||||
dag.AddHandler("output", func(id string) mq.Processor { return handlers.NewOutputHandler(id) })
|
||||
dag.AddHandler("print", func(id string) mq.Processor { return handlers.NewPrintHandler(id) })
|
||||
dag.AddHandler("format", func(id string) mq.Processor { return handlers.NewFormatHandler(id) })
|
||||
dag.AddHandler("data", func(id string) mq.Processor { return handlers.NewDataHandler(id) })
|
||||
dag.AddHandler("log", func(id string) mq.Processor { return handlers.NewLogHandler(id) })
|
||||
dag.AddHandler("json", func(id string) mq.Processor { return handlers.NewJSONHandler(id) })
|
||||
dag.AddHandler("split", func(id string) mq.Processor { return handlers.NewSplitHandler(id) })
|
||||
dag.AddHandler("join", func(id string) mq.Processor { return handlers.NewJoinHandler(id) })
|
||||
dag.AddHandler("field", func(id string) mq.Processor { return handlers.NewFieldHandler(id) })
|
||||
dag.AddHandler("flatten", func(id string) mq.Processor { return handlers.NewFlattenHandler(id) })
|
||||
dag.AddHandler("group", func(id string) mq.Processor { return handlers.NewGroupHandler(id) })
|
||||
dag.AddHandler("start", func(id string) mq.Processor { return handlers.NewStartHandler(id) })
|
||||
}
|
||||
|
175
services/examples/json-engine/public/index.html
Normal file
175
services/examples/json-engine/public/index.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JSON Engine - Workflow Platform</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.api-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.method {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.post {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.get {
|
||||
background-color: #17a2b8;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 JSON Engine - Workflow Platform</h1>
|
||||
<p>Dynamic workflow engine built with user_config.go and setup.go integration</p>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h2>📧 Email Workflow API</h2>
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span>
|
||||
<strong>/api/v1/email/send</strong> - Send email through workflow engine
|
||||
<div class="code">
|
||||
curl -X POST http://localhost:3000/api/v1/email/send \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"to": "user@example.com",
|
||||
"from": "sender@example.com",
|
||||
"subject": "Test Email",
|
||||
"body": "Hello from JSON Engine!"
|
||||
}'
|
||||
</div>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<strong>/api/v1/email/dag</strong> - View email workflow DAG visualization
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h2>📱 SMS Workflow API</h2>
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span>
|
||||
<strong>/api/v1/sms/send</strong> - Send SMS through workflow engine
|
||||
<div class="code">
|
||||
curl -X POST http://localhost:3000/api/v1/sms/send \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "Hello from JSON Engine!",
|
||||
"recipients": ["+1234567890"],
|
||||
"sender": "JsonEngine",
|
||||
"priority": "medium"
|
||||
}'
|
||||
</div>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<strong>/api/v1/sms/dag</strong> - View SMS workflow DAG visualization
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h2>📝 Blog Engine</h2>
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span>
|
||||
<strong>/api/v1/blog/*</strong> - Dynamic blog content generation
|
||||
<div class="code">
|
||||
# Blog index
|
||||
curl http://localhost:3000/api/v1/blog/
|
||||
|
||||
# Category posts
|
||||
curl http://localhost:3000/api/v1/blog/?category=Technology
|
||||
|
||||
# Individual post
|
||||
curl http://localhost:3000/api/v1/blog/post/sample-post
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h2>🔧 Quick Links</h2>
|
||||
<a href="/api/v1/email/dag" class="button">📧 Email Workflow DAG</a>
|
||||
<a href="/api/v1/sms/dag" class="button">📱 SMS Workflow DAG</a>
|
||||
<a href="/api/v1/blog/" class="button">📝 Blog Engine</a>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h2>🏗️ Architecture</h2>
|
||||
<p>This JSON Engine demonstrates:</p>
|
||||
<ul>
|
||||
<li><strong>user_config.go</strong> - Configuration management for both traditional and enhanced
|
||||
handlers</li>
|
||||
<li><strong>setup.go</strong> - Service setup with enhanced workflow engine integration</li>
|
||||
<li><strong>JSON-driven workflows</strong> - Dynamic handler creation from configuration files</li>
|
||||
<li><strong>DAG visualization</strong> - Visual representation of workflow execution paths</li>
|
||||
<li><strong>API integration</strong> - REST endpoints for workflow execution</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
133
services/examples/json-engine/templates/blog.html
Normal file
133
services/examples/json-engine/templates/blog.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JSON Engine Blog</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.post h2 {
|
||||
color: #2c3e50;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.post-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post-list li {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.post-list a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.post-list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-list p {
|
||||
color: #666;
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 JSON Engine Blog</h1>
|
||||
<p>Powered by Dynamic Workflow Engine</p>
|
||||
</div>
|
||||
|
||||
<div class="blog-content">
|
||||
<h2>Recent Posts</h2>
|
||||
<ul class="post-list">
|
||||
<li>
|
||||
<h3><a href="/blog/post/getting-started">Getting Started with JSON Engine</a></h3>
|
||||
<p>Learn how to build powerful workflows with the JSON Engine framework...</p>
|
||||
<div class="meta">By JSON Engine Team on September 18, 2025</div>
|
||||
</li>
|
||||
<li>
|
||||
<h3><a href="/blog/post/workflow-best-practices">Workflow Best Practices</a></h3>
|
||||
<p>Discover the best practices for designing efficient and maintainable workflows...</p>
|
||||
<div class="meta">By Workflow Expert on September 17, 2025</div>
|
||||
</li>
|
||||
<li>
|
||||
<h3><a href="/blog/post/dynamic-rendering">Dynamic Content Rendering</a></h3>
|
||||
<p>Explore how to create dynamic, data-driven content with the rendering engine...</p>
|
||||
<div class="meta">By Rendering Team on September 16, 2025</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="categories">
|
||||
<h3>Categories</h3>
|
||||
<ul class="post-list">
|
||||
<li><a href="/blog?category=tutorials">Tutorials</a></li>
|
||||
<li><a href="/blog?category=best-practices">Best Practices</a></li>
|
||||
<li><a href="/blog?category=examples">Examples</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
58
services/examples/json-engine/templates/email-template.html
Normal file
58
services/examples/json-engine/templates/email-template.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Template</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #f4f4f4;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>{{index . "subject"}}</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{if index . "html"}}
|
||||
{{index . "html"}}
|
||||
{{else}}
|
||||
<p>{{index . "body"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Sent from JSON Engine Workflow System</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
109
services/examples/json-engine/templates/sms-template.html
Normal file
109
services/examples/json-engine/templates/sms-template.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SMS Notification</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.sms-container {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.recipients {
|
||||
background-color: #e7f3ff;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.priority {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.priority.high {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.priority.medium {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.priority.low {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="sms-container">
|
||||
<div class="header">
|
||||
<h1>📱 SMS Notification</h1>
|
||||
<p>From: {{.sender}}</p>
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
{{.message}}
|
||||
</div>
|
||||
|
||||
<div class="recipients">
|
||||
<strong>Recipients:</strong>
|
||||
{{range .recipients}}
|
||||
<span>{{.}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="meta-info">
|
||||
<p><strong>Priority:</strong> <span class="priority {{.priority}}">{{.priority}}</span></p>
|
||||
<p><strong>Sent:</strong> {{.timestamp}}</p>
|
||||
<p><strong>Status:</strong> {{.status}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"route_uri": "/test-route",
|
||||
"route_method": "POST",
|
||||
"schema_file": "test-route.json",
|
||||
"description": "Handle test route",
|
||||
"model": "test_route",
|
||||
"operation": "custom",
|
||||
"handler_key": "print:check"
|
||||
},
|
||||
{
|
||||
"route_uri": "/print",
|
||||
"route_method": "GET",
|
||||
"description": "Handles print",
|
||||
"model": "print",
|
||||
"operation": "custom",
|
||||
"handler_key": "print:check"
|
||||
},
|
||||
{
|
||||
"route_uri": "/send-email",
|
||||
"route_method": "GET",
|
||||
"description": "Handles send email",
|
||||
"model": "print",
|
||||
"operation": "custom",
|
||||
"handler_key": "email:notification"
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "Login Flow",
|
||||
"key": "login:flow",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "LoginForm",
|
||||
"first_node": true,
|
||||
"node": "render-html",
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"schema_file": "login.json",
|
||||
"template_file": "templates/basic.html"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ValidateLogin",
|
||||
"node": "condition",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
"additional_data": {
|
||||
"conditions": {
|
||||
"default": {
|
||||
"id": "condition:default",
|
||||
"node": "output"
|
||||
},
|
||||
"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": "templates/error.html"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"node": "output",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"login_message": "eval.{{'Login successful!'}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "LoginForm",
|
||||
"target": [ "ValidateLogin" ]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Sample Print",
|
||||
"key": "print:check",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "print1",
|
||||
"node": "print",
|
||||
"first_node": true
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Email Notification System",
|
||||
"key": "email:notification",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "Login",
|
||||
"name": "Check Login",
|
||||
"node_key": "login:flow",
|
||||
"first_node": true
|
||||
},
|
||||
{
|
||||
"id": "ContactForm",
|
||||
"node": "render-html",
|
||||
"data": {
|
||||
"additional_data": {
|
||||
"schema_file": "schema.json",
|
||||
"template_file": "templates/basic.html"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "output",
|
||||
"node": "output",
|
||||
"data": {
|
||||
"mapping": {
|
||||
"login_message": "eval.{{'Email sent successfully!'}}"
|
||||
},
|
||||
"additional_data": {
|
||||
"except_fields": [ "html_content" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "Login.output",
|
||||
"label": "on_success",
|
||||
"target": [ "ContactForm" ]
|
||||
},
|
||||
{
|
||||
"source": "ContactForm",
|
||||
"label": "on_email_sent",
|
||||
"target": [ "output" ]
|
||||
}
|
||||
]
|
||||
}
|
@@ -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": "{{current_uri}}?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"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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": "{{current_uri}}?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"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"description": "users",
|
||||
"required": [ "user_id" ],
|
||||
"properties": {
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"default": "now()"
|
||||
},
|
||||
"user_id": {
|
||||
"type": [
|
||||
"integer",
|
||||
"string"
|
||||
],
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
}
|
16
services/examples/json-service/config/policies/web.json
Normal file
16
services/examples/json-service/config/policies/web.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"prefix": "/",
|
||||
"middlewares": [
|
||||
{"name": "cors"}
|
||||
],
|
||||
"static": {
|
||||
"dir": "./public",
|
||||
"prefix": "/",
|
||||
"options": {
|
||||
"byte_range": true,
|
||||
"browse": true,
|
||||
"compress": true,
|
||||
"index_file": "index.html"
|
||||
}
|
||||
}
|
||||
}
|
36
services/examples/json-service/main.go
Normal file
36
services/examples/json-service/main.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/oarkflow/cli"
|
||||
"github.com/oarkflow/cli/console"
|
||||
"github.com/oarkflow/cli/contracts"
|
||||
|
||||
"github.com/oarkflow/mq"
|
||||
"github.com/oarkflow/mq/dag"
|
||||
"github.com/oarkflow/mq/handlers"
|
||||
"github.com/oarkflow/mq/services"
|
||||
dagConsole "github.com/oarkflow/mq/services/console"
|
||||
)
|
||||
|
||||
func main() {
|
||||
handlers.Init()
|
||||
brokerAddr := ":5051"
|
||||
loader := services.NewLoader("config")
|
||||
loader.Load()
|
||||
serverApp := fiber.New(fiber.Config{EnablePrintRoutes: true})
|
||||
services.Setup(loader, serverApp, brokerAddr)
|
||||
cli.Run("mq", "v0.0.1", func(client contracts.Cli) []contracts.Command {
|
||||
return []contracts.Command{
|
||||
console.NewListCommand(client),
|
||||
dagConsole.NewRunHandler(loader.UserConfig, loader.ParsedPath, brokerAddr),
|
||||
dagConsole.NewRunServer(serverApp),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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) })
|
||||
dag.AddHandler("output", func(id string) mq.Processor { return handlers.NewOutputHandler(id) })
|
||||
}
|
42
services/examples/json-service/templates/basic.html
Normal file
42
services/examples/json-service/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/json-service/templates/error.html
Normal file
134
services/examples/json-service/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>
|
@@ -28,6 +28,7 @@ require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
@@ -36,6 +37,7 @@ require (
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hetiansu5/urlquery v1.2.7 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@@ -63,11 +65,15 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.4.0 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.66.0 // indirect
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
|
@@ -18,6 +18,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -46,6 +48,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hetiansu5/urlquery v1.2.7 h1:jn0h+9pIRqUziSPnRdK/gJK8S5TCnk+HZZx5fRHf8K0=
|
||||
@@ -135,6 +139,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -145,12 +151,18 @@ github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNu
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
|
||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
|
||||
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
|
@@ -31,6 +31,11 @@ import (
|
||||
|
||||
var ValidationInstance Validation
|
||||
|
||||
// Enhanced service instances for workflow engine integration
|
||||
var EnhancedValidationInstance EnhancedValidation
|
||||
var EnhancedDAGServiceInstance EnhancedDAGService
|
||||
var EnhancedServiceManagerInstance EnhancedServiceManager
|
||||
|
||||
func Setup(loader *Loader, serverApp *fiber.App, brokerAddr string) error {
|
||||
if loader.UserConfig == nil || serverApp == nil {
|
||||
return nil
|
||||
@@ -38,6 +43,139 @@ func Setup(loader *Loader, serverApp *fiber.App, brokerAddr string) error {
|
||||
return SetupServices(loader.Prefix(), serverApp, brokerAddr)
|
||||
}
|
||||
|
||||
// Enhanced setup function that supports both traditional and enhanced DAG systems
|
||||
func SetupEnhanced(loader *Loader, serverApp *fiber.App, brokerAddr string, config *EnhancedServiceConfig) error {
|
||||
if loader.UserConfig == nil || serverApp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize enhanced services
|
||||
if config != nil {
|
||||
if err := InitializeEnhancedServices(config); err != nil {
|
||||
return fmt.Errorf("failed to initialize enhanced services: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup both traditional and enhanced services
|
||||
return SetupEnhancedServices(loader.Prefix(), serverApp, brokerAddr)
|
||||
}
|
||||
|
||||
// InitializeEnhancedServices initializes the enhanced service instances
|
||||
func InitializeEnhancedServices(config *EnhancedServiceConfig) error {
|
||||
// Initialize enhanced service manager
|
||||
EnhancedServiceManagerInstance = NewEnhancedServiceManager(config)
|
||||
if err := EnhancedServiceManagerInstance.Initialize(config); err != nil {
|
||||
return fmt.Errorf("failed to initialize enhanced service manager: %w", err)
|
||||
}
|
||||
|
||||
// Initialize enhanced DAG service
|
||||
EnhancedDAGServiceInstance = NewEnhancedDAGService(config)
|
||||
|
||||
// Initialize enhanced validation if config is provided
|
||||
if config.ValidationConfig != nil {
|
||||
validation, err := NewEnhancedValidation(config.ValidationConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize enhanced validation: %w", err)
|
||||
}
|
||||
EnhancedValidationInstance = validation
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupEnhancedServices sets up both traditional and enhanced services with workflow engine support
|
||||
func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string) error {
|
||||
if router == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup traditional handlers
|
||||
err := SetupHandlers(userConfig.Policy.Handlers, brokerAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup enhanced handlers if available
|
||||
if len(userConfig.Policy.EnhancedHandlers) > 0 {
|
||||
err = SetupEnhancedHandlers(userConfig.Policy.EnhancedHandlers, brokerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup enhanced handlers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup background handlers (both traditional and enhanced)
|
||||
setupBackgroundHandlers(brokerAddr)
|
||||
setupEnhancedBackgroundHandlers(brokerAddr)
|
||||
|
||||
// Setup static files and rendering
|
||||
static := userConfig.Policy.Web.Static
|
||||
if static != nil && static.Dir != "" {
|
||||
router.Static(
|
||||
static.Prefix,
|
||||
static.Dir,
|
||||
fiber.Static{
|
||||
Compress: static.Options.Compress,
|
||||
ByteRange: static.Options.ByteRange,
|
||||
Browse: static.Options.Browse,
|
||||
Index: static.Options.IndexFile,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
err = setupRender(prefix, router)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup render: %w", err)
|
||||
}
|
||||
|
||||
// Setup API routes (both traditional and enhanced)
|
||||
return SetupEnhancedAPI(prefix, router, brokerAddr)
|
||||
}
|
||||
|
||||
// SetupEnhancedHandler creates and configures an enhanced handler with workflow engine support
|
||||
func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...bool) (*dag.DAG, error) {
|
||||
// For now, convert enhanced handler to traditional handler and use existing SetupHandler
|
||||
traditionalHandler := Handler{
|
||||
Name: handler.Name,
|
||||
Key: handler.Key,
|
||||
DisableLog: handler.DisableLog,
|
||||
Debug: handler.Debug,
|
||||
}
|
||||
|
||||
// Convert enhanced nodes to traditional nodes
|
||||
for _, enhancedNode := range handler.Nodes {
|
||||
traditionalNode := Node{
|
||||
Name: enhancedNode.Name,
|
||||
ID: enhancedNode.ID,
|
||||
NodeKey: enhancedNode.NodeKey,
|
||||
Node: enhancedNode.Node,
|
||||
FirstNode: enhancedNode.FirstNode,
|
||||
Debug: false, // Default to false
|
||||
}
|
||||
traditionalHandler.Nodes = append(traditionalHandler.Nodes, traditionalNode)
|
||||
}
|
||||
|
||||
// Copy edges and convert loops to proper type
|
||||
traditionalHandler.Edges = handler.Edges
|
||||
|
||||
// Convert enhanced loops (Edge type) to traditional loops (Loop type)
|
||||
for _, enhancedLoop := range handler.Loops {
|
||||
traditionalLoop := Loop{
|
||||
Label: enhancedLoop.Label,
|
||||
Source: enhancedLoop.Source,
|
||||
Target: enhancedLoop.Target,
|
||||
}
|
||||
traditionalHandler.Loops = append(traditionalHandler.Loops, traditionalLoop)
|
||||
}
|
||||
|
||||
// Use existing SetupHandler function
|
||||
dagInstance := SetupHandler(traditionalHandler, brokerAddr, async...)
|
||||
if dagInstance.Error != nil {
|
||||
return nil, dagInstance.Error
|
||||
}
|
||||
|
||||
return dagInstance, nil
|
||||
}
|
||||
|
||||
func SetupHandler(handler Handler, brokerAddr string, async ...bool) *dag.DAG {
|
||||
syncMode := true
|
||||
if len(async) > 0 {
|
||||
@@ -716,3 +854,110 @@ func TopologicalSort(handlers map[string]*HandlerInfo) ([]string, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Enhanced setup functions for workflow engine integration
|
||||
|
||||
// SetupEnhancedHandlers sets up enhanced handlers with workflow engine support
|
||||
func SetupEnhancedHandlers(availableHandlers []EnhancedHandler, brokerAddr string) error {
|
||||
for _, handler := range availableHandlers {
|
||||
fmt.Printf("Setting up enhanced handler: %s (key: %s)\n", handler.Name, handler.Key)
|
||||
_, err := SetupEnhancedHandler(handler, brokerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup enhanced handler %s: %w", handler.Key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupEnhancedBackgroundHandlers sets up enhanced background handlers
|
||||
func setupEnhancedBackgroundHandlers(brokerAddress string) {
|
||||
for _, handler := range userConfig.Policy.EnhancedHandlers {
|
||||
if handler.WorkflowEnabled {
|
||||
dagInstance, err := SetupEnhancedHandler(handler, brokerAddress)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to setup enhanced background handler: %s", handler.Key)
|
||||
continue
|
||||
}
|
||||
|
||||
// Start background processing using traditional DAG
|
||||
go func(dag *dag.DAG, key string) {
|
||||
ctx := context.Background()
|
||||
if err := dag.Consume(ctx); err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to start consumer for enhanced handler: %s", key)
|
||||
}
|
||||
}(dagInstance, handler.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetupEnhancedAPI sets up API routes for both traditional and enhanced handlers
|
||||
func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) error {
|
||||
if prefix != "" {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
api := router.Group(prefix)
|
||||
|
||||
// Setup traditional API routes
|
||||
for _, configRoute := range userConfig.Policy.Web.Apis {
|
||||
routeGroup := api.Group(configRoute.Prefix)
|
||||
mws := setupMiddlewares(configRoute.Middlewares...)
|
||||
if len(mws) > 0 {
|
||||
routeGroup.Use(mws...)
|
||||
}
|
||||
for _, route := range configRoute.Routes {
|
||||
switch route.Operation {
|
||||
case "custom":
|
||||
flow := setupFlow(route, routeGroup, brokerAddr)
|
||||
path := CleanAndMergePaths(route.Uri)
|
||||
switch route.Method {
|
||||
case "GET":
|
||||
routeGroup.Get(path, requestMiddleware(route.Model, route), ruleMiddleware(route.Rules), customRuleMiddleware(route, route.CustomRules), customHandler(flow))
|
||||
case "POST":
|
||||
routeGroup.Post(path, requestMiddleware(route.Model, route), ruleMiddleware(route.Rules), customRuleMiddleware(route, route.CustomRules), customHandler(flow))
|
||||
case "PUT":
|
||||
routeGroup.Put(path, requestMiddleware(route.Model, route), ruleMiddleware(route.Rules), customRuleMiddleware(route, route.CustomRules), customHandler(flow))
|
||||
case "DELETE":
|
||||
routeGroup.Delete(path, requestMiddleware(route.Model, route), ruleMiddleware(route.Rules), customRuleMiddleware(route, route.CustomRules), customHandler(flow))
|
||||
case "PATCH":
|
||||
routeGroup.Patch(path, requestMiddleware(route.Model, route), ruleMiddleware(route.Rules), customRuleMiddleware(route, route.CustomRules), customHandler(flow))
|
||||
}
|
||||
case "dag":
|
||||
flow := setupFlow(route, routeGroup, brokerAddr)
|
||||
path := CleanAndMergePaths(route.Uri)
|
||||
routeGroup.Get(path, func(ctx *fiber.Ctx) error {
|
||||
return getDAGPage(ctx, flow)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup enhanced API routes for enhanced handlers
|
||||
for _, handler := range userConfig.Policy.EnhancedHandlers {
|
||||
if handler.WorkflowEnabled {
|
||||
dagInstance, err := SetupEnhancedHandler(handler, brokerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup enhanced handler for API: %w", err)
|
||||
}
|
||||
|
||||
// Create API endpoint for enhanced handler (using traditional DAG handler)
|
||||
path := fmt.Sprintf("/enhanced/%s", handler.Key)
|
||||
api.Post(path, customHandler(dagInstance))
|
||||
|
||||
// Create DAG visualization endpoint (using traditional DAG visualization)
|
||||
api.Get(path+"/dag", func(ctx *fiber.Ctx) error {
|
||||
return getDAGPage(ctx, dagInstance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions for enhanced features (simplified implementation)
|
||||
|
||||
// addEnhancedNode is a placeholder for future enhanced node functionality
|
||||
func addEnhancedNode(enhancedDAG interface{}, node EnhancedNode) error {
|
||||
// For now, this is a placeholder implementation
|
||||
// In the future, this would add enhanced nodes with workflow capabilities
|
||||
return nil
|
||||
}
|
||||
|
@@ -322,6 +322,58 @@ type Policy struct {
|
||||
ApplicationRules []*filters.ApplicationRule `json:"application_rules" yaml:"application_rules"`
|
||||
Handlers []Handler `json:"handlers" yaml:"handlers"`
|
||||
Flows []Flow `json:"flows" yaml:"flows"`
|
||||
// Enhanced configuration support
|
||||
EnhancedHandlers []EnhancedHandler `json:"enhanced_handlers" yaml:"enhanced_handlers"`
|
||||
EnhancedWorkflows []WorkflowDefinition `json:"enhanced_workflows" yaml:"enhanced_workflows"`
|
||||
ValidationRules []ValidationServiceConfig `json:"validation_rules" yaml:"validation_rules"`
|
||||
}
|
||||
|
||||
// Enhanced workflow configuration structures
|
||||
type WorkflowConfig struct {
|
||||
Engine string `json:"engine" yaml:"engine"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Timeout string `json:"timeout" yaml:"timeout"`
|
||||
RetryPolicy *RetryPolicy `json:"retry_policy,omitempty" yaml:"retry_policy,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type RetryPolicy struct {
|
||||
MaxAttempts int `json:"max_attempts" yaml:"max_attempts"`
|
||||
BackoffType string `json:"backoff_type" yaml:"backoff_type"`
|
||||
InitialDelay string `json:"initial_delay" yaml:"initial_delay"`
|
||||
}
|
||||
|
||||
type WorkflowDefinition struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Steps []WorkflowStep `json:"steps" yaml:"steps"`
|
||||
Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type WorkflowStep struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Handler string `json:"handler" yaml:"handler"`
|
||||
Input map[string]any `json:"input,omitempty" yaml:"input,omitempty"`
|
||||
Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ValidationRule struct {
|
||||
Field string `json:"field" yaml:"field"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Required bool `json:"required" yaml:"required"`
|
||||
Message string `json:"message" yaml:"message"`
|
||||
Options map[string]any `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
}
|
||||
|
||||
type ValidationProcessor struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
@@ -445,3 +497,83 @@ func (c *UserConfig) GetFlow(key string) *Flow {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enhanced methods for workflow engine integration
|
||||
|
||||
// GetEnhancedHandler retrieves an enhanced handler by key
|
||||
func (c *UserConfig) GetEnhancedHandler(handlerName string) *EnhancedHandler {
|
||||
for _, handler := range c.Policy.EnhancedHandlers {
|
||||
if handler.Key == handlerName {
|
||||
return &handler
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEnhancedHandlerList returns list of all enhanced handler keys
|
||||
func (c *UserConfig) GetEnhancedHandlerList() (handlers []string) {
|
||||
for _, handler := range c.Policy.EnhancedHandlers {
|
||||
handlers = append(handlers, handler.Key)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetWorkflowDefinition retrieves a workflow definition by ID
|
||||
func (c *UserConfig) GetWorkflowDefinition(workflowID string) *WorkflowDefinition {
|
||||
for _, workflow := range c.Policy.EnhancedWorkflows {
|
||||
if workflow.ID == workflowID {
|
||||
return &workflow
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetValidationConfig retrieves validation configuration by name
|
||||
func (c *UserConfig) GetValidationConfig(name string) *ValidationServiceConfig {
|
||||
for _, config := range c.Policy.ValidationRules {
|
||||
// Since ValidationServiceConfig doesn't have a name field in enhanced_contracts,
|
||||
// we'll use the index or a different approach
|
||||
if len(c.Policy.ValidationRules) > 0 {
|
||||
return &config
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnhancedHandler checks if a handler is configured as enhanced
|
||||
func (c *UserConfig) IsEnhancedHandler(handlerName string) bool {
|
||||
handler := c.GetEnhancedHandler(handlerName)
|
||||
return handler != nil && handler.WorkflowEnabled
|
||||
}
|
||||
|
||||
// GetAllHandlers returns both traditional and enhanced handlers
|
||||
func (c *UserConfig) GetAllHandlers() map[string]interface{} {
|
||||
handlers := make(map[string]interface{})
|
||||
|
||||
// Add traditional handlers
|
||||
for _, handler := range c.Policy.Handlers {
|
||||
handlers[handler.Key] = handler
|
||||
}
|
||||
|
||||
// Add enhanced handlers
|
||||
for _, handler := range c.Policy.EnhancedHandlers {
|
||||
handlers[handler.Key] = handler
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
// GetHandlerByKey returns either traditional or enhanced handler by key
|
||||
func (c *UserConfig) GetHandlerByKey(key string) interface{} {
|
||||
// Check traditional handlers first
|
||||
if handler := c.GetHandler(key); handler != nil {
|
||||
return *handler
|
||||
}
|
||||
|
||||
// Check enhanced handlers
|
||||
if handler := c.GetEnhancedHandler(key); handler != nil {
|
||||
return *handler
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user