41bf189e6b
* Read local queries from pgweb home directory * Refactor local query functionality * Allow picking local query in the query tab * WIP * Disable local query dropdown during execution * Only allow local queries running in a single session mode * Add middleware to enforce local query endpoint availability * Fix query check * Add query store tests * Make query store errors portable * Skip building specific tests on windows
149 lines
3.3 KiB
Go
149 lines
3.3 KiB
Go
package queries
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
reMetaPrefix = regexp.MustCompile(`(?m)^\s*--\s*pgweb:\s*(.+)`)
|
|
reMetaContent = regexp.MustCompile(`([\w]+)\s*=\s*"([^"]+)"`)
|
|
reMatchAll = regexp.MustCompile(`^(.+)$`)
|
|
reExpression = regexp.MustCompile(`[\[\]\(\)\+\*]+`)
|
|
|
|
allowedKeys = []string{"title", "description", "host", "user", "database", "mode", "timeout"}
|
|
allowedModes = map[string]bool{"readonly": true, "*": true}
|
|
)
|
|
|
|
type Metadata struct {
|
|
Title string
|
|
Description string
|
|
Host field
|
|
User field
|
|
Database field
|
|
Mode field
|
|
Timeout *time.Duration
|
|
}
|
|
|
|
func parseMetadata(input string) (*Metadata, error) {
|
|
fields, err := parseFields(input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fields == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Host must be set to limit queries availability
|
|
if fields["host"] == "" {
|
|
return nil, fmt.Errorf("host field must be set")
|
|
}
|
|
|
|
// Allow matching for any user, database and mode by default
|
|
if fields["user"] == "" {
|
|
fields["user"] = "*"
|
|
}
|
|
if fields["database"] == "" {
|
|
fields["database"] = "*"
|
|
}
|
|
if fields["mode"] == "" {
|
|
fields["mode"] = "*"
|
|
}
|
|
|
|
hostField, err := newField(fields["host"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`error initializing "host" field: %w`, err)
|
|
}
|
|
|
|
userField, err := newField(fields["user"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`error initializing "user" field: %w`, err)
|
|
}
|
|
|
|
dbField, err := newField(fields["database"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`error initializing "database" field: %w`, err)
|
|
}
|
|
|
|
if !allowedModes[fields["mode"]] {
|
|
return nil, fmt.Errorf(`invalid "mode" field value: %q`, fields["mode"])
|
|
}
|
|
modeField, err := newField(fields["mode"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`error initializing "mode" field: %w`, err)
|
|
}
|
|
|
|
var timeout *time.Duration
|
|
if fields["timeout"] != "" {
|
|
timeoutSec, err := strconv.Atoi(fields["timeout"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf(`error initializing "timeout" field: %w`, err)
|
|
}
|
|
timeoutVal := time.Duration(timeoutSec) * time.Second
|
|
timeout = &timeoutVal
|
|
}
|
|
|
|
return &Metadata{
|
|
Title: fields["title"],
|
|
Description: fields["description"],
|
|
Host: hostField,
|
|
User: userField,
|
|
Database: dbField,
|
|
Mode: modeField,
|
|
Timeout: timeout,
|
|
}, nil
|
|
}
|
|
|
|
func parseFields(input string) (map[string]string, error) {
|
|
result := map[string]string{}
|
|
seenKeys := map[string]bool{}
|
|
|
|
allowed := map[string]bool{}
|
|
for _, key := range allowedKeys {
|
|
allowed[key] = true
|
|
}
|
|
|
|
matches := reMetaPrefix.FindAllStringSubmatch(input, -1)
|
|
if len(matches) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
for _, match := range matches {
|
|
content := reMetaContent.FindAllStringSubmatch(match[1], -1)
|
|
if len(content) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, field := range content {
|
|
key := field[1]
|
|
value := field[2]
|
|
|
|
if !allowed[key] {
|
|
return result, fmt.Errorf("unknown key: %q", key)
|
|
}
|
|
if seenKeys[key] {
|
|
return result, fmt.Errorf("duplicate key: %q", key)
|
|
}
|
|
|
|
seenKeys[key] = true
|
|
result[key] = value
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func sanitizeMetadata(input string) string {
|
|
lines := []string{}
|
|
for _, line := range strings.Split(input, "\n") {
|
|
line = reMetaPrefix.ReplaceAllString(line, "")
|
|
if len(line) > 0 {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|