mirror of
https://github.com/oarkflow/mq.git
synced 2025-12-24 11:40:53 +08:00
update
This commit is contained in:
@@ -37,7 +37,7 @@ type Condition interface {
|
||||
|
||||
type ConditionProcessor interface {
|
||||
Processor
|
||||
SetConditions(map[string]Condition)
|
||||
SetConditions(map[string]Condition, ...map[string]string)
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
||||
"github.com/oarkflow/jsonschema"
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ var fieldTemplates = map[string]string{
|
||||
"output": `<output {{.AllAttributes}}>{{.Content}}</output>`,
|
||||
"progress": `<progress {{.AllAttributes}}>{{.Content}}</progress>`,
|
||||
"meter": `<meter {{.AllAttributes}}>{{.Content}}</meter>`,
|
||||
|
||||
|
||||
// Text content elements
|
||||
"h1": `<h1 {{.AllAttributes}}>{{.Content}}</h1>`,
|
||||
"h2": `<h2 {{.AllAttributes}}>{{.Content}}</h2>`,
|
||||
@@ -69,7 +69,7 @@ var fieldTemplates = map[string]string{
|
||||
"abbr": `<abbr {{.AllAttributes}}>{{.Content}}</abbr>`,
|
||||
"address": `<address {{.AllAttributes}}>{{.ContentHTML}}</address>`,
|
||||
"time": `<time {{.AllAttributes}}>{{.Content}}</time>`,
|
||||
|
||||
|
||||
// List elements
|
||||
"ul": `<ul {{.AllAttributes}}>{{.ContentHTML}}</ul>`,
|
||||
"ol": `<ol {{.AllAttributes}}>{{.ContentHTML}}</ol>`,
|
||||
@@ -77,7 +77,7 @@ var fieldTemplates = map[string]string{
|
||||
"dl": `<dl {{.AllAttributes}}>{{.ContentHTML}}</dl>`,
|
||||
"dt": `<dt {{.AllAttributes}}>{{.Content}}</dt>`,
|
||||
"dd": `<dd {{.AllAttributes}}>{{.Content}}</dd>`,
|
||||
|
||||
|
||||
// Links and media
|
||||
"a": `<a {{.AllAttributes}}>{{.Content}}</a>`,
|
||||
"img": `<img {{.AllAttributes}} />`,
|
||||
@@ -87,7 +87,7 @@ var fieldTemplates = map[string]string{
|
||||
"video": `<video {{.AllAttributes}}>{{.ContentHTML}}</video>`,
|
||||
"source": `<source {{.AllAttributes}} />`,
|
||||
"track": `<track {{.AllAttributes}} />`,
|
||||
|
||||
|
||||
// Table elements
|
||||
"table": `<table {{.AllAttributes}}>{{.ContentHTML}}</table>`,
|
||||
"caption": `<caption {{.AllAttributes}}>{{.Content}}</caption>`,
|
||||
@@ -99,7 +99,7 @@ var fieldTemplates = map[string]string{
|
||||
"td": `<td {{.AllAttributes}}>{{.Content}}</td>`,
|
||||
"colgroup": `<colgroup {{.AllAttributes}}>{{.ContentHTML}}</colgroup>`,
|
||||
"col": `<col {{.AllAttributes}} />`,
|
||||
|
||||
|
||||
// Sectioning elements
|
||||
"article": `<article {{.AllAttributes}}>{{.ContentHTML}}</article>`,
|
||||
"section": `<section {{.AllAttributes}}>{{.ContentHTML}}</section>`,
|
||||
@@ -108,12 +108,12 @@ var fieldTemplates = map[string]string{
|
||||
"header": `<header {{.AllAttributes}}>{{.ContentHTML}}</header>`,
|
||||
"footer": `<footer {{.AllAttributes}}>{{.ContentHTML}}</footer>`,
|
||||
"main": `<main {{.AllAttributes}}>{{.ContentHTML}}</main>`,
|
||||
|
||||
|
||||
// Interactive elements
|
||||
"details": `<details {{.AllAttributes}}>{{.ContentHTML}}</details>`,
|
||||
"summary": `<summary {{.AllAttributes}}>{{.Content}}</summary>`,
|
||||
"dialog": `<dialog {{.AllAttributes}}>{{.ContentHTML}}</dialog>`,
|
||||
|
||||
|
||||
// Embedded content
|
||||
"iframe": `<iframe {{.AllAttributes}}>{{.Content}}</iframe>`,
|
||||
"embed": `<embed {{.AllAttributes}} />`,
|
||||
@@ -122,12 +122,12 @@ var fieldTemplates = map[string]string{
|
||||
"picture": `<picture {{.AllAttributes}}>{{.ContentHTML}}</picture>`,
|
||||
"canvas": `<canvas {{.AllAttributes}}>{{.Content}}</canvas>`,
|
||||
"svg": `<svg {{.AllAttributes}}>{{.ContentHTML}}</svg>`,
|
||||
|
||||
|
||||
// Meta elements
|
||||
"br": `<br {{.AllAttributes}} />`,
|
||||
"hr": `<hr {{.AllAttributes}} />`,
|
||||
"wbr": `<wbr {{.AllAttributes}} />`,
|
||||
|
||||
|
||||
// Generic template for any unlisted element
|
||||
"generic": `<{{.Element}} {{.AllAttributes}}>{{.ContentHTML}}</{{.Element}}>`,
|
||||
"void": `<{{.Element}} {{.AllAttributes}} />`,
|
||||
@@ -154,10 +154,10 @@ func init() {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to compile group template: %v", err))
|
||||
}
|
||||
|
||||
|
||||
templateCacheMutex.Lock()
|
||||
defer templateCacheMutex.Unlock()
|
||||
|
||||
|
||||
for element, tmplStr := range fieldTemplates {
|
||||
compiled, err := template.New(element).Parse(tmplStr)
|
||||
if err == nil {
|
||||
@@ -228,13 +228,13 @@ func NewJSONSchemaRenderer(schema *jsonschema.Schema, htmlLayout string) *JSONSc
|
||||
Schema: schema,
|
||||
HTMLLayout: htmlLayout,
|
||||
}
|
||||
|
||||
|
||||
// Pre-compile layout template
|
||||
renderer.compileLayoutTemplate()
|
||||
|
||||
|
||||
// Pre-parse and cache groups and form config
|
||||
renderer.precomputeStaticData()
|
||||
|
||||
|
||||
return renderer
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ func (r *JSONSchemaRenderer) compileLayoutTemplate() {
|
||||
r.formConfig.Class, formAction, r.formConfig.Method, r.formConfig.Enctype))
|
||||
},
|
||||
}).Parse(r.HTMLLayout)
|
||||
|
||||
|
||||
if err == nil {
|
||||
r.compiledLayout = tmpl
|
||||
}
|
||||
@@ -262,7 +262,7 @@ func (r *JSONSchemaRenderer) compileLayoutTemplate() {
|
||||
func (r *JSONSchemaRenderer) precomputeStaticData() {
|
||||
r.cachedGroups = r.parseGroupsFromSchema()
|
||||
r.cachedButtons = r.renderButtons()
|
||||
|
||||
|
||||
// Cache form configuration
|
||||
if r.Schema.Form != nil {
|
||||
if class, ok := r.Schema.Form["class"].(string); ok {
|
||||
@@ -285,7 +285,7 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
|
||||
if len(data) == 0 {
|
||||
return templateStr
|
||||
}
|
||||
|
||||
|
||||
tmpl, err := template.New("interpolate").Parse(templateStr)
|
||||
if err != nil {
|
||||
// Fallback to simple string replacement if template parsing fails
|
||||
@@ -298,13 +298,13 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
var templateResult bytes.Buffer
|
||||
err = tmpl.Execute(&templateResult, data)
|
||||
if err != nil {
|
||||
return templateStr // Return original string if execution fails
|
||||
}
|
||||
|
||||
|
||||
return templateResult.String()
|
||||
}
|
||||
|
||||
@@ -312,22 +312,22 @@ func (r *JSONSchemaRenderer) interpolateTemplate(templateStr string, data map[st
|
||||
func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
|
||||
r.cacheMutex.RLock()
|
||||
defer r.cacheMutex.RUnlock()
|
||||
|
||||
|
||||
// Use a string builder for efficient string concatenation
|
||||
var groupHTML strings.Builder
|
||||
groupHTML.Grow(1024) // Pre-allocate reasonable capacity
|
||||
|
||||
|
||||
for _, group := range r.cachedGroups {
|
||||
groupHTML.WriteString(renderGroupWithData(group, data))
|
||||
}
|
||||
|
||||
|
||||
// Interpolate dynamic form action
|
||||
formAction := r.interpolateTemplate(r.formConfig.Action, data)
|
||||
|
||||
|
||||
// Use pre-compiled template if available
|
||||
if r.compiledLayout != nil {
|
||||
var renderedHTML bytes.Buffer
|
||||
|
||||
|
||||
templateData := struct {
|
||||
GroupsHTML string
|
||||
FormAction string
|
||||
@@ -335,9 +335,15 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
|
||||
GroupsHTML: groupHTML.String(),
|
||||
FormAction: formAction,
|
||||
}
|
||||
|
||||
|
||||
// Update the template functions with current data
|
||||
updatedTemplate := r.compiledLayout.Funcs(template.FuncMap{
|
||||
"error_message": func() string {
|
||||
if msg, ok := data["error_message"].(string); ok {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"form_groups": func() template.HTML {
|
||||
return template.HTML(templateData.GroupsHTML)
|
||||
},
|
||||
@@ -349,14 +355,14 @@ func (r *JSONSchemaRenderer) RenderFields(data map[string]any) (string, error) {
|
||||
r.formConfig.Class, templateData.FormAction, r.formConfig.Method, r.formConfig.Enctype))
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
if err := updatedTemplate.Execute(&renderedHTML, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to execute compiled template: %w", err)
|
||||
}
|
||||
|
||||
|
||||
return renderedHTML.String(), nil
|
||||
}
|
||||
|
||||
|
||||
// Fallback to original method if compilation failed
|
||||
return r.renderFieldsFallback(data)
|
||||
}
|
||||
@@ -367,11 +373,17 @@ func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string,
|
||||
for _, group := range r.cachedGroups {
|
||||
groupHTML.WriteString(renderGroupWithData(group, data))
|
||||
}
|
||||
|
||||
|
||||
formAction := r.interpolateTemplate(r.formConfig.Action, data)
|
||||
|
||||
|
||||
// Create a new template with the layout and functions
|
||||
tmpl, err := template.New("layout").Funcs(template.FuncMap{
|
||||
"error_message": func() string {
|
||||
if msg, ok := data["error_message"].(string); ok {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"form_groups": func() template.HTML {
|
||||
return template.HTML(groupHTML.String())
|
||||
},
|
||||
@@ -386,12 +398,12 @@ func (r *JSONSchemaRenderer) renderFieldsFallback(data map[string]any) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse HTML layout: %w", err)
|
||||
}
|
||||
|
||||
|
||||
var renderedHTML bytes.Buffer
|
||||
if err := tmpl.Execute(&renderedHTML, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to execute HTML template: %w", err)
|
||||
}
|
||||
|
||||
|
||||
return renderedHTML.String(), nil
|
||||
}
|
||||
|
||||
@@ -400,26 +412,26 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
|
||||
if r.Schema.Form == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
groupsData, ok := r.Schema.Form["groups"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
groups, ok := groupsData.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
var result []GroupInfo
|
||||
var groupedFields = make(map[string]bool) // Track fields that are already in groups
|
||||
|
||||
|
||||
for _, group := range groups {
|
||||
groupMap, ok := group.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
var groupTitle GroupTitle
|
||||
if titleMap, ok := groupMap["title"].(map[string]any); ok {
|
||||
if text, ok := titleMap["text"].(string); ok {
|
||||
@@ -429,12 +441,12 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
|
||||
groupTitle.Class = class
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
groupClass, _ := groupMap["class"].(string)
|
||||
if groupClass == "" {
|
||||
groupClass = "form-group-fields"
|
||||
}
|
||||
|
||||
|
||||
var fields []FieldInfo
|
||||
if fieldsData, ok := groupMap["fields"].([]any); ok {
|
||||
for _, fieldName := range fieldsData {
|
||||
@@ -449,7 +461,7 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sort fields by order
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
orderI := 0
|
||||
@@ -462,14 +474,14 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
|
||||
}
|
||||
return orderI < orderJ
|
||||
})
|
||||
|
||||
|
||||
result = append(result, GroupInfo{
|
||||
Title: groupTitle,
|
||||
Fields: fields,
|
||||
GroupClass: groupClass,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Add ungrouped hidden fields to the first group or create a hidden group
|
||||
if r.Schema.Properties != nil {
|
||||
var hiddenFields []FieldInfo
|
||||
@@ -494,7 +506,7 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we have hidden fields, add them to the first group or create a new hidden group
|
||||
if len(hiddenFields) > 0 {
|
||||
if len(result) > 0 {
|
||||
@@ -510,31 +522,31 @@ func (r *JSONSchemaRenderer) parseGroupsFromSchema() []GroupInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// extractFieldsFromPath recursively extracts fields from a path, handling nested properties
|
||||
func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string) []FieldInfo {
|
||||
var fields []FieldInfo
|
||||
|
||||
|
||||
// Build the full path
|
||||
fullPath := fieldPath
|
||||
if parentPath != "" {
|
||||
fullPath = parentPath + "." + fieldPath
|
||||
}
|
||||
|
||||
|
||||
// Navigate to the schema at this path
|
||||
schema := r.getSchemaAtPath(fieldPath)
|
||||
if schema == nil {
|
||||
return fields
|
||||
}
|
||||
|
||||
|
||||
// For nested paths like "company.address.street", we need to check if the final part
|
||||
// is required in its immediate parent's schema
|
||||
var fieldName string
|
||||
var parentSchemaPath string
|
||||
|
||||
|
||||
pathParts := strings.Split(fieldPath, ".")
|
||||
if len(pathParts) > 1 {
|
||||
// Extract the field name (last part) and parent path (all but last)
|
||||
@@ -545,10 +557,10 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
||||
fieldName = fieldPath
|
||||
parentSchemaPath = parentPath
|
||||
}
|
||||
|
||||
|
||||
// Check if this field is required at the parent level
|
||||
isRequired := r.isFieldRequiredAtPath(fieldName, parentSchemaPath)
|
||||
|
||||
|
||||
// If this schema has properties, it's a nested object
|
||||
if schema.Properties != nil && len(*schema.Properties) > 0 {
|
||||
// Recursively process nested properties
|
||||
@@ -562,7 +574,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
||||
if schema.Order != nil {
|
||||
order = *schema.Order
|
||||
}
|
||||
|
||||
|
||||
fields = append(fields, FieldInfo{
|
||||
Name: fieldName,
|
||||
FieldPath: fullPath,
|
||||
@@ -572,23 +584,23 @@ func (r *JSONSchemaRenderer) extractFieldsFromPath(fieldPath, parentPath string)
|
||||
Validation: extractValidationInfo(schema, isRequired),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// extractFieldsFromNestedSchema processes nested schema properties
|
||||
func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath string, propSchema *jsonschema.Schema, _ bool) []FieldInfo {
|
||||
var fields []FieldInfo
|
||||
|
||||
|
||||
fullPath := propName
|
||||
if parentPath != "" {
|
||||
fullPath = parentPath + "." + propName
|
||||
}
|
||||
|
||||
|
||||
// Check if this field is required at its immediate parent level
|
||||
// The parent schema path is the current parentPath, not one level up
|
||||
isRequired := r.isFieldRequiredAtPath(propName, parentPath)
|
||||
|
||||
|
||||
// If this property has nested properties, recurse
|
||||
if propSchema.Properties != nil && len(*propSchema.Properties) > 0 {
|
||||
for nestedPropName, nestedPropSchema := range *propSchema.Properties {
|
||||
@@ -601,7 +613,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
|
||||
if propSchema.Order != nil {
|
||||
order = *propSchema.Order
|
||||
}
|
||||
|
||||
|
||||
fields = append(fields, FieldInfo{
|
||||
Name: propName,
|
||||
FieldPath: fullPath,
|
||||
@@ -611,7 +623,7 @@ func (r *JSONSchemaRenderer) extractFieldsFromNestedSchema(propName, parentPath
|
||||
Validation: extractValidationInfo(propSchema, isRequired),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -620,29 +632,29 @@ func (r *JSONSchemaRenderer) getSchemaAtPath(path string) *jsonschema.Schema {
|
||||
if r.Schema.Properties == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
parts := strings.Split(path, ".")
|
||||
currentSchema := r.Schema
|
||||
|
||||
|
||||
for _, part := range parts {
|
||||
if currentSchema.Properties == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if propSchema, exists := (*currentSchema.Properties)[part]; exists {
|
||||
currentSchema = propSchema
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return currentSchema
|
||||
}
|
||||
|
||||
// isFieldRequiredAtPath checks if a field is required at a specific schema path
|
||||
func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string) bool {
|
||||
var schema *jsonschema.Schema
|
||||
|
||||
|
||||
if schemaPath == "" {
|
||||
// Check at root level
|
||||
schema = r.Schema
|
||||
@@ -650,11 +662,11 @@ func (r *JSONSchemaRenderer) isFieldRequiredAtPath(fieldName, schemaPath string)
|
||||
// Navigate to the schema at the given path
|
||||
schema = r.getSchemaAtPath(schemaPath)
|
||||
}
|
||||
|
||||
|
||||
if schema == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return contains(schema.Required, fieldName)
|
||||
}
|
||||
|
||||
@@ -666,18 +678,18 @@ func renderGroup(group GroupInfo) string {
|
||||
for _, field := range group.Fields {
|
||||
fieldsHTML.WriteString(renderField(field))
|
||||
}
|
||||
|
||||
|
||||
tmpl := template.Must(template.New("group").Parse(groupTemplateStr))
|
||||
data := map[string]any{
|
||||
"Title": group.Title,
|
||||
"GroupClass": group.GroupClass,
|
||||
"FieldsHTML": template.HTML(fieldsHTML.String()),
|
||||
}
|
||||
|
||||
|
||||
if err := tmpl.Execute(&groupHTML, data); err != nil {
|
||||
return "" // Return empty string on error
|
||||
}
|
||||
|
||||
|
||||
return groupHTML.String()
|
||||
}
|
||||
|
||||
@@ -688,20 +700,20 @@ func renderGroupWithData(group GroupInfo, data map[string]any) string {
|
||||
for _, field := range group.Fields {
|
||||
allValidations[field.FieldPath] = field.Validation
|
||||
}
|
||||
|
||||
|
||||
// Use string builder for better performance
|
||||
var fieldsHTML strings.Builder
|
||||
fieldsHTML.Grow(512) // Pre-allocate reasonable capacity
|
||||
|
||||
|
||||
for _, field := range group.Fields {
|
||||
fieldsHTML.WriteString(renderFieldWithContextAndData(field, allValidations, data))
|
||||
}
|
||||
|
||||
|
||||
// Use pre-compiled group template
|
||||
templateCacheMutex.RLock()
|
||||
groupTemplate := compiledGroupTemplate
|
||||
templateCacheMutex.RUnlock()
|
||||
|
||||
|
||||
if groupTemplate != nil {
|
||||
var groupHTML bytes.Buffer
|
||||
templateData := map[string]any{
|
||||
@@ -709,12 +721,12 @@ func renderGroupWithData(group GroupInfo, data map[string]any) string {
|
||||
"GroupClass": group.GroupClass,
|
||||
"FieldsHTML": template.HTML(fieldsHTML.String()),
|
||||
}
|
||||
|
||||
|
||||
if err := groupTemplate.Execute(&groupHTML, templateData); err == nil {
|
||||
return groupHTML.String()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to original method
|
||||
return renderGroup(group)
|
||||
}
|
||||
@@ -735,18 +747,18 @@ func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]Va
|
||||
return ""
|
||||
}
|
||||
allAttributes := buildAttributesWithValidation(field, data)
|
||||
|
||||
|
||||
content := getFieldContent(field)
|
||||
contentHTML := getFieldContentHTML(field)
|
||||
|
||||
|
||||
var labelHTML string
|
||||
if element != "input" || getInputTypeFromSchema(field.Schema) != "hidden" {
|
||||
labelHTML = generateLabel(field)
|
||||
}
|
||||
|
||||
|
||||
optionsHTML := generateOptionsFromSchema(field.Schema)
|
||||
validationJS := generateClientSideValidation(field.FieldPath, field.Validation, allValidations)
|
||||
|
||||
|
||||
templateData := map[string]any{
|
||||
"Element": element,
|
||||
"AllAttributes": template.HTMLAttr(allAttributes),
|
||||
@@ -756,7 +768,7 @@ func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]Va
|
||||
"Class": getFieldWrapperClass(field.Schema),
|
||||
"OptionsHTML": template.HTML(optionsHTML),
|
||||
}
|
||||
|
||||
|
||||
var tmplStr string
|
||||
if element == "input" && getInputTypeFromSchema(field.Schema) == "hidden" {
|
||||
if template, exists := fieldTemplates["input_hidden"]; exists {
|
||||
@@ -771,7 +783,7 @@ func renderFieldWithContextAndData(field FieldInfo, allValidations map[string]Va
|
||||
} else {
|
||||
tmplStr = fieldTemplates["generic"]
|
||||
}
|
||||
|
||||
|
||||
tmpl := template.Must(template.New(element).Parse(tmplStr))
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, templateData); err != nil {
|
||||
@@ -787,15 +799,15 @@ func determineFieldElement(schema *jsonschema.Schema) string {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if shouldUseSelect(schema) {
|
||||
return "select"
|
||||
}
|
||||
|
||||
|
||||
if shouldUseTextarea(schema) {
|
||||
return "textarea"
|
||||
}
|
||||
|
||||
|
||||
var typeStr string
|
||||
if len(schema.Type) > 0 {
|
||||
typeStr = schema.Type[0]
|
||||
@@ -816,7 +828,7 @@ func determineFieldElement(schema *jsonschema.Schema) string {
|
||||
func buildAttributesWithValidation(field FieldInfo, data map[string]any) string {
|
||||
var builder strings.Builder
|
||||
builder.Grow(512) // Pre-allocate capacity
|
||||
|
||||
|
||||
// Check if UI specifies a custom name, otherwise use field path for nested fields
|
||||
var fieldName string
|
||||
if field.Schema.UI != nil {
|
||||
@@ -824,7 +836,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
fieldName = customName
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to field path or field name
|
||||
if fieldName == "" {
|
||||
fieldName = field.FieldPath
|
||||
@@ -832,18 +844,18 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
fieldName = field.Name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add name attribute
|
||||
builder.WriteString(`name="`)
|
||||
builder.WriteString(fieldName)
|
||||
builder.WriteString(`"`)
|
||||
|
||||
|
||||
// Add id attribute for accessibility
|
||||
fieldId := strings.ReplaceAll(fieldName, ".", "_")
|
||||
builder.WriteString(` id="`)
|
||||
builder.WriteString(fieldId)
|
||||
builder.WriteString(`"`)
|
||||
|
||||
|
||||
// Determine and add type attribute for input elements
|
||||
element := determineFieldElement(field.Schema)
|
||||
if element == "input" {
|
||||
@@ -852,14 +864,14 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
builder.WriteString(inputType)
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
|
||||
|
||||
// Add validation attributes
|
||||
validationAttrs := generateValidationAttributes(field.Validation)
|
||||
for _, attr := range validationAttrs {
|
||||
builder.WriteString(` `)
|
||||
builder.WriteString(attr)
|
||||
}
|
||||
|
||||
|
||||
// Add default value with interpolation
|
||||
defaultValue := getDefaultValue(field.Schema)
|
||||
if field.Schema.UI != nil {
|
||||
@@ -877,7 +889,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
builder.WriteString(defaultValue)
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
|
||||
|
||||
// Add placeholder with interpolation
|
||||
placeholder := getPlaceholder(field.Schema)
|
||||
if field.Schema.UI != nil {
|
||||
@@ -894,7 +906,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
builder.WriteString(placeholder)
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
|
||||
|
||||
// Add standard attributes from UI with interpolation
|
||||
if field.Schema.UI != nil {
|
||||
for _, attr := range standardAttrs {
|
||||
@@ -907,13 +919,13 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
if attr == "class" && value == "" {
|
||||
continue // Skip empty class
|
||||
}
|
||||
|
||||
|
||||
// Apply interpolation to string values if data is available
|
||||
valueStr := fmt.Sprintf("%v", value)
|
||||
if data != nil && strings.Contains(valueStr, "{{") {
|
||||
valueStr = interpolateString(valueStr, data)
|
||||
}
|
||||
|
||||
|
||||
builder.WriteString(` `)
|
||||
builder.WriteString(attr)
|
||||
builder.WriteString(`="`)
|
||||
@@ -921,7 +933,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
builder.WriteString(`"`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add data-* and aria-* attributes with interpolation
|
||||
for key, value := range field.Schema.UI {
|
||||
if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") {
|
||||
@@ -929,7 +941,7 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
if data != nil && strings.Contains(valueStr, "{{") {
|
||||
valueStr = interpolateString(valueStr, data)
|
||||
}
|
||||
|
||||
|
||||
builder.WriteString(` `)
|
||||
builder.WriteString(key)
|
||||
builder.WriteString(`="`)
|
||||
@@ -938,14 +950,14 @@ func buildAttributesWithValidation(field FieldInfo, data map[string]any) string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// generateOptionsFromSchema generates option HTML from schema enum or UI options
|
||||
func generateOptionsFromSchema(schema *jsonschema.Schema) string {
|
||||
var optionsHTML strings.Builder
|
||||
|
||||
|
||||
// Check UI options first
|
||||
if schema.UI != nil {
|
||||
if options, ok := schema.UI["options"].([]any); ok {
|
||||
@@ -970,14 +982,14 @@ func generateOptionsFromSchema(schema *jsonschema.Schema) string {
|
||||
return optionsHTML.String()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate options from enum
|
||||
if len(schema.Enum) > 0 {
|
||||
for _, enumValue := range schema.Enum {
|
||||
optionsHTML.WriteString(fmt.Sprintf(`<option value="%v">%v</option>`, enumValue, enumValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return optionsHTML.String()
|
||||
}
|
||||
|
||||
@@ -1006,16 +1018,16 @@ func getPlaceholder(schema *jsonschema.Schema) string {
|
||||
return placeholder
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Auto-generate placeholder from title or description
|
||||
if schema.Title != nil {
|
||||
return fmt.Sprintf("Enter %s", strings.ToLower(*schema.Title))
|
||||
}
|
||||
|
||||
|
||||
if schema.Description != nil {
|
||||
return *schema.Description
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1032,7 +1044,7 @@ func getFieldContent(field FieldInfo) string {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1042,13 +1054,13 @@ func getFieldContentHTML(field FieldInfo) string {
|
||||
if contentHTML, ok := field.Schema.UI["contentHTML"].(string); ok {
|
||||
return contentHTML
|
||||
}
|
||||
|
||||
|
||||
// Check for children elements
|
||||
if children, ok := field.Schema.UI["children"].([]any); ok {
|
||||
return renderChildren(children)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1063,7 +1075,7 @@ func renderChildren(children []any) string {
|
||||
if title, ok := childMap["title"].(string); ok {
|
||||
childSchema.Title = &title
|
||||
}
|
||||
|
||||
|
||||
childField := FieldInfo{
|
||||
Name: getMapValue(childMap, "name", ""),
|
||||
Schema: childSchema,
|
||||
@@ -1081,7 +1093,7 @@ func generateLabel(field FieldInfo) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var title string
|
||||
if field.Schema.Title != nil {
|
||||
title = *field.Schema.Title
|
||||
@@ -1089,18 +1101,18 @@ func generateLabel(field FieldInfo) string {
|
||||
if title == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
fieldName := field.FieldPath
|
||||
if fieldName == "" {
|
||||
fieldName = field.Name
|
||||
}
|
||||
|
||||
|
||||
// Check if field is required
|
||||
requiredSpan := ""
|
||||
if field.IsRequired {
|
||||
requiredSpan = ` <span class="required">*</span>`
|
||||
}
|
||||
|
||||
|
||||
return fmt.Sprintf(`<label for="%s">%s%s</label>`, fieldName, title, requiredSpan)
|
||||
}
|
||||
|
||||
@@ -1125,19 +1137,19 @@ func (r *JSONSchemaRenderer) renderButtons() string {
|
||||
if r.Schema.Form == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
var buttonsHTML bytes.Buffer
|
||||
|
||||
|
||||
if submitConfig, ok := r.Schema.Form["submit"].(map[string]any); ok {
|
||||
buttonHTML := renderButtonFromConfig(submitConfig, "submit")
|
||||
buttonsHTML.WriteString(buttonHTML)
|
||||
}
|
||||
|
||||
|
||||
if resetConfig, ok := r.Schema.Form["reset"].(map[string]any); ok {
|
||||
buttonHTML := renderButtonFromConfig(resetConfig, "reset")
|
||||
buttonsHTML.WriteString(buttonHTML)
|
||||
}
|
||||
|
||||
|
||||
// Support for additional custom buttons
|
||||
if buttons, ok := r.Schema.Form["buttons"].([]any); ok {
|
||||
for _, button := range buttons {
|
||||
@@ -1148,20 +1160,20 @@ func (r *JSONSchemaRenderer) renderButtons() string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return buttonsHTML.String()
|
||||
}
|
||||
|
||||
func renderButtonFromConfig(config map[string]any, defaultType string) string {
|
||||
var attributes []string
|
||||
|
||||
|
||||
buttonType := getMapValue(config, "type", defaultType)
|
||||
attributes = append(attributes, fmt.Sprintf(`type="%s"`, buttonType))
|
||||
|
||||
|
||||
if class := getMapValue(config, "class", ""); class != "" {
|
||||
attributes = append(attributes, fmt.Sprintf(`class="%s"`, class))
|
||||
}
|
||||
|
||||
|
||||
// Add other button attributes
|
||||
for key, value := range config {
|
||||
switch key {
|
||||
@@ -1171,9 +1183,9 @@ func renderButtonFromConfig(config map[string]any, defaultType string) string {
|
||||
attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
content := getMapValue(config, "label", getMapValue(config, "content", "Button"))
|
||||
|
||||
|
||||
return fmt.Sprintf(`<button %s>%s</button>`,
|
||||
strings.Join(attributes, " "), content)
|
||||
}
|
||||
@@ -1237,7 +1249,7 @@ func GetFromBytes(schemaContent []byte, template string, templateFiles ...string
|
||||
</form>
|
||||
`)
|
||||
}
|
||||
|
||||
|
||||
renderer := NewJSONSchemaRenderer(schema, string(htmlLayout))
|
||||
cachedTemplate := &RequestSchemaTemplate{
|
||||
Schema: schema,
|
||||
@@ -1276,7 +1288,7 @@ func GetFromSchema(schema *jsonschema.Schema, template string, templateFiles ...
|
||||
</form>
|
||||
`)
|
||||
}
|
||||
|
||||
|
||||
renderer := NewJSONSchemaRenderer(schema, string(htmlLayout))
|
||||
cachedTemplate := &RequestSchemaTemplate{
|
||||
Schema: schema,
|
||||
@@ -1319,7 +1331,7 @@ func interpolateString(templateStr string, data map[string]any) string {
|
||||
if len(data) == 0 {
|
||||
return templateStr
|
||||
}
|
||||
|
||||
|
||||
// First try Go template interpolation
|
||||
tmpl, err := template.New("interpolate").Parse(templateStr)
|
||||
if err == nil {
|
||||
@@ -1329,7 +1341,7 @@ func interpolateString(templateStr string, data map[string]any) string {
|
||||
return templateResult.String()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to simple string replacement if template parsing/execution fails
|
||||
result := templateStr
|
||||
for key, value := range data {
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/oarkflow/mq"
|
||||
@@ -27,11 +28,15 @@ var defaultKey = "default"
|
||||
|
||||
type Condition struct {
|
||||
dag.Operation
|
||||
conditions map[string]dag.Condition
|
||||
conditions map[string]dag.Condition
|
||||
conditionalResetTo map[string]string
|
||||
}
|
||||
|
||||
func (e *Condition) SetConditions(conditions map[string]dag.Condition) {
|
||||
func (e *Condition) SetConditions(conditions map[string]dag.Condition, conditionalResetTo ...map[string]string) {
|
||||
e.conditions = conditions
|
||||
if len(conditionalResetTo) > 0 {
|
||||
e.conditionalResetTo = conditionalResetTo[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
@@ -51,6 +56,19 @@ func (e *Condition) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||
if conditionStatus == "" && ok {
|
||||
conditionStatus = defaultKey
|
||||
}
|
||||
if conditionStatus != "" {
|
||||
if resetTo, ok := e.conditionalResetTo[conditionStatus]; ok && resetTo != "" {
|
||||
errorMessage, _ := e.Payload.Data["error_message"].(string)
|
||||
if errorMessage != "" {
|
||||
data["error_message"] = errorMessage
|
||||
} else {
|
||||
data["error_message"] = "Please try again."
|
||||
}
|
||||
bt, _ := json.Marshal(data)
|
||||
return mq.Result{Payload: bt, ConditionStatus: conditionStatus, Ctx: ctx, ResetTo: resetTo}
|
||||
}
|
||||
}
|
||||
|
||||
return mq.Result{Payload: task.Payload, ConditionStatus: conditionStatus, Ctx: ctx}
|
||||
}
|
||||
|
||||
|
||||
29
services/examples/config/policies/apis/sample.json
Normal file
29
services/examples/config/policies/apis/sample.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
86
services/examples/config/policies/handlers/login.json
Normal file
86
services/examples/config/policies/handlers/login.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"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": {
|
||||
"error_message": "Invalid login credentials."
|
||||
},
|
||||
"conditions": {
|
||||
"default": {
|
||||
"id": "condition:default",
|
||||
"node": "output"
|
||||
},
|
||||
"invalid": {
|
||||
"id": "condition:invalid_login",
|
||||
"node": "error-page",
|
||||
"reset_to": "back",
|
||||
"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" ]
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
11
services/examples/config/policies/handlers/print-check.json
Normal file
11
services/examples/config/policies/handlers/print-check.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Sample Print",
|
||||
"key": "print:check",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "print1",
|
||||
"node": "print",
|
||||
"first_node": true
|
||||
}
|
||||
]
|
||||
}
|
||||
46
services/examples/config/policies/handlers/send-email.json
Normal file
46
services/examples/config/policies/handlers/send-email.json
Normal file
@@ -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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
63
services/examples/config/policies/schemas/login.json
Normal file
63
services/examples/config/policies/schemas/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": "{{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"
|
||||
}
|
||||
}
|
||||
}
|
||||
105
services/examples/config/policies/schemas/schema.json
Normal file
105
services/examples/config/policies/schemas/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": "{{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"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
services/examples/config/policies/schemas/test-route.json
Normal file
18
services/examples/config/policies/schemas/test-route.json
Normal file
@@ -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/config/policies/web.json
Normal file
16
services/examples/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/main.go
Normal file
36
services/examples/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()
|
||||
services.Setup(loader, serverApp, brokerAddr)
|
||||
cli.Run("mq", "0.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) })
|
||||
}
|
||||
47
services/examples/templates/basic.html
Normal file
47
services/examples/templates/basic.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!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">
|
||||
{{if error_message}}
|
||||
<div class="status-message status-error">
|
||||
<strong>❌ Request Failed:</strong> {{error_message}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{form_groups}}
|
||||
<div class="mt-4 flex gap-2">
|
||||
{{form_buttons}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
134
services/examples/templates/error.html
Normal file
134
services/examples/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=
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/basicauth"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"github.com/oarkflow/mq/dag"
|
||||
"github.com/oarkflow/mq/services/http/responses"
|
||||
"github.com/oarkflow/mq/services/middlewares"
|
||||
"github.com/oarkflow/mq/services/web"
|
||||
"github.com/oarkflow/mq/services/utils"
|
||||
"github.com/oarkflow/mq/services/web"
|
||||
"github.com/oarkflow/protocol/utils/str"
|
||||
)
|
||||
|
||||
@@ -48,14 +48,14 @@ func SetupEnhanced(loader *Loader, serverApp *fiber.App, brokerAddr string, conf
|
||||
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)
|
||||
}
|
||||
@@ -67,10 +67,10 @@ func InitializeEnhancedServices(config *EnhancedServiceConfig) error {
|
||||
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)
|
||||
@@ -79,7 +79,7 @@ func InitializeEnhancedServices(config *EnhancedServiceConfig) error {
|
||||
}
|
||||
EnhancedValidationInstance = validation
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,13 +88,13 @@ func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string
|
||||
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)
|
||||
@@ -102,11 +102,11 @@ func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string
|
||||
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 != "" {
|
||||
@@ -121,12 +121,12 @@ func SetupEnhancedServices(prefix string, router fiber.Router, brokerAddr string
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...b
|
||||
DisableLog: handler.DisableLog,
|
||||
Debug: handler.Debug,
|
||||
}
|
||||
|
||||
|
||||
// Convert enhanced nodes to traditional nodes
|
||||
for _, enhancedNode := range handler.Nodes {
|
||||
traditionalNode := Node{
|
||||
@@ -153,10 +153,10 @@ func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...b
|
||||
}
|
||||
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{
|
||||
@@ -166,13 +166,13 @@ func SetupEnhancedHandler(handler EnhancedHandler, brokerAddr string, async ...b
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ type FilterGroup struct {
|
||||
type Filter struct {
|
||||
Filter *filters.Filter `json:"condition"`
|
||||
FilterGroup *FilterGroup `json:"group"`
|
||||
ResetTo string `json:"reset_to" yaml:"reset_to"`
|
||||
Node string `json:"node"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
@@ -280,8 +281,12 @@ func prepareNode(flow *dag.DAG, node Node) error {
|
||||
Providers: providers,
|
||||
})
|
||||
condition := make(map[string]string)
|
||||
conditionalResetTo := make(map[string]string)
|
||||
conditions := make(map[string]dag.Condition)
|
||||
for key, cond := range node.Data.Conditions {
|
||||
if cond.ResetTo != "" {
|
||||
conditionalResetTo[key] = cond.ResetTo
|
||||
}
|
||||
condition[key] = cond.Node
|
||||
if cond.Filter != nil {
|
||||
conditions[key] = cond.Filter
|
||||
@@ -306,7 +311,7 @@ func prepareNode(flow *dag.DAG, node Node) error {
|
||||
}
|
||||
}
|
||||
flow.AddCondition(node.ID, condition)
|
||||
nodeHandler.SetConditions(conditions)
|
||||
nodeHandler.SetConditions(conditions, conditionalResetTo)
|
||||
case dag.Processor:
|
||||
nodeHandler.SetConfig(dag.Payload{
|
||||
Mapping: node.Data.Mapping,
|
||||
@@ -429,7 +434,7 @@ func SetupAPI(prefix string, router fiber.Router, brokerAddr string) error {
|
||||
if flow.HasPageNode() {
|
||||
mw = append(mw, flow.RenderFiber)
|
||||
routeGroup.All(route.Uri, mw...)
|
||||
|
||||
|
||||
} else {
|
||||
mw = append(mw, customHandler(flow))
|
||||
routeGroup.Add(strings.ToUpper(route.Method), route.Uri, mw...)
|
||||
@@ -563,7 +568,7 @@ func customHandler(flow *dag.DAG) fiber.Handler {
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
// Step 1: always parse query params
|
||||
utils.ParseQueryParams(ctx)
|
||||
|
||||
|
||||
// Step 2: build user context
|
||||
userCtx := ctx.UserContext()
|
||||
contentType := ctx.Get("Content-Type")
|
||||
@@ -571,13 +576,13 @@ func customHandler(flow *dag.DAG) fiber.Handler {
|
||||
// attach Fiber ctx so downstream can access files later
|
||||
userCtx = context.WithValue(userCtx, "fiberCtx", ctx)
|
||||
}
|
||||
|
||||
|
||||
// Step 3: run DAG flow with the enriched context
|
||||
result := flow.Process(userCtx, ctx.BodyRaw())
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
|
||||
// Step 4: handle response content type
|
||||
contentType = ""
|
||||
if ct := result.Ctx.Value(consts.ContentType); ct != nil {
|
||||
@@ -585,18 +590,18 @@ func customHandler(flow *dag.DAG) fiber.Handler {
|
||||
contentType = s
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if contentType == "" ||
|
||||
contentType == fiber.MIMEApplicationJSON ||
|
||||
contentType == fiber.MIMEApplicationJSONCharsetUTF8 {
|
||||
return responses.Success(ctx, 200, result.Payload)
|
||||
}
|
||||
|
||||
|
||||
var resultData map[string]any
|
||||
if err := json.Unmarshal(result.Payload, &resultData); err != nil {
|
||||
return ctx.JSON(fiber.Map{"success": false, "error": "Invalid response payload"})
|
||||
}
|
||||
|
||||
|
||||
ctx.Set(consts.ContentType, contentType)
|
||||
html, _ := resultData["html_content"].(string)
|
||||
return ctx.SendString(html)
|
||||
@@ -722,7 +727,7 @@ func setupMiddlewares(middlewares ...Middleware) (mid []any) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
||||
expiration, err := utils.ParseDuration(options.Expiration)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -878,7 +883,7 @@ func setupEnhancedBackgroundHandlers(brokerAddress string) {
|
||||
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()
|
||||
@@ -896,7 +901,7 @@ func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) err
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
api := router.Group(prefix)
|
||||
|
||||
|
||||
// Setup traditional API routes
|
||||
for _, configRoute := range userConfig.Policy.Web.Apis {
|
||||
routeGroup := api.Group(configRoute.Prefix)
|
||||
@@ -930,7 +935,7 @@ func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Setup enhanced API routes for enhanced handlers
|
||||
for _, handler := range userConfig.Policy.EnhancedHandlers {
|
||||
if handler.WorkflowEnabled {
|
||||
@@ -938,18 +943,18 @@ func SetupEnhancedAPI(prefix string, router fiber.Router, brokerAddr string) err
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user