1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-07-17 19:17:50 +02:00

[#943] exposed apis.EnrichRecord and apis.EnrichRecords

This commit is contained in:
Gani Georgiev
2022-11-17 14:17:10 +02:00
parent 6e9cf986c5
commit 39408f135b
16 changed files with 297 additions and 212 deletions

View File

@ -131,7 +131,7 @@ PocketBase has a [roadmap](https://github.com/orgs/pocketbase/projects/2)
and I try to work on issues in a specific order and PRs often come in out of nowhere and skew all initial planning.
Don't get upset if I close your PR, even if it is well executed and tested. This doesn't mean that it will never be merged.
Later we can always refer to it and/or take pieces of your implementation when the time to work on the issue come in (don't worry you'll be credited in the release notes).
Later we can always refer to it and/or take pieces of your implementation when the time comes to work on the issue (don't worry you'll be credited in the release notes).
_Please also note that PocketBase was initially created to serve as a new backend for my other open source project - [Presentator](https://presentator.io) (see [#183](https://github.com/presentator/presentator/issues/183)),
so all feature requests will be first aligned with what we need for Presentator v3._

View File

@ -251,16 +251,10 @@ func (api *realtimeApi) canAccessRecord(client subscriptions.Client, record *mod
}
// emulate request data
requestData := map[string]any{
"method": "GET",
"query": map[string]any{},
"data": map[string]any{},
"auth": nil,
}
authRecord, _ := client.Get(ContextAuthRecordKey).(*models.Record)
if authRecord != nil {
requestData["auth"] = authRecord.PublicExport()
requestData := &models.FilterRequestData{
Method: "GET",
}
requestData.AuthRecord, _ = client.Get(ContextAuthRecordKey).(*models.Record)
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestData, true)
expr, err := search.FilterData(*accessRule).BuildExpr(resolver)

View File

@ -65,20 +65,19 @@ func (api *recordAuthApi) authResponse(c echo.Context, authRecord *models.Record
}
return api.app.OnRecordAuthRequest().Trigger(event, func(e *core.RecordAuthEvent) error {
admin, _ := e.HttpContext.Get(ContextAdminKey).(*models.Admin)
// allow always returning the email address of the authenticated account
e.Record.IgnoreEmailVisibility(true)
// expand record relations
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
if len(expands) > 0 {
requestData := exportRequestData(e.HttpContext)
requestData["auth"] = e.Record.PublicExport()
requestData := GetRequestData(e.HttpContext)
requestData.Admin = nil
requestData.AuthRecord = e.Record
failed := api.app.Dao().ExpandRecord(
e.Record,
expands,
expandFetch(api.app.Dao(), admin != nil, requestData),
expandFetch(api.app.Dao(), requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
@ -204,8 +203,8 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
record, authData, submitErr := form.Submit(func(createForm *forms.RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error {
return createForm.DrySubmit(func(txDao *daos.Dao) error {
requestData := exportRequestData(c)
requestData["data"] = form.CreateData
requestData := GetRequestData(c)
requestData.Data = form.CreateData
createRuleFunc := func(q *dbx.SelectQuery) error {
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
@ -422,7 +421,7 @@ func (api *recordAuthApi) listExternalAuths(c echo.Context) error {
ExternalAuths: externalAuths,
}
return api.app.OnRecordListExternalAuths().Trigger(event, func(e *core.RecordListExternalAuthsEvent) error {
return api.app.OnRecordListExternalAuthsRequest().Trigger(event, func(e *core.RecordListExternalAuthsEvent) error {
return e.HttpContext.JSON(http.StatusOK, e.ExternalAuths)
})
}

View File

@ -886,7 +886,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{`[]`},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
{
Name: "admin + existing user id and 2 external auths",
@ -902,7 +902,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
`"recordId":"4q1xlclmfloku33"`,
`"collectionId":"_pb_users_auth_"`,
},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
{
Name: "auth record + trying to list another user external auths",
@ -933,7 +933,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{`[]`},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
{
Name: "authorized as user - owner with 2 external auths",
@ -949,7 +949,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
`"recordId":"4q1xlclmfloku33"`,
`"collectionId":"_pb_users_auth_"`,
},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
}

View File

@ -46,31 +46,30 @@ func (api *recordApi) list(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.ListRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
// forbid users and guests to query special filter/sort fields
if err := api.checkForForbiddenQueryFields(c); err != nil {
return err
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.ListRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
fieldsResolver := resolvers.NewRecordFieldResolver(
api.app.Dao(),
collection,
requestData,
// hidden fields are searchable only by admins
admin != nil,
requestData.Admin != nil,
)
searchProvider := search.NewProvider(fieldsResolver).
Query(api.app.Dao().RecordQuery(collection))
if admin == nil && collection.ListRule != nil {
if requestData.Admin == nil && collection.ListRule != nil {
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
}
@ -82,28 +81,6 @@ func (api *recordApi) list(c echo.Context) error {
records := models.NewRecordsFromNullStringMaps(collection, rawRecords)
// expand records relations
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
if len(expands) > 0 {
failed := api.app.Dao().ExpandRecords(
records,
expands,
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), records, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
}
result.Items = records
event := &core.RecordsListEvent{
@ -114,6 +91,10 @@ func (api *recordApi) list(c echo.Context) error {
}
return api.app.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListEvent) error {
if err := EnrichRecords(e.HttpContext, api.app.Dao(), e.Records); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Result)
})
}
@ -124,21 +105,20 @@ func (api *recordApi) view(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.ViewRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
recordId := c.PathParam("id")
if recordId == "" {
return NewNotFoundError("", nil)
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.ViewRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
ruleFunc := func(q *dbx.SelectQuery) error {
if admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
if requestData.Admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
if err != nil {
@ -155,31 +135,16 @@ func (api *recordApi) view(c echo.Context) error {
return NewNotFoundError("", fetchErr)
}
// expand record relations
failed := api.app.Dao().ExpandRecord(
record,
strings.Split(c.QueryParam(expandQueryParam), ","),
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), []*models.Record{record}, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
}
event := &core.RecordViewEvent{
HttpContext: c,
Record: record,
}
return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error {
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
}
@ -190,18 +155,17 @@ func (api *recordApi) create(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.CreateRule == nil {
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.CreateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
requestData := exportRequestData(c)
hasFullManageAccess := admin != nil
hasFullManageAccess := requestData.Admin != nil
// temporary save the record and check it against the create rule
if admin == nil && collection.CreateRule != nil {
if requestData.Admin == nil && collection.CreateRule != nil {
createRuleFunc := func(q *dbx.SelectQuery) error {
if *collection.CreateRule == "" {
return nil // no create rule to resolve
@ -260,23 +224,8 @@ func (api *recordApi) create(c echo.Context) error {
return NewBadRequestError("Failed to create record.", err)
}
// expand record relations
failed := api.app.Dao().ExpandRecord(
e.Record,
strings.Split(e.HttpContext.QueryParam(expandQueryParam), ","),
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), []*models.Record{e.Record}, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
@ -297,21 +246,20 @@ func (api *recordApi) update(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.UpdateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
recordId := c.PathParam("id")
if recordId == "" {
return NewNotFoundError("", nil)
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.UpdateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
ruleFunc := func(q *dbx.SelectQuery) error {
if admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
if requestData.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
if err != nil {
@ -330,7 +278,7 @@ func (api *recordApi) update(c echo.Context) error {
}
form := forms.NewRecordUpsert(api.app, record)
form.SetFullManageAccess(admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestData))
form.SetFullManageAccess(requestData.Admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestData))
// load request
if err := form.LoadRequest(c.Request(), ""); err != nil {
@ -350,23 +298,8 @@ func (api *recordApi) update(c echo.Context) error {
return NewBadRequestError("Failed to update record.", err)
}
// expand record relations
failed := api.app.Dao().ExpandRecord(
e.Record,
strings.Split(e.HttpContext.QueryParam(expandQueryParam), ","),
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), []*models.Record{e.Record}, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
@ -387,21 +320,20 @@ func (api *recordApi) delete(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.DeleteRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
recordId := c.PathParam("id")
if recordId == "" {
return NewNotFoundError("", nil)
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.DeleteRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
ruleFunc := func(q *dbx.SelectQuery) error {
if admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
if requestData.Admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
if err != nil {

View File

@ -2,6 +2,7 @@ package apis
import (
"fmt"
"strings"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
@ -10,45 +11,77 @@ import (
"github.com/pocketbase/pocketbase/resolvers"
"github.com/pocketbase/pocketbase/tools/rest"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/spf13/cast"
)
// exportRequestData exports a map with common request fields.
//
// @todo consider changing the map to a typed struct after v0.8 and the
// IN operator support.
func exportRequestData(c echo.Context) map[string]any {
result := map[string]any{}
queryParams := map[string]any{}
bodyData := map[string]any{}
method := c.Request().Method
const ContextRequestDataKey = "requestData"
echo.BindQueryParams(c, &queryParams)
rest.BindBody(c, &bodyData)
result["method"] = method
result["query"] = queryParams
result["data"] = bodyData
result["auth"] = nil
auth, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if auth != nil {
result["auth"] = auth.PublicExport()
// GetRequestData exports common request data fields
// (query, body, logged auth state, etc.) from the provided context.
func GetRequestData(c echo.Context) *models.FilterRequestData {
// return cached to avoid reading the body multiple times
if v := c.Get(ContextRequestDataKey); v != nil {
if data, ok := v.(*models.FilterRequestData); ok {
return data
}
}
result := &models.FilterRequestData{
Method: c.Request().Method,
Query: map[string]any{},
Data: map[string]any{},
}
result.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record)
result.Admin, _ = c.Get(ContextAdminKey).(*models.Admin)
echo.BindQueryParams(c, &result.Query)
rest.BindBody(c, &result.Data)
c.Set(ContextRequestDataKey, result)
return result
}
// EnrichRecord parses the request context and enrich the provided record:
// - expands relations (if defaultExpands and/or ?expand query param is set)
// - ensures that the emails of the auth record and its expanded auth relations
// are visibe only for the current logged admin, record owner or record with manage access
func EnrichRecord(c echo.Context, dao *daos.Dao, record *models.Record, defaultExpands ...string) error {
return EnrichRecords(c, dao, []*models.Record{record}, defaultExpands...)
}
// EnrichRecords parses the request context and enriches the provided records:
// - expands relations (if defaultExpands and/or ?expand query param is set)
// - ensures that the emails of the auth records and their expanded auth relations
// are visibe only for the current logged admin, record owner or record with manage access
func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defaultExpands ...string) error {
requestData := GetRequestData(c)
if err := autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData); err != nil {
return fmt.Errorf("Failed to resolve email visibility: %v", err)
}
expands := defaultExpands
expands = append(expands, strings.Split(c.QueryParam(expandQueryParam), ",")...)
if len(expands) == 0 {
return nil // nothing to expand
}
errs := dao.ExpandRecords(records, expands, expandFetch(dao, requestData))
if len(errs) > 0 {
return fmt.Errorf("Failed to expand: %v", errs)
}
return nil
}
// expandFetch is the records fetch function that is used to expand related records.
func expandFetch(
dao *daos.Dao,
isAdmin bool,
requestData map[string]any,
requestData *models.FilterRequestData,
) daos.ExpandFetchFunc {
return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) {
records, err := dao.FindRecordsByIds(relCollection.Id, relIds, func(q *dbx.SelectQuery) error {
if isAdmin {
if requestData.Admin != nil {
return nil // admins can access everything
}
@ -70,7 +103,7 @@ func expandFetch(
})
if err == nil && len(records) > 0 {
autoIgnoreAuthRecordsEmailVisibility(dao, records, isAdmin, requestData)
autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData)
}
return records, err
@ -84,14 +117,13 @@ func expandFetch(
func autoIgnoreAuthRecordsEmailVisibility(
dao *daos.Dao,
records []*models.Record,
isAdmin bool,
requestData map[string]any,
requestData *models.FilterRequestData,
) error {
if len(records) == 0 || !records[0].Collection().IsAuth() {
return nil // nothing to check
}
if isAdmin {
if requestData.Admin != nil {
for _, rec := range records {
rec.IgnoreEmailVisibility(true)
}
@ -107,8 +139,8 @@ func autoIgnoreAuthRecordsEmailVisibility(
recordIds = append(recordIds, rec.Id)
}
if auth, ok := requestData["auth"].(map[string]any); ok && mappedRecords[cast.ToString(auth["id"])] != nil {
mappedRecords[cast.ToString(auth["id"])].IgnoreEmailVisibility(true)
if requestData != nil && requestData.AuthRecord != nil && mappedRecords[requestData.AuthRecord.Id] != nil {
mappedRecords[requestData.AuthRecord.Id].IgnoreEmailVisibility(true)
}
authOptions := collection.AuthOptions()
@ -153,7 +185,7 @@ func autoIgnoreAuthRecordsEmailVisibility(
func hasAuthManageAccess(
dao *daos.Dao,
record *models.Record,
requestData map[string]any,
requestData *models.FilterRequestData,
) bool {
if !record.Collection().IsAuth() {
return false
@ -165,7 +197,7 @@ func hasAuthManageAccess(
return false // only for admins (manageRule can't be empty)
}
if auth, ok := requestData["auth"].(map[string]any); !ok || cast.ToString(auth["id"]) == "" {
if requestData == nil || requestData.AuthRecord == nil {
return false // no auth record
}

101
apis/record_helpers_test.go Normal file
View File

@ -0,0 +1,101 @@
package apis_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tests"
)
func TestGetRequestData(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/?test=123", strings.NewReader(`{"test":456}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
dummyRecord := &models.Record{}
dummyRecord.Id = "id1"
c.Set(apis.ContextAuthRecordKey, dummyRecord)
dummyAdmin := &models.Admin{}
dummyAdmin.Id = "id2"
c.Set(apis.ContextAdminKey, dummyAdmin)
result := apis.GetRequestData(c)
if result == nil {
t.Fatal("Expected *models.FilterRequestData instance, got nil")
}
if result.Method != http.MethodPost {
t.Fatalf("Expected Method %v, got %v", http.MethodPost, result.Method)
}
rawQuery, _ := json.Marshal(result.Query)
expectedQuery := `{"test":"123"}`
if v := string(rawQuery); v != expectedQuery {
t.Fatalf("Expected Query %v, got %v", expectedQuery, v)
}
rawData, _ := json.Marshal(result.Data)
expectedData := `{"test":456}`
if v := string(rawData); v != expectedData {
t.Fatalf("Expected Data %v, got %v", expectedData, v)
}
if result.AuthRecord == nil || result.AuthRecord.Id != dummyRecord.Id {
t.Fatalf("Expected AuthRecord %v, got %v", dummyRecord, result.AuthRecord)
}
if result.Admin == nil || result.Admin.Id != dummyAdmin.Id {
t.Fatalf("Expected Admin %v, got %v", dummyAdmin, result.Admin)
}
}
func TestEnrichRecords(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/?expand=rel_many", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
dummyAdmin := &models.Admin{}
dummyAdmin.Id = "test_id"
c.Set(apis.ContextAdminKey, dummyAdmin)
app, _ := tests.NewTestApp()
defer app.Cleanup()
records, err := app.Dao().FindRecordsByIds("demo1", []string{"al1h9ijdeojtsjy", "84nmscqy84lsi1t"})
if err != nil {
t.Fatal(err)
}
apis.EnrichRecords(c, app.Dao(), records, "rel_one")
for _, record := range records {
expand := record.Expand()
if len(expand) == 0 {
t.Fatalf("Expected non-empty expand, got nil for record %v", record)
}
if len(record.GetStringSlice("rel_one")) != 0 {
if _, ok := expand["rel_one"]; !ok {
t.Fatalf("Expected rel_one to be expanded for record %v, got \n%v", record, expand)
}
}
if len(record.GetStringSlice("rel_many")) != 0 {
if _, ok := expand["rel_many"]; !ok {
t.Fatalf("Expected rel_many to be expanded for record %v, got \n%v", record, expand)
}
}
}
}

View File

@ -274,10 +274,10 @@ type App interface {
// record data and token.
OnRecordAuthRequest() *hook.Hook[*RecordAuthEvent]
// OnRecordListExternalAuths hook is triggered on each API record external auths list request.
// OnRecordListExternalAuthsRequest hook is triggered on each API record external auths list request.
//
// Could be used to validate or modify the response before returning it to the client.
OnRecordListExternalAuths() *hook.Hook[*RecordListExternalAuthsEvent]
OnRecordListExternalAuthsRequest() *hook.Hook[*RecordListExternalAuthsEvent]
// OnRecordBeforeUnlinkExternalAuthRequest hook is triggered before each API record
// external auth unlink request (after models load and before the actual relation deletion).

View File

@ -88,7 +88,7 @@ type BaseApp struct {
// user api event hooks
onRecordAuthRequest *hook.Hook[*RecordAuthEvent]
onRecordListExternalAuths *hook.Hook[*RecordListExternalAuthsEvent]
onRecordListExternalAuthsRequest *hook.Hook[*RecordListExternalAuthsEvent]
onRecordBeforeUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent]
onRecordAfterUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent]
@ -175,7 +175,7 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
// user API event hooks
onRecordAuthRequest: &hook.Hook[*RecordAuthEvent]{},
onRecordListExternalAuths: &hook.Hook[*RecordListExternalAuthsEvent]{},
onRecordListExternalAuthsRequest: &hook.Hook[*RecordListExternalAuthsEvent]{},
onRecordBeforeUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{},
onRecordAfterUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{},
@ -574,8 +574,8 @@ func (app *BaseApp) OnRecordAuthRequest() *hook.Hook[*RecordAuthEvent] {
return app.onRecordAuthRequest
}
func (app *BaseApp) OnRecordListExternalAuths() *hook.Hook[*RecordListExternalAuthsEvent] {
return app.onRecordListExternalAuths
func (app *BaseApp) OnRecordListExternalAuthsRequest() *hook.Hook[*RecordListExternalAuthsEvent] {
return app.onRecordListExternalAuthsRequest
}
func (app *BaseApp) OnRecordBeforeUnlinkExternalAuthRequest() *hook.Hook[*RecordUnlinkExternalAuthEvent] {

View File

@ -319,8 +319,8 @@ func TestBaseAppGetters(t *testing.T) {
t.Fatalf("Getter app.OnRecordAuthRequest does not match or nil (%v vs %v)", app.OnRecordAuthRequest(), app.onRecordAuthRequest)
}
if app.onRecordListExternalAuths != app.OnRecordListExternalAuths() || app.OnRecordListExternalAuths() == nil {
t.Fatalf("Getter app.OnRecordListExternalAuths does not match or nil (%v vs %v)", app.OnRecordListExternalAuths(), app.onRecordListExternalAuths)
if app.onRecordListExternalAuthsRequest != app.OnRecordListExternalAuthsRequest() || app.OnRecordListExternalAuthsRequest() == nil {
t.Fatalf("Getter app.OnRecordListExternalAuthsRequest does not match or nil (%v vs %v)", app.OnRecordListExternalAuthsRequest(), app.onRecordListExternalAuthsRequest)
}
if app.onRecordBeforeUnlinkExternalAuthRequest != app.OnRecordBeforeUnlinkExternalAuthRequest() || app.OnRecordBeforeUnlinkExternalAuthRequest() == nil {

View File

@ -0,0 +1,11 @@
package models
// FilterRequestData defines a HTTP request data struct, usually used
// as part of the `@request.*` filter resolver.
type FilterRequestData struct {
Method string `json:"method"`
Query map[string]any `json:"query"`
Data map[string]any `json:"data"`
AuthRecord *Record `json:"authRecord"`
Admin *Admin `json:"admin"`
}

View File

@ -52,10 +52,11 @@ type RecordFieldResolver struct {
baseCollection *models.Collection
allowHiddenFields bool
allowedFields []string
requestData map[string]any
loadedCollections []*models.Collection
joins []join // we cannot use a map because the insertion order is not preserved
exprs []dbx.Expression
requestData *models.FilterRequestData
staticRequestData map[string]any
}
// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`.
@ -66,10 +67,10 @@ type RecordFieldResolver struct {
func NewRecordFieldResolver(
dao *daos.Dao,
baseCollection *models.Collection,
requestData map[string]any,
requestData *models.FilterRequestData,
allowHiddenFields bool,
) *RecordFieldResolver {
return &RecordFieldResolver{
r := &RecordFieldResolver{
dao: dao,
baseCollection: baseCollection,
requestData: requestData,
@ -86,6 +87,22 @@ func NewRecordFieldResolver(
`^\@collection\.\w+\.\w+[\w\.]*$`,
},
}
// @todo remove after IN operator and multi-match filter enhancements
r.staticRequestData = map[string]any{}
if r.requestData != nil {
r.staticRequestData["method"] = r.requestData.Method
r.staticRequestData["query"] = r.requestData.Query
r.staticRequestData["data"] = r.requestData.Data
r.staticRequestData["auth"] = nil
if r.requestData.AuthRecord != nil {
r.requestData.AuthRecord.IgnoreEmailVisibility(true)
r.staticRequestData["auth"] = r.requestData.AuthRecord.PublicExport()
r.requestData.AuthRecord.IgnoreEmailVisibility(false)
}
}
return r
}
// UpdateQuery implements `search.FieldResolver` interface.
@ -159,6 +176,10 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName)
}
if r.requestData == nil {
return "NULL", nil, nil
}
// plain @request.* field
if !strings.HasPrefix(fieldName, "@request.auth.") || list.ExistInSlice(fieldName, plainRequestAuthFields) {
return r.resolveStaticRequestField(props[1:]...)
@ -173,28 +194,18 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
// resolve the auth collection fields
// ---
rawAuthRecordId, _ := extractNestedMapVal(r.requestData, "auth", "id")
authRecordId := cast.ToString(rawAuthRecordId)
if authRecordId == "" {
if r.requestData == nil || r.requestData.AuthRecord == nil || r.requestData.AuthRecord.Collection() == nil {
return "NULL", nil, nil
}
rawAuthCollectionId, _ := extractNestedMapVal(r.requestData, "auth", schema.FieldNameCollectionId)
authCollectionId := cast.ToString(rawAuthCollectionId)
if authCollectionId == "" {
return "NULL", nil, nil
}
collection, err := r.loadCollection(authCollectionId)
if err != nil {
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
}
collection := r.requestData.AuthRecord.Collection()
r.loadedCollections = append(r.loadedCollections, collection)
currentCollectionName = collection.Name
currentTableAlias = "__auth_" + inflector.Columnify(currentCollectionName)
authIdParamKey := "auth" + security.PseudorandomString(5)
authIdParams := dbx.Params{authIdParamKey: authRecordId}
authIdParams := dbx.Params{authIdParamKey: r.requestData.AuthRecord.Id}
// ---
// join the auth collection
@ -331,7 +342,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) {
// ignore error because requestData is dynamic and some of the
// lookup keys may not be defined for the request
resultVal, _ := extractNestedMapVal(r.requestData, path...)
resultVal, _ := extractNestedMapVal(r.staticRequestData, path...)
switch v := resultVal.(type) {
case nil:
@ -387,7 +398,7 @@ func extractNestedMapVal(m map[string]any, keys ...string) (result any, err erro
func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models.Collection, error) {
// return already loaded
for _, collection := range r.loadedCollections {
if collection.Name == collectionNameOrId || collection.Id == collectionNameOrId {
if collection.Id == collectionNameOrId || strings.EqualFold(collection.Name, collectionNameOrId) {
return collection, nil
}
}

View File

@ -5,6 +5,7 @@ import (
"regexp"
"testing"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/resolvers"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/list"
@ -19,8 +20,8 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
t.Fatal(err)
}
requestData := map[string]any{
"auth": authRecord.PublicExport(),
requestData := &models.FilterRequestData{
AuthRecord: authRecord,
}
scenarios := []struct {
@ -181,8 +182,8 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
t.Fatal(err)
}
requestData := map[string]any{
"auth": authRecord.PublicExport(),
requestData := &models.FilterRequestData{
AuthRecord: authRecord,
}
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
@ -262,16 +263,16 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
t.Fatal(err)
}
requestData := map[string]any{
"method": "get",
"query": map[string]any{
requestData := &models.FilterRequestData{
Method: "get",
Query: map[string]any{
"a": 123,
},
"data": map[string]any{
Data: map[string]any{
"b": 456,
"c": map[string]int{"sub": 1},
},
"user": authRecord.PublicExport(),
AuthRecord: authRecord,
}
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
@ -295,7 +296,11 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
{"@request.data.c", false, `"{\"sub\":1}"`},
{"@request.auth", true, ""},
{"@request.auth.id", false, `"4q1xlclmfloku33"`},
{"@request.auth.file", false, `"[]"`},
{"@request.auth.email", false, `"test@example.com"`},
{"@request.auth.username", false, `"users75657"`},
{"@request.auth.verified", false, `false`},
{"@request.auth.emailVisibility", false, `false`},
{"@request.auth.missing", false, `NULL`},
}
for i, s := range scenarios {
@ -315,7 +320,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
// ---
if len(params) == 0 {
if name != "NULL" {
t.Errorf("(%d) Expected 0 placeholder parameters, got %v", i, params)
t.Errorf("(%d) Expected 0 placeholder parameters for %v, got %v", i, name, params)
}
continue
}
@ -323,7 +328,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
// existing key
// ---
if len(params) != 1 {
t.Errorf("(%d) Expected 1 placeholder parameter, got %v", i, params)
t.Errorf("(%d) Expected 1 placeholder parameter for %v, got %v", i, name, params)
continue
}
@ -340,7 +345,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
encodedParamValue, _ := json.Marshal(paramValue)
if string(encodedParamValue) != s.expectParamValue {
t.Errorf("(%d) Expected params %v, got %v", i, s.expectParamValue, string(encodedParamValue))
t.Errorf("(%d) Expected params %v for %v, got %v", i, s.expectParamValue, name, string(encodedParamValue))
}
}
}

View File

@ -162,8 +162,8 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
return nil
})
t.OnRecordListExternalAuths().Add(func(e *core.RecordListExternalAuthsEvent) error {
t.EventCalls["OnRecordListExternalAuths"]++
t.OnRecordListExternalAuthsRequest().Add(func(e *core.RecordListExternalAuthsEvent) error {
t.EventCalls["OnRecordListExternalAuthsRequest"]++
return nil
})

Binary file not shown.

Binary file not shown.