2024-09-29 19:23:19 +03:00
|
|
|
package picker
|
2023-04-25 17:58:51 +03:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/pocketbase/pocketbase/tools/search"
|
2023-09-18 15:16:06 +03:00
|
|
|
"github.com/pocketbase/pocketbase/tools/tokenizer"
|
2023-04-25 17:58:51 +03:00
|
|
|
)
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
// Pick converts data into a []any, map[string]any, etc. (using json marshal->unmarshal)
|
|
|
|
// containing only the fields from the parsed rawFields expression.
|
2023-10-23 22:46:47 +03:00
|
|
|
//
|
2024-09-29 19:23:19 +03:00
|
|
|
// rawFields is a comma separated string of the fields to include.
|
|
|
|
// Nested fields should be listed with dot-notation.
|
|
|
|
// Fields value modifiers are also supported using the `:modifier(args)` format (see Modifiers).
|
2023-10-23 22:46:47 +03:00
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
//
|
2024-01-20 15:03:45 +02:00
|
|
|
// data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}}
|
2024-09-29 19:23:19 +03:00
|
|
|
// Pick(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}}
|
|
|
|
func Pick(data any, rawFields string) (any, error) {
|
2023-10-23 22:46:47 +03:00
|
|
|
parsedFields, err := parseFields(rawFields)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-09-18 22:52:36 +03:00
|
|
|
// marshalize the provided data to ensure that the related json.Marshaler
|
|
|
|
// implementations are invoked, and then convert it back to a plain
|
|
|
|
// json value that we can further operate on.
|
|
|
|
//
|
|
|
|
// @todo research other approaches to avoid the double serialization
|
|
|
|
// ---
|
2024-09-29 19:23:19 +03:00
|
|
|
encoded, err := json.Marshal(data)
|
2023-04-25 17:58:51 +03:00
|
|
|
if err != nil {
|
2023-10-23 22:46:47 +03:00
|
|
|
return nil, err
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var decoded any
|
2024-09-29 19:23:19 +03:00
|
|
|
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
2023-10-23 22:46:47 +03:00
|
|
|
return nil, err
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
2023-09-18 22:52:36 +03:00
|
|
|
// ---
|
2023-04-25 17:58:51 +03:00
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
// special cases to preserve the same fields format when used with single item or search results data.
|
2023-04-25 17:58:51 +03:00
|
|
|
var isSearchResult bool
|
2023-10-23 22:46:47 +03:00
|
|
|
switch data.(type) {
|
2023-04-25 17:58:51 +03:00
|
|
|
case search.Result, *search.Result:
|
|
|
|
isSearchResult = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if isSearchResult {
|
|
|
|
if decodedMap, ok := decoded.(map[string]any); ok {
|
2023-10-23 22:46:47 +03:00
|
|
|
pickParsedFields(decodedMap["items"], parsedFields)
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
} else {
|
2023-10-23 22:46:47 +03:00
|
|
|
pickParsedFields(decoded, parsedFields)
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
2023-10-23 22:46:47 +03:00
|
|
|
return decoded, nil
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
func parseFields(rawFields string) (map[string]Modifier, error) {
|
2023-09-18 15:16:06 +03:00
|
|
|
t := tokenizer.NewFromString(rawFields)
|
|
|
|
|
|
|
|
fields, err := t.ScanAll()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
result := make(map[string]Modifier, len(fields))
|
2023-09-18 15:16:06 +03:00
|
|
|
|
|
|
|
for _, f := range fields {
|
|
|
|
parts := strings.SplitN(strings.TrimSpace(f), ":", 2)
|
|
|
|
|
|
|
|
if len(parts) > 1 {
|
|
|
|
m, err := initModifer(parts[1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[parts[0]] = m
|
|
|
|
} else {
|
|
|
|
result[parts[0]] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
func pickParsedFields(data any, fields map[string]Modifier) error {
|
2023-04-25 17:58:51 +03:00
|
|
|
switch v := data.(type) {
|
|
|
|
case map[string]any:
|
|
|
|
pickMapFields(v, fields)
|
|
|
|
case []map[string]any:
|
|
|
|
for _, item := range v {
|
2023-09-18 15:16:06 +03:00
|
|
|
if err := pickMapFields(item, fields); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
case []any:
|
|
|
|
if len(v) == 0 {
|
2023-09-18 15:16:06 +03:00
|
|
|
return nil // nothing to pick
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := v[0].(map[string]any); !ok {
|
2023-09-18 15:16:06 +03:00
|
|
|
return nil // for now ignore non-map values
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, item := range v {
|
2023-09-18 15:16:06 +03:00
|
|
|
if err := pickMapFields(item.(map[string]any), fields); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
}
|
2023-09-18 15:16:06 +03:00
|
|
|
|
|
|
|
return nil
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
func pickMapFields(data map[string]any, fields map[string]Modifier) error {
|
2023-04-25 17:58:51 +03:00
|
|
|
if len(fields) == 0 {
|
2023-09-18 15:16:06 +03:00
|
|
|
return nil // nothing to pick
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
2023-09-18 15:16:06 +03:00
|
|
|
if m, ok := fields["*"]; ok {
|
2023-08-23 20:50:36 +03:00
|
|
|
// append all missing root level data keys
|
|
|
|
for k := range data {
|
|
|
|
var exists bool
|
|
|
|
|
2023-09-18 15:16:06 +03:00
|
|
|
for f := range fields {
|
2023-08-23 20:50:36 +03:00
|
|
|
if strings.HasPrefix(f+".", k+".") {
|
|
|
|
exists = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !exists {
|
2023-09-18 15:16:06 +03:00
|
|
|
fields[k] = m
|
2023-08-23 20:50:36 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-25 17:58:51 +03:00
|
|
|
DataLoop:
|
|
|
|
for k := range data {
|
2024-09-29 19:23:19 +03:00
|
|
|
matchingFields := make(map[string]Modifier, len(fields))
|
2023-09-18 15:16:06 +03:00
|
|
|
for f, m := range fields {
|
2023-04-25 17:58:51 +03:00
|
|
|
if strings.HasPrefix(f+".", k+".") {
|
2023-09-18 15:16:06 +03:00
|
|
|
matchingFields[f] = m
|
2023-04-25 17:58:51 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(matchingFields) == 0 {
|
|
|
|
delete(data, k)
|
|
|
|
continue DataLoop
|
|
|
|
}
|
|
|
|
|
2023-09-18 15:16:06 +03:00
|
|
|
// remove the current key from the matching fields path
|
|
|
|
for f, m := range matchingFields {
|
|
|
|
remains := strings.TrimSuffix(strings.TrimPrefix(f+".", k+"."), ".")
|
|
|
|
|
|
|
|
// final key
|
|
|
|
if remains == "" {
|
|
|
|
if m != nil {
|
|
|
|
var err error
|
|
|
|
data[k], err = m.Modify(data[k])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2023-04-25 17:58:51 +03:00
|
|
|
continue DataLoop
|
|
|
|
}
|
2023-09-18 15:16:06 +03:00
|
|
|
|
|
|
|
// cleanup the old field key and continue with the rest of the field path
|
|
|
|
delete(matchingFields, f)
|
|
|
|
matchingFields[remains] = m
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
|
|
|
|
2023-10-23 22:46:47 +03:00
|
|
|
if err := pickParsedFields(data[k], matchingFields); err != nil {
|
2023-09-18 15:16:06 +03:00
|
|
|
return err
|
|
|
|
}
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|
2023-09-18 15:16:06 +03:00
|
|
|
|
|
|
|
return nil
|
2023-04-25 17:58:51 +03:00
|
|
|
}
|