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 }