Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1760,9 +1760,29 @@ triggers:

Both approaches work with `wfctl template validate --config` for validation.

## Visual Workflow Builder (UI)
## Engine Validation Config

Control the engine's startup validation behaviour via the `engine.validation` block:

```yaml
engine:
validation:
templateRefs: warn # "off" | "warn" | "error" (default: "warn")
```

| Value | Behaviour |
|-------|-----------|
| `warn` | (default) Log warnings for suspicious pipeline template references at startup. Engine starts normally. |
| `error` | Refuse to start if any pipeline template reference issues are detected (e.g. dangling step refs, unknown output fields). |
| `off` | Skip template reference validation entirely. Preserves the previous engine behavior. |

A React-based visual editor for composing workflow configurations (`ui/` directory).
The validation checks performed at startup match those run by `wfctl template validate`, including:
- Step name references (`{{ .steps.NAME.field }}`) against declared steps in the same pipeline
- Forward references (referencing a step that appears later in the pipeline)
- Output field validation against each step type's declared output schema
- SQL column validation for `step.db_query` steps with a static `query`

## Visual Workflow Builder (UI)

**Technology stack:** React, ReactFlow, Zustand, TypeScript, Vite

Expand Down
77 changes: 2 additions & 75 deletions cmd/wfctl/api_extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/GoCodeAlone/workflow/config"
"github.com/GoCodeAlone/workflow/module"
"github.com/GoCodeAlone/workflow/validation"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -502,7 +503,7 @@ func inferBodyFromSchema(bodyFrom string, steps []map[string]any) *module.OpenAP
if query == "" {
break
}
columns := extractSQLColumns(query)
columns := validation.ExtractSQLColumns(query)
if len(columns) == 0 {
break
}
Expand Down Expand Up @@ -530,80 +531,6 @@ func inferBodyFromSchema(bodyFrom string, steps []map[string]any) *module.OpenAP
return nil
}

// extractSQLColumns parses a SQL SELECT statement and returns the column names
// (or aliases) from the SELECT clause.
func extractSQLColumns(query string) []string {
// Normalize whitespace
query = strings.Join(strings.Fields(query), " ")

// Find SELECT ... FROM
upper := strings.ToUpper(query)
selectIdx := strings.Index(upper, "SELECT ")
fromIdx := strings.Index(upper, " FROM ")
if selectIdx < 0 || fromIdx < 0 || fromIdx <= selectIdx {
return nil
}

selectClause := query[selectIdx+7 : fromIdx]

// Handle DISTINCT
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(selectClause)), "DISTINCT ") {
selectClause = strings.TrimSpace(selectClause)[9:]
}

// Split by comma, handling parenthesized subexpressions
var columns []string
depth := 0
current := ""
for _, ch := range selectClause {
switch ch {
case '(':
depth++
current += string(ch)
case ')':
depth--
current += string(ch)
case ',':
if depth == 0 {
if col := extractColumnName(strings.TrimSpace(current)); col != "" {
columns = append(columns, col)
}
current = ""
} else {
current += string(ch)
}
default:
current += string(ch)
}
}
if col := extractColumnName(strings.TrimSpace(current)); col != "" {
columns = append(columns, col)
}
return columns
}

// extractColumnName extracts the effective column name from a SELECT expression.
// Handles: "col", "table.col", "expr AS alias", "COALESCE(...) AS alias".
func extractColumnName(expr string) string {
if expr == "" || expr == "*" {
return ""
}
// Check for AS alias (case-insensitive)
upper := strings.ToUpper(expr)
if asIdx := strings.LastIndex(upper, " AS "); asIdx >= 0 {
alias := strings.TrimSpace(expr[asIdx+4:])
// Remove quotes if present
alias = strings.Trim(alias, "\"'`")
return alias
}
// Check for table.column
if dotIdx := strings.LastIndex(expr, "."); dotIdx >= 0 {
return strings.TrimSpace(expr[dotIdx+1:])
}
// Simple column name
return strings.TrimSpace(expr)
}

// userCredentialsSchema returns a schema for email+password request bodies.
func userCredentialsSchema() *module.OpenAPISchema {
return &module.OpenAPISchema{
Expand Down
Loading
Loading