Local queries (#641)
* 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
This commit is contained in:
148
pkg/queries/metadata.go
Normal file
148
pkg/queries/metadata.go
Normal file
@@ -0,0 +1,148 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user