185 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			185 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Go
		
	
	
	
package picker
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/pocketbase/pocketbase/tools/search"
 | 
						|
	"github.com/pocketbase/pocketbase/tools/tokenizer"
 | 
						|
)
 | 
						|
 | 
						|
// Pick converts data into a []any, map[string]any, etc. (using json marshal->unmarshal)
 | 
						|
// containing only the fields from the parsed rawFields expression.
 | 
						|
//
 | 
						|
// 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).
 | 
						|
//
 | 
						|
// Example:
 | 
						|
//
 | 
						|
//	data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}}
 | 
						|
//	Pick(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}}
 | 
						|
func Pick(data any, rawFields string) (any, error) {
 | 
						|
	parsedFields, err := parseFields(rawFields)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// 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
 | 
						|
	// ---
 | 
						|
	encoded, err := json.Marshal(data)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	var decoded any
 | 
						|
	if err := json.Unmarshal(encoded, &decoded); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	// ---
 | 
						|
 | 
						|
	// special cases to preserve the same fields format when used with single item or search results data.
 | 
						|
	var isSearchResult bool
 | 
						|
	switch data.(type) {
 | 
						|
	case search.Result, *search.Result:
 | 
						|
		isSearchResult = true
 | 
						|
	}
 | 
						|
 | 
						|
	if isSearchResult {
 | 
						|
		if decodedMap, ok := decoded.(map[string]any); ok {
 | 
						|
			pickParsedFields(decodedMap["items"], parsedFields)
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		pickParsedFields(decoded, parsedFields)
 | 
						|
	}
 | 
						|
 | 
						|
	return decoded, nil
 | 
						|
}
 | 
						|
 | 
						|
func parseFields(rawFields string) (map[string]Modifier, error) {
 | 
						|
	t := tokenizer.NewFromString(rawFields)
 | 
						|
 | 
						|
	fields, err := t.ScanAll()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	result := make(map[string]Modifier, len(fields))
 | 
						|
 | 
						|
	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
 | 
						|
}
 | 
						|
 | 
						|
func pickParsedFields(data any, fields map[string]Modifier) error {
 | 
						|
	switch v := data.(type) {
 | 
						|
	case map[string]any:
 | 
						|
		pickMapFields(v, fields)
 | 
						|
	case []map[string]any:
 | 
						|
		for _, item := range v {
 | 
						|
			if err := pickMapFields(item, fields); err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
	case []any:
 | 
						|
		if len(v) == 0 {
 | 
						|
			return nil // nothing to pick
 | 
						|
		}
 | 
						|
 | 
						|
		if _, ok := v[0].(map[string]any); !ok {
 | 
						|
			return nil // for now ignore non-map values
 | 
						|
		}
 | 
						|
 | 
						|
		for _, item := range v {
 | 
						|
			if err := pickMapFields(item.(map[string]any), fields); err != nil {
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func pickMapFields(data map[string]any, fields map[string]Modifier) error {
 | 
						|
	if len(fields) == 0 {
 | 
						|
		return nil // nothing to pick
 | 
						|
	}
 | 
						|
 | 
						|
	if m, ok := fields["*"]; ok {
 | 
						|
		// append all missing root level data keys
 | 
						|
		for k := range data {
 | 
						|
			var exists bool
 | 
						|
 | 
						|
			for f := range fields {
 | 
						|
				if strings.HasPrefix(f+".", k+".") {
 | 
						|
					exists = true
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			if !exists {
 | 
						|
				fields[k] = m
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
DataLoop:
 | 
						|
	for k := range data {
 | 
						|
		matchingFields := make(map[string]Modifier, len(fields))
 | 
						|
		for f, m := range fields {
 | 
						|
			if strings.HasPrefix(f+".", k+".") {
 | 
						|
				matchingFields[f] = m
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if len(matchingFields) == 0 {
 | 
						|
			delete(data, k)
 | 
						|
			continue DataLoop
 | 
						|
		}
 | 
						|
 | 
						|
		// 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
 | 
						|
					}
 | 
						|
				}
 | 
						|
				continue DataLoop
 | 
						|
			}
 | 
						|
 | 
						|
			// cleanup the old field key and continue with the rest of the field path
 | 
						|
			delete(matchingFields, f)
 | 
						|
			matchingFields[remains] = m
 | 
						|
		}
 | 
						|
 | 
						|
		if err := pickParsedFields(data[k], matchingFields); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 |