mirror of
https://github.com/pocketbase/pocketbase.git
synced 2024-11-24 09:02:26 +02:00
replaced the default binder with rest.MultiBinder
This commit is contained in:
parent
d9b219d64f
commit
9855397a22
@ -28,6 +28,7 @@ const trailedAdminPath = "/_/"
|
|||||||
func InitApi(app core.App) (*echo.Echo, error) {
|
func InitApi(app core.App) (*echo.Echo, error) {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Debug = false
|
e.Debug = false
|
||||||
|
e.Binder = &rest.MultiBinder{}
|
||||||
e.JSONSerializer = &rest.Serializer{
|
e.JSONSerializer = &rest.Serializer{
|
||||||
FieldsParam: fieldsQueryParam,
|
FieldsParam: fieldsQueryParam,
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
"github.com/pocketbase/pocketbase/tests"
|
"github.com/pocketbase/pocketbase/tests"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/rest"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -220,15 +221,24 @@ func TestRemoveTrailingSlashMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEagerRequestInfoCache(t *testing.T) {
|
func TestMultiBinder(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
rawJson := `{"name":"test123"}`
|
||||||
|
|
||||||
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
||||||
|
rest.MultipartJsonKey: rawJson,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
scenarios := []tests.ApiScenario{
|
scenarios := []tests.ApiScenario{
|
||||||
{
|
{
|
||||||
Name: "custom non-api group route",
|
Name: "non-api group route",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Url: "/custom",
|
Url: "/custom",
|
||||||
Body: strings.NewReader(`{"name":"test123"}`),
|
Body: strings.NewReader(rawJson),
|
||||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||||
e.AddRoute(echo.Route{
|
e.AddRoute(echo.Route{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@ -242,11 +252,10 @@ func TestEagerRequestInfoCache(t *testing.T) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// since the unknown method is not eager cache support
|
// try to read the body again
|
||||||
// it should fail reading the json body twice
|
|
||||||
r := apis.RequestInfo(c)
|
r := apis.RequestInfo(c)
|
||||||
if v := cast.ToString(r.Data["name"]); v != "" {
|
if v := cast.ToString(r.Data["name"]); v != "test123" {
|
||||||
t.Fatalf("Expected empty request data body, got, %v", r.Data)
|
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.NoContent(200)
|
return c.NoContent(200)
|
||||||
@ -256,41 +265,10 @@ func TestEagerRequestInfoCache(t *testing.T) {
|
|||||||
ExpectedStatus: 200,
|
ExpectedStatus: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "api group route with unsupported eager cache request method",
|
Name: "api group route",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Url: "/api/admins",
|
Url: "/api/admins",
|
||||||
Body: strings.NewReader(`{"name":"test123"}`),
|
Body: strings.NewReader(rawJson),
|
||||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
||||||
return func(c echo.Context) error {
|
|
||||||
// it is not important whether the route handler return an error since
|
|
||||||
// we just need to ensure that the eagerRequestInfoCache was registered
|
|
||||||
next(c)
|
|
||||||
|
|
||||||
// ensure that the body was read at least once
|
|
||||||
data := &struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}{}
|
|
||||||
c.Bind(data)
|
|
||||||
|
|
||||||
// since the unknown method is not eager cache support
|
|
||||||
// it should fail reading the json body twice
|
|
||||||
r := apis.RequestInfo(c)
|
|
||||||
if v := cast.ToString(r.Data["name"]); v != "" {
|
|
||||||
t.Fatalf("Expected empty request data body, got, %v", r.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
ExpectedStatus: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "api group route with supported eager cache request method",
|
|
||||||
Method: "POST",
|
|
||||||
Url: "/api/admins",
|
|
||||||
Body: strings.NewReader(`{"name":"test123"}`),
|
|
||||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
@ -316,6 +294,39 @@ func TestEagerRequestInfoCache(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ExpectedStatus: 200,
|
ExpectedStatus: 200,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "custom route with @jsonPayload as multipart body",
|
||||||
|
Method: "POST",
|
||||||
|
Url: "/custom",
|
||||||
|
Body: formData,
|
||||||
|
RequestHeaders: map[string]string{
|
||||||
|
"Content-Type": mp.FormDataContentType(),
|
||||||
|
},
|
||||||
|
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||||
|
e.AddRoute(echo.Route{
|
||||||
|
Method: "POST",
|
||||||
|
Path: "/custom",
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
data := &struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := c.Bind(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to read the body again
|
||||||
|
r := apis.RequestInfo(c)
|
||||||
|
if v := cast.ToString(r.Data["name"]); v != "test123" {
|
||||||
|
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(200)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
ExpectedStatus: 200,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
|
@ -402,6 +402,8 @@ func realUserIp(r *http.Request, fallbackIp string) string {
|
|||||||
return fallbackIp
|
return fallbackIp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @todo consider removing as this may no longer be needed due to the custom rest.MultiBinder.
|
||||||
|
//
|
||||||
// eagerRequestInfoCache ensures that the request data is cached in the request
|
// eagerRequestInfoCache ensures that the request data is cached in the request
|
||||||
// context to allow reading for example the json request body data more than once.
|
// context to allow reading for example the json request body data more than once.
|
||||||
func eagerRequestInfoCache(app core.App) echo.MiddlewareFunc {
|
func eagerRequestInfoCache(app core.App) echo.MiddlewareFunc {
|
||||||
|
@ -68,8 +68,8 @@ func (s *Serializer) Serialize(c echo.Context, i any, indent string) error {
|
|||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}}
|
// data := map[string]any{"a": 1, "b": 2, "c": map[string]any{"c1": 11, "c2": 22}}
|
||||||
// PickFields(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}}
|
// PickFields(data, "a,c.c1") // map[string]any{"a": 1, "c": map[string]any{"c1": 11}}
|
||||||
func PickFields(data any, rawFields string) (any, error) {
|
func PickFields(data any, rawFields string) (any, error) {
|
||||||
parsedFields, err := parseFields(rawFields)
|
parsedFields, err := parseFields(rawFields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -13,9 +13,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MultipartJsonKey is the key for the special multipart/form-data
|
// MultipartJsonKey is the key for the special multipart/form-data
|
||||||
// handling allowing reading serialized json payload without normalizations
|
// handling allowing reading serialized json payload without normalization.
|
||||||
const MultipartJsonKey string = "@jsonPayload"
|
const MultipartJsonKey string = "@jsonPayload"
|
||||||
|
|
||||||
|
// MultiBinder is similar to [echo.DefaultBinder] but uses slightly different
|
||||||
|
// application/json and multipart/form-data bind methods to accommodate better
|
||||||
|
// the PocketBase router needs.
|
||||||
|
type MultiBinder struct{}
|
||||||
|
|
||||||
|
// Bind implements the [Binder.Bind] method.
|
||||||
|
//
|
||||||
|
// Bind is almost identical to [echo.DefaultBinder.Bind] but uses the
|
||||||
|
// [rest.BindBody] function for binding the request body.
|
||||||
|
func (b *MultiBinder) Bind(c echo.Context, i interface{}) (err error) {
|
||||||
|
if err := echo.BindPathParams(c, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body.
|
||||||
|
// For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues.
|
||||||
|
method := c.Request().Method
|
||||||
|
if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {
|
||||||
|
if err = echo.BindQueryParams(c, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BindBody(c, i)
|
||||||
|
}
|
||||||
|
|
||||||
// BindBody binds request body content to i.
|
// BindBody binds request body content to i.
|
||||||
//
|
//
|
||||||
// This is similar to `echo.BindBody()`, but for JSON requests uses
|
// This is similar to `echo.BindBody()`, but for JSON requests uses
|
||||||
@ -47,8 +73,8 @@ func BindBody(c echo.Context, i any) error {
|
|||||||
func CopyJsonBody(r *http.Request, i any) error {
|
func CopyJsonBody(r *http.Request, i any) error {
|
||||||
body := r.Body
|
body := r.Body
|
||||||
|
|
||||||
// this usually shouldn't be needed because the Server calls close for us
|
// this usually shouldn't be needed because the Server calls close
|
||||||
// but we are changing the request body with a new reader
|
// for us but we are changing the request body with a new reader
|
||||||
defer body.Close()
|
defer body.Close()
|
||||||
|
|
||||||
limitReader := io.LimitReader(body, DefaultMaxMemory)
|
limitReader := io.LimitReader(body, DefaultMaxMemory)
|
||||||
@ -82,7 +108,7 @@ func bindFormData(c echo.Context, i any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// special case to allow submitting json without normalizations
|
// special case to allow submitting json without normalization
|
||||||
// alongside the other multipart/form-data values
|
// alongside the other multipart/form-data values
|
||||||
jsonPayloadValues := values[MultipartJsonKey]
|
jsonPayloadValues := values[MultipartJsonKey]
|
||||||
for _, payload := range jsonPayloadValues {
|
for _, payload := range jsonPayloadValues {
|
||||||
|
@ -13,6 +13,46 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/tools/rest"
|
"github.com/pocketbase/pocketbase/tools/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMultiBinderBind(t *testing.T) {
|
||||||
|
binder := rest.MultiBinder{}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test?query=123", strings.NewReader(`{"body":"456"}`))
|
||||||
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
e.Any("/:name", func(c echo.Context) error {
|
||||||
|
// bind twice to ensure that the json body reader copy is invoked
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
data := struct {
|
||||||
|
Name string `param:"name"`
|
||||||
|
Query string `query:"query"`
|
||||||
|
Body string `form:"body"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := binder.Bind(c, &data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Name != "test" {
|
||||||
|
t.Fatalf("Expected Name %q, got %q", "test", data.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Query != "123" {
|
||||||
|
t.Fatalf("Expected Query %q, got %q", "123", data.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Body != "456" {
|
||||||
|
t.Fatalf("Expected Body %q, got %q", "456", data.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindBody(t *testing.T) {
|
func TestBindBody(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
body io.Reader
|
body io.Reader
|
||||||
|
Loading…
Reference in New Issue
Block a user