[#943] exposed apis.EnrichRecord and apis.EnrichRecords
This commit is contained in:
parent
6e9cf986c5
commit
39408f135b
|
@ -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.
|
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.
|
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)),
|
_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._
|
so all feature requests will be first aligned with what we need for Presentator v3._
|
||||||
|
|
|
@ -251,16 +251,10 @@ func (api *realtimeApi) canAccessRecord(client subscriptions.Client, record *mod
|
||||||
}
|
}
|
||||||
|
|
||||||
// emulate request data
|
// emulate request data
|
||||||
requestData := map[string]any{
|
requestData := &models.FilterRequestData{
|
||||||
"method": "GET",
|
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.AuthRecord, _ = client.Get(ContextAuthRecordKey).(*models.Record)
|
||||||
|
|
||||||
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestData, true)
|
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestData, true)
|
||||||
expr, err := search.FilterData(*accessRule).BuildExpr(resolver)
|
expr, err := search.FilterData(*accessRule).BuildExpr(resolver)
|
||||||
|
|
|
@ -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 {
|
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
|
// allow always returning the email address of the authenticated account
|
||||||
e.Record.IgnoreEmailVisibility(true)
|
e.Record.IgnoreEmailVisibility(true)
|
||||||
|
|
||||||
// expand record relations
|
// expand record relations
|
||||||
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
|
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
|
||||||
if len(expands) > 0 {
|
if len(expands) > 0 {
|
||||||
requestData := exportRequestData(e.HttpContext)
|
requestData := GetRequestData(e.HttpContext)
|
||||||
requestData["auth"] = e.Record.PublicExport()
|
requestData.Admin = nil
|
||||||
|
requestData.AuthRecord = e.Record
|
||||||
failed := api.app.Dao().ExpandRecord(
|
failed := api.app.Dao().ExpandRecord(
|
||||||
e.Record,
|
e.Record,
|
||||||
expands,
|
expands,
|
||||||
expandFetch(api.app.Dao(), admin != nil, requestData),
|
expandFetch(api.app.Dao(), requestData),
|
||||||
)
|
)
|
||||||
if len(failed) > 0 && api.app.IsDebug() {
|
if len(failed) > 0 && api.app.IsDebug() {
|
||||||
log.Println("Failed to expand relations: ", failed)
|
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 {
|
record, authData, submitErr := form.Submit(func(createForm *forms.RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error {
|
||||||
return createForm.DrySubmit(func(txDao *daos.Dao) error {
|
return createForm.DrySubmit(func(txDao *daos.Dao) error {
|
||||||
requestData := exportRequestData(c)
|
requestData := GetRequestData(c)
|
||||||
requestData["data"] = form.CreateData
|
requestData.Data = form.CreateData
|
||||||
|
|
||||||
createRuleFunc := func(q *dbx.SelectQuery) error {
|
createRuleFunc := func(q *dbx.SelectQuery) error {
|
||||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
||||||
|
@ -422,7 +421,7 @@ func (api *recordAuthApi) listExternalAuths(c echo.Context) error {
|
||||||
ExternalAuths: externalAuths,
|
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)
|
return e.HttpContext.JSON(http.StatusOK, e.ExternalAuths)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -886,7 +886,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
|
||||||
},
|
},
|
||||||
ExpectedStatus: 200,
|
ExpectedStatus: 200,
|
||||||
ExpectedContent: []string{`[]`},
|
ExpectedContent: []string{`[]`},
|
||||||
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
|
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "admin + existing user id and 2 external auths",
|
Name: "admin + existing user id and 2 external auths",
|
||||||
|
@ -902,7 +902,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
|
||||||
`"recordId":"4q1xlclmfloku33"`,
|
`"recordId":"4q1xlclmfloku33"`,
|
||||||
`"collectionId":"_pb_users_auth_"`,
|
`"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",
|
Name: "auth record + trying to list another user external auths",
|
||||||
|
@ -933,7 +933,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
|
||||||
},
|
},
|
||||||
ExpectedStatus: 200,
|
ExpectedStatus: 200,
|
||||||
ExpectedContent: []string{`[]`},
|
ExpectedContent: []string{`[]`},
|
||||||
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
|
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "authorized as user - owner with 2 external auths",
|
Name: "authorized as user - owner with 2 external auths",
|
||||||
|
@ -949,7 +949,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
|
||||||
`"recordId":"4q1xlclmfloku33"`,
|
`"recordId":"4q1xlclmfloku33"`,
|
||||||
`"collectionId":"_pb_users_auth_"`,
|
`"collectionId":"_pb_users_auth_"`,
|
||||||
},
|
},
|
||||||
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
|
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,31 +46,30 @@ func (api *recordApi) list(c echo.Context) error {
|
||||||
return NewNotFoundError("", "Missing collection context.")
|
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
|
// forbid users and guests to query special filter/sort fields
|
||||||
if err := api.checkForForbiddenQueryFields(c); err != nil {
|
if err := api.checkForForbiddenQueryFields(c); err != nil {
|
||||||
return err
|
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(
|
fieldsResolver := resolvers.NewRecordFieldResolver(
|
||||||
api.app.Dao(),
|
api.app.Dao(),
|
||||||
collection,
|
collection,
|
||||||
requestData,
|
requestData,
|
||||||
// hidden fields are searchable only by admins
|
// hidden fields are searchable only by admins
|
||||||
admin != nil,
|
requestData.Admin != nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
searchProvider := search.NewProvider(fieldsResolver).
|
searchProvider := search.NewProvider(fieldsResolver).
|
||||||
Query(api.app.Dao().RecordQuery(collection))
|
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))
|
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,28 +81,6 @@ func (api *recordApi) list(c echo.Context) error {
|
||||||
|
|
||||||
records := models.NewRecordsFromNullStringMaps(collection, rawRecords)
|
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
|
result.Items = records
|
||||||
|
|
||||||
event := &core.RecordsListEvent{
|
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 {
|
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)
|
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.")
|
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")
|
recordId := c.PathParam("id")
|
||||||
if recordId == "" {
|
if recordId == "" {
|
||||||
return NewNotFoundError("", nil)
|
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 {
|
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)
|
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
|
||||||
expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
|
expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -155,31 +135,16 @@ func (api *recordApi) view(c echo.Context) error {
|
||||||
return NewNotFoundError("", fetchErr)
|
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{
|
event := &core.RecordViewEvent{
|
||||||
HttpContext: c,
|
HttpContext: c,
|
||||||
Record: record,
|
Record: record,
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error {
|
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)
|
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.")
|
return NewNotFoundError("", "Missing collection context.")
|
||||||
}
|
}
|
||||||
|
|
||||||
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
|
requestData := GetRequestData(c)
|
||||||
if admin == nil && collection.CreateRule == nil {
|
|
||||||
|
if requestData.Admin == nil && collection.CreateRule == nil {
|
||||||
// only admins can access if the rule is nil
|
// only admins can access if the rule is nil
|
||||||
return NewForbiddenError("Only admins can perform this action.", nil)
|
return NewForbiddenError("Only admins can perform this action.", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestData := exportRequestData(c)
|
hasFullManageAccess := requestData.Admin != nil
|
||||||
|
|
||||||
hasFullManageAccess := admin != nil
|
|
||||||
|
|
||||||
// temporary save the record and check it against the create rule
|
// 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 {
|
createRuleFunc := func(q *dbx.SelectQuery) error {
|
||||||
if *collection.CreateRule == "" {
|
if *collection.CreateRule == "" {
|
||||||
return nil // no create rule to resolve
|
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)
|
return NewBadRequestError("Failed to create record.", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand record relations
|
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
|
||||||
failed := api.app.Dao().ExpandRecord(
|
log.Println(err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
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.")
|
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")
|
recordId := c.PathParam("id")
|
||||||
if recordId == "" {
|
if recordId == "" {
|
||||||
return NewNotFoundError("", nil)
|
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 {
|
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)
|
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
|
||||||
expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
|
expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -330,7 +278,7 @@ func (api *recordApi) update(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
form := forms.NewRecordUpsert(api.app, record)
|
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
|
// load request
|
||||||
if err := form.LoadRequest(c.Request(), ""); err != nil {
|
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)
|
return NewBadRequestError("Failed to update record.", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand record relations
|
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
|
||||||
failed := api.app.Dao().ExpandRecord(
|
log.Println(err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.HttpContext.JSON(http.StatusOK, e.Record)
|
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.")
|
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")
|
recordId := c.PathParam("id")
|
||||||
if recordId == "" {
|
if recordId == "" {
|
||||||
return NewNotFoundError("", nil)
|
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 {
|
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)
|
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
|
||||||
expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
|
expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package apis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
|
@ -10,45 +11,77 @@ import (
|
||||||
"github.com/pocketbase/pocketbase/resolvers"
|
"github.com/pocketbase/pocketbase/resolvers"
|
||||||
"github.com/pocketbase/pocketbase/tools/rest"
|
"github.com/pocketbase/pocketbase/tools/rest"
|
||||||
"github.com/pocketbase/pocketbase/tools/search"
|
"github.com/pocketbase/pocketbase/tools/search"
|
||||||
"github.com/spf13/cast"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// exportRequestData exports a map with common request fields.
|
const ContextRequestDataKey = "requestData"
|
||||||
//
|
|
||||||
// @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
|
|
||||||
|
|
||||||
echo.BindQueryParams(c, &queryParams)
|
// GetRequestData exports common request data fields
|
||||||
|
// (query, body, logged auth state, etc.) from the provided context.
|
||||||
rest.BindBody(c, &bodyData)
|
func GetRequestData(c echo.Context) *models.FilterRequestData {
|
||||||
|
// return cached to avoid reading the body multiple times
|
||||||
result["method"] = method
|
if v := c.Get(ContextRequestDataKey); v != nil {
|
||||||
result["query"] = queryParams
|
if data, ok := v.(*models.FilterRequestData); ok {
|
||||||
result["data"] = bodyData
|
return data
|
||||||
result["auth"] = nil
|
}
|
||||||
|
|
||||||
auth, _ := c.Get(ContextAuthRecordKey).(*models.Record)
|
|
||||||
if auth != nil {
|
|
||||||
result["auth"] = auth.PublicExport()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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.
|
// expandFetch is the records fetch function that is used to expand related records.
|
||||||
func expandFetch(
|
func expandFetch(
|
||||||
dao *daos.Dao,
|
dao *daos.Dao,
|
||||||
isAdmin bool,
|
requestData *models.FilterRequestData,
|
||||||
requestData map[string]any,
|
|
||||||
) daos.ExpandFetchFunc {
|
) daos.ExpandFetchFunc {
|
||||||
return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) {
|
return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) {
|
||||||
records, err := dao.FindRecordsByIds(relCollection.Id, relIds, func(q *dbx.SelectQuery) 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
|
return nil // admins can access everything
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +103,7 @@ func expandFetch(
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil && len(records) > 0 {
|
if err == nil && len(records) > 0 {
|
||||||
autoIgnoreAuthRecordsEmailVisibility(dao, records, isAdmin, requestData)
|
autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return records, err
|
return records, err
|
||||||
|
@ -84,14 +117,13 @@ func expandFetch(
|
||||||
func autoIgnoreAuthRecordsEmailVisibility(
|
func autoIgnoreAuthRecordsEmailVisibility(
|
||||||
dao *daos.Dao,
|
dao *daos.Dao,
|
||||||
records []*models.Record,
|
records []*models.Record,
|
||||||
isAdmin bool,
|
requestData *models.FilterRequestData,
|
||||||
requestData map[string]any,
|
|
||||||
) error {
|
) error {
|
||||||
if len(records) == 0 || !records[0].Collection().IsAuth() {
|
if len(records) == 0 || !records[0].Collection().IsAuth() {
|
||||||
return nil // nothing to check
|
return nil // nothing to check
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAdmin {
|
if requestData.Admin != nil {
|
||||||
for _, rec := range records {
|
for _, rec := range records {
|
||||||
rec.IgnoreEmailVisibility(true)
|
rec.IgnoreEmailVisibility(true)
|
||||||
}
|
}
|
||||||
|
@ -107,8 +139,8 @@ func autoIgnoreAuthRecordsEmailVisibility(
|
||||||
recordIds = append(recordIds, rec.Id)
|
recordIds = append(recordIds, rec.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth, ok := requestData["auth"].(map[string]any); ok && mappedRecords[cast.ToString(auth["id"])] != nil {
|
if requestData != nil && requestData.AuthRecord != nil && mappedRecords[requestData.AuthRecord.Id] != nil {
|
||||||
mappedRecords[cast.ToString(auth["id"])].IgnoreEmailVisibility(true)
|
mappedRecords[requestData.AuthRecord.Id].IgnoreEmailVisibility(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
authOptions := collection.AuthOptions()
|
authOptions := collection.AuthOptions()
|
||||||
|
@ -153,7 +185,7 @@ func autoIgnoreAuthRecordsEmailVisibility(
|
||||||
func hasAuthManageAccess(
|
func hasAuthManageAccess(
|
||||||
dao *daos.Dao,
|
dao *daos.Dao,
|
||||||
record *models.Record,
|
record *models.Record,
|
||||||
requestData map[string]any,
|
requestData *models.FilterRequestData,
|
||||||
) bool {
|
) bool {
|
||||||
if !record.Collection().IsAuth() {
|
if !record.Collection().IsAuth() {
|
||||||
return false
|
return false
|
||||||
|
@ -165,7 +197,7 @@ func hasAuthManageAccess(
|
||||||
return false // only for admins (manageRule can't be empty)
|
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
|
return false // no auth record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -274,10 +274,10 @@ type App interface {
|
||||||
// record data and token.
|
// record data and token.
|
||||||
OnRecordAuthRequest() *hook.Hook[*RecordAuthEvent]
|
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.
|
// 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
|
// OnRecordBeforeUnlinkExternalAuthRequest hook is triggered before each API record
|
||||||
// external auth unlink request (after models load and before the actual relation deletion).
|
// external auth unlink request (after models load and before the actual relation deletion).
|
||||||
|
|
|
@ -88,7 +88,7 @@ type BaseApp struct {
|
||||||
|
|
||||||
// user api event hooks
|
// user api event hooks
|
||||||
onRecordAuthRequest *hook.Hook[*RecordAuthEvent]
|
onRecordAuthRequest *hook.Hook[*RecordAuthEvent]
|
||||||
onRecordListExternalAuths *hook.Hook[*RecordListExternalAuthsEvent]
|
onRecordListExternalAuthsRequest *hook.Hook[*RecordListExternalAuthsEvent]
|
||||||
onRecordBeforeUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent]
|
onRecordBeforeUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent]
|
||||||
onRecordAfterUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent]
|
onRecordAfterUnlinkExternalAuthRequest *hook.Hook[*RecordUnlinkExternalAuthEvent]
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
|
||||||
|
|
||||||
// user API event hooks
|
// user API event hooks
|
||||||
onRecordAuthRequest: &hook.Hook[*RecordAuthEvent]{},
|
onRecordAuthRequest: &hook.Hook[*RecordAuthEvent]{},
|
||||||
onRecordListExternalAuths: &hook.Hook[*RecordListExternalAuthsEvent]{},
|
onRecordListExternalAuthsRequest: &hook.Hook[*RecordListExternalAuthsEvent]{},
|
||||||
onRecordBeforeUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{},
|
onRecordBeforeUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{},
|
||||||
onRecordAfterUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{},
|
onRecordAfterUnlinkExternalAuthRequest: &hook.Hook[*RecordUnlinkExternalAuthEvent]{},
|
||||||
|
|
||||||
|
@ -574,8 +574,8 @@ func (app *BaseApp) OnRecordAuthRequest() *hook.Hook[*RecordAuthEvent] {
|
||||||
return app.onRecordAuthRequest
|
return app.onRecordAuthRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BaseApp) OnRecordListExternalAuths() *hook.Hook[*RecordListExternalAuthsEvent] {
|
func (app *BaseApp) OnRecordListExternalAuthsRequest() *hook.Hook[*RecordListExternalAuthsEvent] {
|
||||||
return app.onRecordListExternalAuths
|
return app.onRecordListExternalAuthsRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *BaseApp) OnRecordBeforeUnlinkExternalAuthRequest() *hook.Hook[*RecordUnlinkExternalAuthEvent] {
|
func (app *BaseApp) OnRecordBeforeUnlinkExternalAuthRequest() *hook.Hook[*RecordUnlinkExternalAuthEvent] {
|
||||||
|
|
|
@ -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)
|
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 {
|
if app.onRecordListExternalAuthsRequest != app.OnRecordListExternalAuthsRequest() || app.OnRecordListExternalAuthsRequest() == nil {
|
||||||
t.Fatalf("Getter app.OnRecordListExternalAuths does not match or nil (%v vs %v)", app.OnRecordListExternalAuths(), app.onRecordListExternalAuths)
|
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 {
|
if app.onRecordBeforeUnlinkExternalAuthRequest != app.OnRecordBeforeUnlinkExternalAuthRequest() || app.OnRecordBeforeUnlinkExternalAuthRequest() == nil {
|
||||||
|
|
|
@ -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"`
|
||||||
|
}
|
|
@ -52,10 +52,11 @@ type RecordFieldResolver struct {
|
||||||
baseCollection *models.Collection
|
baseCollection *models.Collection
|
||||||
allowHiddenFields bool
|
allowHiddenFields bool
|
||||||
allowedFields []string
|
allowedFields []string
|
||||||
requestData map[string]any
|
|
||||||
loadedCollections []*models.Collection
|
loadedCollections []*models.Collection
|
||||||
joins []join // we cannot use a map because the insertion order is not preserved
|
joins []join // we cannot use a map because the insertion order is not preserved
|
||||||
exprs []dbx.Expression
|
exprs []dbx.Expression
|
||||||
|
requestData *models.FilterRequestData
|
||||||
|
staticRequestData map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`.
|
// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`.
|
||||||
|
@ -66,10 +67,10 @@ type RecordFieldResolver struct {
|
||||||
func NewRecordFieldResolver(
|
func NewRecordFieldResolver(
|
||||||
dao *daos.Dao,
|
dao *daos.Dao,
|
||||||
baseCollection *models.Collection,
|
baseCollection *models.Collection,
|
||||||
requestData map[string]any,
|
requestData *models.FilterRequestData,
|
||||||
allowHiddenFields bool,
|
allowHiddenFields bool,
|
||||||
) *RecordFieldResolver {
|
) *RecordFieldResolver {
|
||||||
return &RecordFieldResolver{
|
r := &RecordFieldResolver{
|
||||||
dao: dao,
|
dao: dao,
|
||||||
baseCollection: baseCollection,
|
baseCollection: baseCollection,
|
||||||
requestData: requestData,
|
requestData: requestData,
|
||||||
|
@ -86,6 +87,22 @@ func NewRecordFieldResolver(
|
||||||
`^\@collection\.\w+\.\w+[\w\.]*$`,
|
`^\@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.
|
// 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)
|
return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.requestData == nil {
|
||||||
|
return "NULL", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// plain @request.* field
|
// plain @request.* field
|
||||||
if !strings.HasPrefix(fieldName, "@request.auth.") || list.ExistInSlice(fieldName, plainRequestAuthFields) {
|
if !strings.HasPrefix(fieldName, "@request.auth.") || list.ExistInSlice(fieldName, plainRequestAuthFields) {
|
||||||
return r.resolveStaticRequestField(props[1:]...)
|
return r.resolveStaticRequestField(props[1:]...)
|
||||||
|
@ -173,28 +194,18 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||||
|
|
||||||
// resolve the auth collection fields
|
// resolve the auth collection fields
|
||||||
// ---
|
// ---
|
||||||
rawAuthRecordId, _ := extractNestedMapVal(r.requestData, "auth", "id")
|
if r.requestData == nil || r.requestData.AuthRecord == nil || r.requestData.AuthRecord.Collection() == nil {
|
||||||
authRecordId := cast.ToString(rawAuthRecordId)
|
|
||||||
if authRecordId == "" {
|
|
||||||
return "NULL", nil, nil
|
return "NULL", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rawAuthCollectionId, _ := extractNestedMapVal(r.requestData, "auth", schema.FieldNameCollectionId)
|
collection := r.requestData.AuthRecord.Collection()
|
||||||
authCollectionId := cast.ToString(rawAuthCollectionId)
|
r.loadedCollections = append(r.loadedCollections, collection)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCollectionName = collection.Name
|
currentCollectionName = collection.Name
|
||||||
currentTableAlias = "__auth_" + inflector.Columnify(currentCollectionName)
|
currentTableAlias = "__auth_" + inflector.Columnify(currentCollectionName)
|
||||||
|
|
||||||
authIdParamKey := "auth" + security.PseudorandomString(5)
|
authIdParamKey := "auth" + security.PseudorandomString(5)
|
||||||
authIdParams := dbx.Params{authIdParamKey: authRecordId}
|
authIdParams := dbx.Params{authIdParamKey: r.requestData.AuthRecord.Id}
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
// join the auth collection
|
// 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) {
|
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||||
// ignore error because requestData is dynamic and some of the
|
// ignore error because requestData is dynamic and some of the
|
||||||
// lookup keys may not be defined for the request
|
// lookup keys may not be defined for the request
|
||||||
resultVal, _ := extractNestedMapVal(r.requestData, path...)
|
resultVal, _ := extractNestedMapVal(r.staticRequestData, path...)
|
||||||
|
|
||||||
switch v := resultVal.(type) {
|
switch v := resultVal.(type) {
|
||||||
case nil:
|
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) {
|
func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models.Collection, error) {
|
||||||
// return already loaded
|
// return already loaded
|
||||||
for _, collection := range r.loadedCollections {
|
for _, collection := range r.loadedCollections {
|
||||||
if collection.Name == collectionNameOrId || collection.Id == collectionNameOrId {
|
if collection.Id == collectionNameOrId || strings.EqualFold(collection.Name, collectionNameOrId) {
|
||||||
return collection, nil
|
return collection, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/resolvers"
|
"github.com/pocketbase/pocketbase/resolvers"
|
||||||
"github.com/pocketbase/pocketbase/tests"
|
"github.com/pocketbase/pocketbase/tests"
|
||||||
"github.com/pocketbase/pocketbase/tools/list"
|
"github.com/pocketbase/pocketbase/tools/list"
|
||||||
|
@ -19,8 +20,8 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestData := map[string]any{
|
requestData := &models.FilterRequestData{
|
||||||
"auth": authRecord.PublicExport(),
|
AuthRecord: authRecord,
|
||||||
}
|
}
|
||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
|
@ -181,8 +182,8 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestData := map[string]any{
|
requestData := &models.FilterRequestData{
|
||||||
"auth": authRecord.PublicExport(),
|
AuthRecord: authRecord,
|
||||||
}
|
}
|
||||||
|
|
||||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
|
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
|
||||||
|
@ -262,16 +263,16 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestData := map[string]any{
|
requestData := &models.FilterRequestData{
|
||||||
"method": "get",
|
Method: "get",
|
||||||
"query": map[string]any{
|
Query: map[string]any{
|
||||||
"a": 123,
|
"a": 123,
|
||||||
},
|
},
|
||||||
"data": map[string]any{
|
Data: map[string]any{
|
||||||
"b": 456,
|
"b": 456,
|
||||||
"c": map[string]int{"sub": 1},
|
"c": map[string]int{"sub": 1},
|
||||||
},
|
},
|
||||||
"user": authRecord.PublicExport(),
|
AuthRecord: authRecord,
|
||||||
}
|
}
|
||||||
|
|
||||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
|
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
|
||||||
|
@ -295,7 +296,11 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||||
{"@request.data.c", false, `"{\"sub\":1}"`},
|
{"@request.data.c", false, `"{\"sub\":1}"`},
|
||||||
{"@request.auth", true, ""},
|
{"@request.auth", true, ""},
|
||||||
{"@request.auth.id", false, `"4q1xlclmfloku33"`},
|
{"@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 {
|
for i, s := range scenarios {
|
||||||
|
@ -315,7 +320,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||||
// ---
|
// ---
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
if name != "NULL" {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
@ -323,7 +328,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||||
// existing key
|
// existing key
|
||||||
// ---
|
// ---
|
||||||
if len(params) != 1 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +345,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||||
|
|
||||||
encodedParamValue, _ := json.Marshal(paramValue)
|
encodedParamValue, _ := json.Marshal(paramValue)
|
||||||
if string(encodedParamValue) != s.expectParamValue {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,8 +162,8 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
t.OnRecordListExternalAuths().Add(func(e *core.RecordListExternalAuthsEvent) error {
|
t.OnRecordListExternalAuthsRequest().Add(func(e *core.RecordListExternalAuthsEvent) error {
|
||||||
t.EventCalls["OnRecordListExternalAuths"]++
|
t.EventCalls["OnRecordListExternalAuthsRequest"]++
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue