init: rapid_dashboards app from fn_registry
This commit is contained in:
+156
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"fn-registry/functions/infra"
|
||||
)
|
||||
|
||||
var namedParamRe = regexp.MustCompile(`:([a-zA-Z_][a-zA-Z0-9_]*)`)
|
||||
|
||||
// QueryEngine executes queries with parameter resolution.
|
||||
type QueryEngine struct {
|
||||
config *DashboardConfig
|
||||
pool map[string]*sql.DB
|
||||
}
|
||||
|
||||
func NewQueryEngine(cfg *DashboardConfig, pool map[string]*sql.DB) *QueryEngine {
|
||||
return &QueryEngine{config: cfg, pool: pool}
|
||||
}
|
||||
|
||||
// Execute runs the query for a widget, resolving filter references and named params.
|
||||
func (qe *QueryEngine) Execute(widgetID string, filters map[string]any) ([]map[string]any, error) {
|
||||
// Find the widget and its query.
|
||||
widget, err := qe.findWidget(widgetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qdef, ok := qe.config.Queries[widget.Query]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("query %q not found", widget.Query)
|
||||
}
|
||||
|
||||
db, ok := qe.pool[qdef.Connection]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("connection %q not found", qdef.Connection)
|
||||
}
|
||||
|
||||
// Resolve parameter values: static or $filter.xxx references.
|
||||
resolvedParams := qe.resolveParams(qdef.Params, filters)
|
||||
|
||||
// Convert :name placeholders to driver-appropriate positional params.
|
||||
driver := qe.config.Connections[qdef.Connection].Driver
|
||||
query, args := convertNamedParams(qdef.SQL, resolvedParams, driver)
|
||||
|
||||
return infra.DBQuery(db, query, args...)
|
||||
}
|
||||
|
||||
func (qe *QueryEngine) findWidget(id string) (*WidgetDef, error) {
|
||||
for _, sec := range qe.config.Sections {
|
||||
for i := range sec.Widgets {
|
||||
if sec.Widgets[i].ID == id {
|
||||
return &sec.Widgets[i], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("widget %q not found", id)
|
||||
}
|
||||
|
||||
// resolveParams replaces $filter.xxx references with actual filter values
|
||||
// and resolves relative dates.
|
||||
func (qe *QueryEngine) resolveParams(params map[string]string, filters map[string]any) map[string]string {
|
||||
resolved := make(map[string]string, len(params))
|
||||
|
||||
for name, ref := range params {
|
||||
val := ref
|
||||
|
||||
// $filter.date_range.from → filters["date_range"]["from"]
|
||||
if strings.HasPrefix(ref, "$filter.") {
|
||||
val = resolveFilterRef(ref, filters)
|
||||
}
|
||||
|
||||
// Resolve relative dates (now-7d, etc.)
|
||||
if resolved, ok := ResolveRelativeDate(val); ok {
|
||||
val = resolved
|
||||
}
|
||||
|
||||
resolved[name] = val
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
// resolveFilterRef extracts a value from the filters map using dot notation.
|
||||
// "$filter.date_range.from" → filters["date_range"] → map["from"]
|
||||
func resolveFilterRef(ref string, filters map[string]any) string {
|
||||
// Strip "$filter." prefix.
|
||||
path := strings.TrimPrefix(ref, "$filter.")
|
||||
parts := strings.SplitN(path, ".", 2)
|
||||
|
||||
v, ok := filters[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Simple value (e.g. $filter.category).
|
||||
if len(parts) == 1 {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
// Nested value (e.g. $filter.date_range.from).
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
if val, ok := m[parts[1]]; ok {
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// convertNamedParams replaces :name placeholders with positional params ($1, ?, etc.)
|
||||
// and builds the args slice in the correct order.
|
||||
func convertNamedParams(query string, params map[string]string, driver string) (string, []any) {
|
||||
matches := namedParamRe.FindAllStringSubmatch(query, -1)
|
||||
if len(matches) == 0 {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// Deduplicate and order params as they appear.
|
||||
seen := make(map[string]int)
|
||||
var ordered []string
|
||||
for _, m := range matches {
|
||||
name := m[1]
|
||||
if _, ok := seen[name]; !ok {
|
||||
seen[name] = len(ordered)
|
||||
ordered = append(ordered, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Build args slice.
|
||||
args := make([]any, len(ordered))
|
||||
for i, name := range ordered {
|
||||
args[i] = params[name]
|
||||
}
|
||||
|
||||
// Replace :name with positional placeholder.
|
||||
result := namedParamRe.ReplaceAllStringFunc(query, func(match string) string {
|
||||
name := match[1:] // strip leading ':'
|
||||
idx := seen[name]
|
||||
return placeholder(driver, idx)
|
||||
})
|
||||
|
||||
return result, args
|
||||
}
|
||||
|
||||
func placeholder(driver string, idx int) string {
|
||||
switch driver {
|
||||
case "postgres":
|
||||
return fmt.Sprintf("$%d", idx+1)
|
||||
default:
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user