mirror of
https://github.com/pocketbase/pocketbase.git
synced 2024-11-24 17:07:00 +02:00
243 lines
5.3 KiB
Go
243 lines
5.3 KiB
Go
package rest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
// experimental! (need more tests before replacing encoding/json entirely)
|
|
goccy "github.com/goccy/go-json"
|
|
|
|
"github.com/labstack/echo/v5"
|
|
"github.com/pocketbase/pocketbase/tools/search"
|
|
"github.com/pocketbase/pocketbase/tools/tokenizer"
|
|
)
|
|
|
|
type FieldModifier interface {
|
|
// Modify executes the modifier and returns a new modified value.
|
|
Modify(value any) (any, error)
|
|
}
|
|
|
|
// Serializer represents custom REST JSON serializer based on echo.DefaultJSONSerializer,
|
|
// with support for additional generic response data transformation (eg. fields picker).
|
|
type Serializer struct {
|
|
echo.DefaultJSONSerializer
|
|
|
|
FieldsParam string
|
|
}
|
|
|
|
// Serialize converts an interface into a json and writes it to the response.
|
|
//
|
|
// It also provides a generic response data fields picker via the FieldsParam query parameter (default to "fields").
|
|
//
|
|
// Note: for the places where it is safe, the std encoding/json is replaced
|
|
// with goccy due to its slightly better Unmarshal/Marshal performance.
|
|
func (s *Serializer) Serialize(c echo.Context, i any, indent string) error {
|
|
fieldsParam := s.FieldsParam
|
|
if fieldsParam == "" {
|
|
fieldsParam = "fields"
|
|
}
|
|
|
|
statusCode := c.Response().Status
|
|
|
|
rawFields := c.QueryParam(fieldsParam)
|
|
if rawFields == "" || statusCode < 200 || statusCode > 299 {
|
|
return s.DefaultJSONSerializer.Serialize(c, i, indent)
|
|
}
|
|
|
|
parsedFields, err := parseFields(rawFields)
|
|
if err != nil {
|
|
return 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(i) // use the std json since goccy has several bugs reported with struct marshaling and it is not safe
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var decoded any
|
|
if err := goccy.Unmarshal(encoded, &decoded); err != nil {
|
|
return err
|
|
}
|
|
// ---
|
|
|
|
var isSearchResult bool
|
|
switch i.(type) {
|
|
case search.Result, *search.Result:
|
|
isSearchResult = true
|
|
}
|
|
|
|
if isSearchResult {
|
|
if decodedMap, ok := decoded.(map[string]any); ok {
|
|
pickFields(decodedMap["items"], parsedFields)
|
|
}
|
|
} else {
|
|
pickFields(decoded, parsedFields)
|
|
}
|
|
|
|
enc := goccy.NewEncoder(c.Response())
|
|
if indent != "" {
|
|
enc.SetIndent("", indent)
|
|
}
|
|
|
|
return enc.Encode(decoded)
|
|
}
|
|
|
|
func parseFields(rawFields string) (map[string]FieldModifier, error) {
|
|
t := tokenizer.NewFromString(rawFields)
|
|
|
|
fields, err := t.ScanAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(map[string]FieldModifier, 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 initModifer(rawModifier string) (FieldModifier, error) {
|
|
t := tokenizer.NewFromString(rawModifier)
|
|
t.Separators('(', ')', ',', ' ')
|
|
t.IgnoreParenthesis(true)
|
|
|
|
parts, err := t.ScanAll()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(parts) == 0 {
|
|
return nil, fmt.Errorf("invalid or empty modifier expression %q", rawModifier)
|
|
}
|
|
|
|
name := parts[0]
|
|
args := parts[1:]
|
|
|
|
switch name {
|
|
case "excerpt":
|
|
m, err := newExcerptModifier(args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid excerpt modifier: %w", err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("missing or invalid modifier %q", name)
|
|
}
|
|
|
|
func pickFields(data any, fields map[string]FieldModifier) 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]FieldModifier) 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]FieldModifier, 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 := pickFields(data[k], matchingFields); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|