soft deprecated apis.RequestData(c) in favor of apis.RequestInfo(c) and updated jsvm bindings

This commit is contained in:
Gani Georgiev 2023-07-17 23:13:39 +03:00
parent 7d4017225c
commit 0110869c89
22 changed files with 3158 additions and 2990 deletions

View File

@ -31,7 +31,7 @@
``` ```
app.Dao().FindRecordsByFilter("posts", "title ~ 'lorem ipsum' && visible = true", "-created", 10) app.Dao().FindRecordsByFilter("posts", "title ~ 'lorem ipsum' && visible = true", "-created", 10)
app.Dao().FindFirstRecordByFilter("posts", "slug='test' && active=true") app.Dao().FindFirstRecordByFilter("posts", "slug='test' && active=true")
app.Dao().CanAccessRecord(record, requestData, rule) app.Dao().CanAccessRecord(record, requestInfo, rule)
``` ```
- (@todo docs) Added `Dao.WithoutHooks()` helper to create a new `Dao` from the current one but without the create/update/delete hooks. - (@todo docs) Added `Dao.WithoutHooks()` helper to create a new `Dao` from the current one but without the create/update/delete hooks.
@ -84,6 +84,9 @@
- (@todo docs) Added `record.ExpandedOne(rel)` and `record.ExpandedAll(rel)` helpers to retrieve casted single or multiple expand relations from the already loaded "expand" Record data. - (@todo docs) Added `record.ExpandedOne(rel)` and `record.ExpandedAll(rel)` helpers to retrieve casted single or multiple expand relations from the already loaded "expand" Record data.
- **!** renamed `models.RequestData` to `models.RequestInfo` and soft-deprecated `apis.RequestData(c)` to `apis.RequestInfo(c)` to avoid the stuttering with the `Data` field.
_The old `apis.RequestData()` method still works to minimize the breaking changes but it is recommended to replace it with `apis.RequestInfo(c)`._
## v0.16.10 ## v0.16.10
@ -92,7 +95,7 @@
## v0.16.9 ## v0.16.9
- Register the `eagerRequestDataCache` middleware only for the internal `api` group routes to avoid conflicts with custom route handlers ([#2914](https://github.com/pocketbase/pocketbase/issues/2914)). - Register the `eagerRequestInfoCache` middleware only for the internal `api` group routes to avoid conflicts with custom route handlers ([#2914](https://github.com/pocketbase/pocketbase/issues/2914)).
## v0.16.8 ## v0.16.8

View File

@ -120,7 +120,7 @@ func InitApi(app core.App) (*echo.Echo, error) {
bindStaticAdminUI(app, e) bindStaticAdminUI(app, e)
// default routes // default routes
api := e.Group("/api", eagerRequestDataCache(app)) api := e.Group("/api", eagerRequestInfoCache(app))
bindSettingsApi(app, api) bindSettingsApi(app, api)
bindAdminApi(app, api) bindAdminApi(app, api)
bindCollectionApi(app, api) bindCollectionApi(app, api)

View File

@ -213,7 +213,7 @@ func TestRemoveTrailingSlashMiddleware(t *testing.T) {
} }
} }
func TestEagerRequestDataCache(t *testing.T) { func TestEagerRequestInfoCache(t *testing.T) {
scenarios := []tests.ApiScenario{ scenarios := []tests.ApiScenario{
{ {
@ -236,7 +236,7 @@ func TestEagerRequestDataCache(t *testing.T) {
// since the unknown method is not eager cache support // since the unknown method is not eager cache support
// it should fail reading the json body twice // it should fail reading the json body twice
r := apis.RequestData(c) r := apis.RequestInfo(c)
if v := cast.ToString(r.Data["name"]); v != "" { if v := cast.ToString(r.Data["name"]); v != "" {
t.Fatalf("Expected empty request data body, got, %v", r.Data) t.Fatalf("Expected empty request data body, got, %v", r.Data)
} }
@ -256,7 +256,7 @@ func TestEagerRequestDataCache(t *testing.T) {
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 {
// it is not important whether the route handler return an error since // it is not important whether the route handler return an error since
// we just need to ensure that the eagerRequestDataCache was registered // we just need to ensure that the eagerRequestInfoCache was registered
next(c) next(c)
// ensure that the body was read at least once // ensure that the body was read at least once
@ -267,7 +267,7 @@ func TestEagerRequestDataCache(t *testing.T) {
// since the unknown method is not eager cache support // since the unknown method is not eager cache support
// it should fail reading the json body twice // it should fail reading the json body twice
r := apis.RequestData(c) r := apis.RequestInfo(c)
if v := cast.ToString(r.Data["name"]); v != "" { if v := cast.ToString(r.Data["name"]); v != "" {
t.Fatalf("Expected empty request data body, got, %v", r.Data) t.Fatalf("Expected empty request data body, got, %v", r.Data)
} }
@ -287,7 +287,7 @@ func TestEagerRequestDataCache(t *testing.T) {
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 {
// it is not important whether the route handler return an error since // it is not important whether the route handler return an error since
// we just need to ensure that the eagerRequestDataCache was registered // we just need to ensure that the eagerRequestInfoCache was registered
next(c) next(c)
// ensure that the body was read at least once // ensure that the body was read at least once
@ -297,7 +297,7 @@ func TestEagerRequestDataCache(t *testing.T) {
c.Bind(data) c.Bind(data)
// try to read the body again // try to read the body again
r := apis.RequestData(c) r := apis.RequestInfo(c)
fmt.Println(r) fmt.Println(r)
if v := cast.ToString(r.Data["name"]); v != "test123" { if v := cast.ToString(r.Data["name"]); v != "test123" {
t.Fatalf("Expected request data with name %q, got, %q", "test123", v) t.Fatalf("Expected request data with name %q, got, %q", "test123", v)

View File

@ -95,18 +95,18 @@ func (api *fileApi) download(c echo.Context) error {
adminOrAuthRecord, _ := api.findAdminOrAuthRecordByFileToken(token) adminOrAuthRecord, _ := api.findAdminOrAuthRecordByFileToken(token)
// create a copy of the cached request data and adjust it for the current auth model // create a copy of the cached request data and adjust it for the current auth model
requestData := *RequestData(c) requestInfo := *RequestInfo(c)
requestData.Admin = nil requestInfo.Admin = nil
requestData.AuthRecord = nil requestInfo.AuthRecord = nil
if adminOrAuthRecord != nil { if adminOrAuthRecord != nil {
if admin, _ := adminOrAuthRecord.(*models.Admin); admin != nil { if admin, _ := adminOrAuthRecord.(*models.Admin); admin != nil {
requestData.Admin = admin requestInfo.Admin = admin
} else if record, _ := adminOrAuthRecord.(*models.Record); record != nil { } else if record, _ := adminOrAuthRecord.(*models.Record); record != nil {
requestData.AuthRecord = record requestInfo.AuthRecord = record
} }
} }
if ok, _ := api.app.Dao().CanAccessRecord(record, &requestData, record.Collection().ViewRule); !ok { if ok, _ := api.app.Dao().CanAccessRecord(record, &requestInfo, record.Collection().ViewRule); !ok {
return NewForbiddenError("Insufficient permissions to access the file resource.", nil) return NewForbiddenError("Insufficient permissions to access the file resource.", nil)
} }
} }

View File

@ -393,15 +393,15 @@ func realUserIp(r *http.Request, fallbackIp string) string {
return fallbackIp return fallbackIp
} }
// eagerRequestDataCache 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 eagerRequestDataCache(app core.App) echo.MiddlewareFunc { func eagerRequestInfoCache(app core.App) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
switch c.Request().Method { switch c.Request().Method {
// currently we are eagerly caching only the requests with body // currently we are eagerly caching only the requests with body
case "POST", "PUT", "PATCH", "DELETE": case "POST", "PUT", "PATCH", "DELETE":
RequestData(c) RequestInfo(c)
} }
return next(c) return next(c)

View File

@ -347,12 +347,12 @@ func (api *realtimeApi) canAccessRecord(client subscriptions.Client, record *mod
} }
// mock request data // mock request data
requestData := &models.RequestData{ requestInfo := &models.RequestInfo{
Method: "GET", Method: "GET",
} }
requestData.AuthRecord, _ = client.Get(ContextAuthRecordKey).(*models.Record) requestInfo.AuthRecord, _ = client.Get(ContextAuthRecordKey).(*models.Record)
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestData, true) resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestInfo, true)
expr, err := search.FilterData(*accessRule).BuildExpr(resolver) expr, err := search.FilterData(*accessRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err

View File

@ -191,8 +191,8 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
return createForm.DrySubmit(func(txDao *daos.Dao) error { return createForm.DrySubmit(func(txDao *daos.Dao) error {
event.IsNewRecord = true event.IsNewRecord = true
// clone the current request data and assign the form create data as its body data // clone the current request data and assign the form create data as its body data
requestData := *RequestData(c) requestInfo := *RequestInfo(c)
requestData.Data = form.CreateData requestInfo.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)
@ -205,7 +205,7 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
} }
if *collection.CreateRule != "" { if *collection.CreateRule != "" {
resolver := resolvers.NewRecordFieldResolver(txDao, collection, &requestData, true) resolver := resolvers.NewRecordFieldResolver(txDao, collection, &requestInfo, true)
expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver) expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err

View File

@ -50,9 +50,9 @@ func (api *recordApi) list(c echo.Context) error {
return err return err
} }
requestData := RequestData(c) requestInfo := RequestInfo(c)
if requestData.Admin == nil && collection.ListRule == nil { if requestInfo.Admin == nil && collection.ListRule == 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)
} }
@ -60,9 +60,9 @@ func (api *recordApi) list(c echo.Context) error {
fieldsResolver := resolvers.NewRecordFieldResolver( fieldsResolver := resolvers.NewRecordFieldResolver(
api.app.Dao(), api.app.Dao(),
collection, collection,
requestData, requestInfo,
// hidden fields are searchable only by admins // hidden fields are searchable only by admins
requestData.Admin != nil, requestInfo.Admin != nil,
) )
searchProvider := search.NewProvider(fieldsResolver). searchProvider := search.NewProvider(fieldsResolver).
@ -73,7 +73,7 @@ func (api *recordApi) list(c echo.Context) error {
searchProvider.CountCol("id") searchProvider.CountCol("id")
} }
if requestData.Admin == nil && collection.ListRule != nil { if requestInfo.Admin == nil && collection.ListRule != nil {
searchProvider.AddFilter(search.FilterData(*collection.ListRule)) searchProvider.AddFilter(search.FilterData(*collection.ListRule))
} }
@ -110,16 +110,16 @@ func (api *recordApi) view(c echo.Context) error {
return NewNotFoundError("", nil) return NewNotFoundError("", nil)
} }
requestData := RequestData(c) requestInfo := RequestInfo(c)
if requestData.Admin == nil && collection.ViewRule == nil { if requestInfo.Admin == nil && collection.ViewRule == 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)
} }
ruleFunc := func(q *dbx.SelectQuery) error { ruleFunc := func(q *dbx.SelectQuery) error {
if requestData.Admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" { if requestInfo.Admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true) resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver) expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err
@ -155,23 +155,23 @@ func (api *recordApi) create(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.") return NewNotFoundError("", "Missing collection context.")
} }
requestData := RequestData(c) requestInfo := RequestInfo(c)
if requestData.Admin == nil && collection.CreateRule == nil { if requestInfo.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)
} }
hasFullManageAccess := requestData.Admin != nil hasFullManageAccess := requestInfo.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 requestData.Admin == nil && collection.CreateRule != nil { if requestInfo.Admin == nil && collection.CreateRule != nil {
testRecord := models.NewRecord(collection) testRecord := models.NewRecord(collection)
// replace modifiers fields so that the resolved value is always // replace modifiers fields so that the resolved value is always
// available when accessing requestData.Data using just the field name // available when accessing requestInfo.Data using just the field name
if requestData.HasModifierDataKeys() { if requestInfo.HasModifierDataKeys() {
requestData.Data = testRecord.ReplaceModifers(requestData.Data) requestInfo.Data = testRecord.ReplaceModifers(requestInfo.Data)
} }
testForm := forms.NewRecordUpsert(api.app, testRecord) testForm := forms.NewRecordUpsert(api.app, testRecord)
@ -185,7 +185,7 @@ func (api *recordApi) create(c echo.Context) error {
return nil // no create rule to resolve return nil // no create rule to resolve
} }
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true) resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver) expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err
@ -200,7 +200,7 @@ func (api *recordApi) create(c echo.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("DrySubmit create rule failure: %w", err) return fmt.Errorf("DrySubmit create rule failure: %w", err)
} }
hasFullManageAccess = hasAuthManageAccess(txDao, foundRecord, requestData) hasFullManageAccess = hasAuthManageAccess(txDao, foundRecord, requestInfo)
return nil return nil
}) })
@ -259,26 +259,26 @@ func (api *recordApi) update(c echo.Context) error {
return NewNotFoundError("", nil) return NewNotFoundError("", nil)
} }
requestData := RequestData(c) requestInfo := RequestInfo(c)
if requestData.Admin == nil && collection.UpdateRule == nil { if requestInfo.Admin == nil && collection.UpdateRule == 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)
} }
// eager fetch the record so that the modifier field values are replaced // eager fetch the record so that the modifier field values are replaced
// and available when accessing requestData.Data using just the field name // and available when accessing requestInfo.Data using just the field name
if requestData.HasModifierDataKeys() { if requestInfo.HasModifierDataKeys() {
record, err := api.app.Dao().FindRecordById(collection.Id, recordId) record, err := api.app.Dao().FindRecordById(collection.Id, recordId)
if err != nil || record == nil { if err != nil || record == nil {
return NewNotFoundError("", err) return NewNotFoundError("", err)
} }
requestData.Data = record.ReplaceModifers(requestData.Data) requestInfo.Data = record.ReplaceModifers(requestInfo.Data)
} }
ruleFunc := func(q *dbx.SelectQuery) error { ruleFunc := func(q *dbx.SelectQuery) error {
if requestData.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" { if requestInfo.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true) resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver) expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err
@ -296,7 +296,7 @@ func (api *recordApi) update(c echo.Context) error {
} }
form := forms.NewRecordUpsert(api.app, record) form := forms.NewRecordUpsert(api.app, record)
form.SetFullManageAccess(requestData.Admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestData)) form.SetFullManageAccess(requestInfo.Admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestInfo))
// load request // load request
if err := form.LoadRequest(c.Request(), ""); err != nil { if err := form.LoadRequest(c.Request(), ""); err != nil {
@ -344,16 +344,16 @@ func (api *recordApi) delete(c echo.Context) error {
return NewNotFoundError("", nil) return NewNotFoundError("", nil)
} }
requestData := RequestData(c) requestInfo := RequestInfo(c)
if requestData.Admin == nil && collection.DeleteRule == nil { if requestInfo.Admin == nil && collection.DeleteRule == 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)
} }
ruleFunc := func(q *dbx.SelectQuery) error { ruleFunc := func(q *dbx.SelectQuery) error {
if requestData.Admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" { if requestInfo.Admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true) resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestInfo, true)
expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver) expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err

View File

@ -17,14 +17,20 @@ import (
"github.com/pocketbase/pocketbase/tools/search" "github.com/pocketbase/pocketbase/tools/search"
) )
const ContextRequestDataKey = "requestData" const ContextRequestInfoKey = "requestInfo"
// RequestData exports cached common request data fields // Deprecated: Use RequestInfo instead.
func RequestData(c echo.Context) *models.RequestInfo {
log.Println("RequestInfo(c) is depracated and will be removed in the future! You can replace it with RequestInfo(c).")
return RequestInfo(c)
}
// RequestInfo exports cached common request data fields
// (query, body, logged auth state, etc.) from the provided context. // (query, body, logged auth state, etc.) from the provided context.
func RequestData(c echo.Context) *models.RequestData { func RequestInfo(c echo.Context) *models.RequestInfo {
// return cached to avoid copying the body multiple times // return cached to avoid copying the body multiple times
if v := c.Get(ContextRequestDataKey); v != nil { if v := c.Get(ContextRequestInfoKey); v != nil {
if data, ok := v.(*models.RequestData); ok { if data, ok := v.(*models.RequestInfo); ok {
// refresh auth state // refresh auth state
data.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record) data.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record)
data.Admin, _ = c.Get(ContextAdminKey).(*models.Admin) data.Admin, _ = c.Get(ContextAdminKey).(*models.Admin)
@ -32,7 +38,7 @@ func RequestData(c echo.Context) *models.RequestData {
} }
} }
result := &models.RequestData{ result := &models.RequestInfo{
Method: c.Request().Method, Method: c.Request().Method,
Query: map[string]any{}, Query: map[string]any{},
Data: map[string]any{}, Data: map[string]any{},
@ -52,7 +58,7 @@ func RequestData(c echo.Context) *models.RequestData {
echo.BindQueryParams(c, &result.Query) echo.BindQueryParams(c, &result.Query)
rest.BindBody(c, &result.Data) rest.BindBody(c, &result.Data)
c.Set(ContextRequestDataKey, result) c.Set(ContextRequestInfoKey, result)
return result return result
} }
@ -86,13 +92,13 @@ func RecordAuthResponse(
expands := strings.Split(c.QueryParam(expandQueryParam), ",") expands := strings.Split(c.QueryParam(expandQueryParam), ",")
if len(expands) > 0 { if len(expands) > 0 {
// create a copy of the cached request data and adjust it to the current auth record // create a copy of the cached request data and adjust it to the current auth record
requestData := *RequestData(e.HttpContext) requestInfo := *RequestInfo(e.HttpContext)
requestData.Admin = nil requestInfo.Admin = nil
requestData.AuthRecord = e.Record requestInfo.AuthRecord = e.Record
failed := app.Dao().ExpandRecord( failed := app.Dao().ExpandRecord(
e.Record, e.Record,
expands, expands,
expandFetch(app.Dao(), &requestData), expandFetch(app.Dao(), &requestInfo),
) )
if len(failed) > 0 && app.IsDebug() { if len(failed) > 0 && app.IsDebug() {
log.Println("Failed to expand relations: ", failed) log.Println("Failed to expand relations: ", failed)
@ -131,9 +137,9 @@ func EnrichRecord(c echo.Context, dao *daos.Dao, record *models.Record, defaultE
// - ensures that the emails of the auth records and their expanded auth relations // - 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 // 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 { func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defaultExpands ...string) error {
requestData := RequestData(c) requestInfo := RequestInfo(c)
if err := autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData); err != nil { if err := autoIgnoreAuthRecordsEmailVisibility(dao, records, requestInfo); err != nil {
return fmt.Errorf("Failed to resolve email visibility: %w", err) return fmt.Errorf("Failed to resolve email visibility: %w", err)
} }
@ -145,7 +151,7 @@ func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defa
return nil // nothing to expand return nil // nothing to expand
} }
errs := dao.ExpandRecords(records, expands, expandFetch(dao, requestData)) errs := dao.ExpandRecords(records, expands, expandFetch(dao, requestInfo))
if len(errs) > 0 { if len(errs) > 0 {
return fmt.Errorf("Failed to expand: %v", errs) return fmt.Errorf("Failed to expand: %v", errs)
} }
@ -156,11 +162,11 @@ func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defa
// 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,
requestData *models.RequestData, requestInfo *models.RequestInfo,
) 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 requestData.Admin != nil { if requestInfo.Admin != nil {
return nil // admins can access everything return nil // admins can access everything
} }
@ -169,7 +175,7 @@ func expandFetch(
} }
if *relCollection.ViewRule != "" { if *relCollection.ViewRule != "" {
resolver := resolvers.NewRecordFieldResolver(dao, relCollection, requestData, true) resolver := resolvers.NewRecordFieldResolver(dao, relCollection, requestInfo, true)
expr, err := search.FilterData(*(relCollection.ViewRule)).BuildExpr(resolver) expr, err := search.FilterData(*(relCollection.ViewRule)).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err
@ -182,7 +188,7 @@ func expandFetch(
}) })
if err == nil && len(records) > 0 { if err == nil && len(records) > 0 {
autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData) autoIgnoreAuthRecordsEmailVisibility(dao, records, requestInfo)
} }
return records, err return records, err
@ -196,13 +202,13 @@ func expandFetch(
func autoIgnoreAuthRecordsEmailVisibility( func autoIgnoreAuthRecordsEmailVisibility(
dao *daos.Dao, dao *daos.Dao,
records []*models.Record, records []*models.Record,
requestData *models.RequestData, requestInfo *models.RequestInfo,
) 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 requestData.Admin != nil { if requestInfo.Admin != nil {
for _, rec := range records { for _, rec := range records {
rec.IgnoreEmailVisibility(true) rec.IgnoreEmailVisibility(true)
} }
@ -218,8 +224,8 @@ func autoIgnoreAuthRecordsEmailVisibility(
recordIds[i] = rec.Id recordIds[i] = rec.Id
} }
if requestData != nil && requestData.AuthRecord != nil && mappedRecords[requestData.AuthRecord.Id] != nil { if requestInfo != nil && requestInfo.AuthRecord != nil && mappedRecords[requestInfo.AuthRecord.Id] != nil {
mappedRecords[requestData.AuthRecord.Id].IgnoreEmailVisibility(true) mappedRecords[requestInfo.AuthRecord.Id].IgnoreEmailVisibility(true)
} }
authOptions := collection.AuthOptions() authOptions := collection.AuthOptions()
@ -235,7 +241,7 @@ func autoIgnoreAuthRecordsEmailVisibility(
Select(dao.DB().QuoteSimpleColumnName(collection.Name) + ".id"). Select(dao.DB().QuoteSimpleColumnName(collection.Name) + ".id").
AndWhere(dbx.In(dao.DB().QuoteSimpleColumnName(collection.Name)+".id", recordIds...)) AndWhere(dbx.In(dao.DB().QuoteSimpleColumnName(collection.Name)+".id", recordIds...))
resolver := resolvers.NewRecordFieldResolver(dao, collection, requestData, true) resolver := resolvers.NewRecordFieldResolver(dao, collection, requestInfo, true)
expr, err := search.FilterData(*authOptions.ManageRule).BuildExpr(resolver) expr, err := search.FilterData(*authOptions.ManageRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err
@ -264,7 +270,7 @@ func autoIgnoreAuthRecordsEmailVisibility(
func hasAuthManageAccess( func hasAuthManageAccess(
dao *daos.Dao, dao *daos.Dao,
record *models.Record, record *models.Record,
requestData *models.RequestData, requestInfo *models.RequestInfo,
) bool { ) bool {
if !record.Collection().IsAuth() { if !record.Collection().IsAuth() {
return false return false
@ -276,12 +282,12 @@ func hasAuthManageAccess(
return false // only for admins (manageRule can't be empty) return false // only for admins (manageRule can't be empty)
} }
if requestData == nil || requestData.AuthRecord == nil { if requestInfo == nil || requestInfo.AuthRecord == nil {
return false // no auth record return false // no auth record
} }
ruleFunc := func(q *dbx.SelectQuery) error { ruleFunc := func(q *dbx.SelectQuery) error {
resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestData, true) resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestInfo, true)
expr, err := search.FilterData(*manageRule).BuildExpr(resolver) expr, err := search.FilterData(*manageRule).BuildExpr(resolver)
if err != nil { if err != nil {
return err return err

View File

@ -13,7 +13,7 @@ import (
"github.com/pocketbase/pocketbase/tests" "github.com/pocketbase/pocketbase/tests"
) )
func TestRequestData(t *testing.T) { func TestRequestInfo(t *testing.T) {
e := echo.New() e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/?test=123", strings.NewReader(`{"test":456}`)) req := httptest.NewRequest(http.MethodPost, "/?test=123", strings.NewReader(`{"test":456}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
@ -29,10 +29,10 @@ func TestRequestData(t *testing.T) {
dummyAdmin.Id = "id2" dummyAdmin.Id = "id2"
c.Set(apis.ContextAdminKey, dummyAdmin) c.Set(apis.ContextAdminKey, dummyAdmin)
result := apis.RequestData(c) result := apis.RequestInfo(c)
if result == nil { if result == nil {
t.Fatal("Expected *models.RequestData instance, got nil") t.Fatal("Expected *models.RequestInfo instance, got nil")
} }
if result.Method != http.MethodPost { if result.Method != http.MethodPost {

View File

@ -520,9 +520,9 @@ func (dao *Dao) SuggestUniqueAuthRecordUsername(
} }
// CanAccessRecord checks if a record is allowed to be accessed by the // CanAccessRecord checks if a record is allowed to be accessed by the
// specified requestData and accessRule. // specified requestInfo and accessRule.
// //
// Rule and db checks are ignored in case requestData.Admin is set. // Rule and db checks are ignored in case requestInfo.Admin is set.
// //
// The returned error indicate that something unexpected happened during // The returned error indicate that something unexpected happened during
// the check (eg. invalid rule or db error). // the check (eg. invalid rule or db error).
@ -531,14 +531,14 @@ func (dao *Dao) SuggestUniqueAuthRecordUsername(
// //
// Example: // Example:
// //
// requestData := apis.RequestData(c /* echo.Context */) // requestInfo := apis.RequestInfo(c /* echo.Context */)
// record, _ := dao.FindRecordById("example", "RECORD_ID") // record, _ := dao.FindRecordById("example", "RECORD_ID")
// rule := types.Pointer("@request.auth.id != '' || status = 'public'") // rule := types.Pointer("@request.auth.id != '' || status = 'public'")
// // ... or use one of the record collection's rule, eg. record.Collection().ViewRule // // ... or use one of the record collection's rule, eg. record.Collection().ViewRule
// //
// if ok, _ := dao.CanAccessRecord(record, requestData, rule); ok { ... } // if ok, _ := dao.CanAccessRecord(record, requestInfo, rule); ok { ... }
func (dao *Dao) CanAccessRecord(record *models.Record, requestData *models.RequestData, accessRule *string) (bool, error) { func (dao *Dao) CanAccessRecord(record *models.Record, requestInfo *models.RequestInfo, accessRule *string) (bool, error) {
if requestData.Admin != nil { if requestInfo.Admin != nil {
// admins can access everything // admins can access everything
return true, nil return true, nil
} }
@ -560,7 +560,7 @@ func (dao *Dao) CanAccessRecord(record *models.Record, requestData *models.Reque
AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id}) AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id})
// parse and apply the access rule filter // parse and apply the access rule filter
resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestData, true) resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestInfo, true)
expr, err := search.FilterData(*accessRule).BuildExpr(resolver) expr, err := search.FilterData(*accessRule).BuildExpr(resolver)
if err != nil { if err != nil {
return false, err return false, err

View File

@ -625,7 +625,7 @@ func TestCanAccessRecord(t *testing.T) {
scenarios := []struct { scenarios := []struct {
name string name string
record *models.Record record *models.Record
requestData *models.RequestData requestInfo *models.RequestInfo
rule *string rule *string
expected bool expected bool
expectError bool expectError bool
@ -633,7 +633,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as admin with nil rule", "as admin with nil rule",
record, record,
&models.RequestData{ &models.RequestInfo{
Admin: admin, Admin: admin,
}, },
nil, nil,
@ -643,7 +643,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as admin with non-empty rule", "as admin with non-empty rule",
record, record,
&models.RequestData{ &models.RequestInfo{
Admin: admin, Admin: admin,
}, },
types.Pointer("id = ''"), // the filter rule should be ignored types.Pointer("id = ''"), // the filter rule should be ignored
@ -653,7 +653,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as admin with invalid rule", "as admin with invalid rule",
record, record,
&models.RequestData{ &models.RequestInfo{
Admin: admin, Admin: admin,
}, },
types.Pointer("id ?!@ 1"), // the filter rule should be ignored types.Pointer("id ?!@ 1"), // the filter rule should be ignored
@ -663,7 +663,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as guest with nil rule", "as guest with nil rule",
record, record,
&models.RequestData{}, &models.RequestInfo{},
nil, nil,
false, false,
false, false,
@ -671,7 +671,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as guest with empty rule", "as guest with empty rule",
record, record,
&models.RequestData{}, &models.RequestInfo{},
types.Pointer(""), types.Pointer(""),
true, true,
false, false,
@ -679,7 +679,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as guest with invalid rule", "as guest with invalid rule",
record, record,
&models.RequestData{}, &models.RequestInfo{},
types.Pointer("id ?!@ 1"), types.Pointer("id ?!@ 1"),
false, false,
true, true,
@ -687,7 +687,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as guest with mismatched rule", "as guest with mismatched rule",
record, record,
&models.RequestData{}, &models.RequestInfo{},
types.Pointer("@request.auth.id != ''"), types.Pointer("@request.auth.id != ''"),
false, false,
false, false,
@ -695,7 +695,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as guest with matched rule", "as guest with matched rule",
record, record,
&models.RequestData{ &models.RequestInfo{
Data: map[string]any{"test": 1}, Data: map[string]any{"test": 1},
}, },
types.Pointer("@request.auth.id != '' || @request.data.test = 1"), types.Pointer("@request.auth.id != '' || @request.data.test = 1"),
@ -705,7 +705,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as auth record with nil rule", "as auth record with nil rule",
record, record,
&models.RequestData{ &models.RequestInfo{
AuthRecord: authRecord, AuthRecord: authRecord,
}, },
nil, nil,
@ -715,7 +715,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as auth record with empty rule", "as auth record with empty rule",
record, record,
&models.RequestData{ &models.RequestInfo{
AuthRecord: authRecord, AuthRecord: authRecord,
}, },
types.Pointer(""), types.Pointer(""),
@ -725,7 +725,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as auth record with invalid rule", "as auth record with invalid rule",
record, record,
&models.RequestData{ &models.RequestInfo{
AuthRecord: authRecord, AuthRecord: authRecord,
}, },
types.Pointer("id ?!@ 1"), types.Pointer("id ?!@ 1"),
@ -735,7 +735,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as auth record with mismatched rule", "as auth record with mismatched rule",
record, record,
&models.RequestData{ &models.RequestInfo{
AuthRecord: authRecord, AuthRecord: authRecord,
Data: map[string]any{"test": 1}, Data: map[string]any{"test": 1},
}, },
@ -746,7 +746,7 @@ func TestCanAccessRecord(t *testing.T) {
{ {
"as auth record with matched rule", "as auth record with matched rule",
record, record,
&models.RequestData{ &models.RequestInfo{
AuthRecord: authRecord, AuthRecord: authRecord,
Data: map[string]any{"test": 2}, Data: map[string]any{"test": 2},
}, },
@ -757,7 +757,7 @@ func TestCanAccessRecord(t *testing.T) {
} }
for _, s := range scenarios { for _, s := range scenarios {
result, err := app.Dao().CanAccessRecord(s.record, s.requestData, s.rule) result, err := app.Dao().CanAccessRecord(s.record, s.requestInfo, s.rule)
if result != s.expected { if result != s.expected {
t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result) t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result)

View File

@ -117,7 +117,7 @@ func (form *RecordUpsert) getContentType(r *http.Request) string {
return t return t
} }
func (form *RecordUpsert) extractRequestData( func (form *RecordUpsert) extractRequestInfo(
r *http.Request, r *http.Request,
keyPrefix string, keyPrefix string,
) (map[string]any, map[string][]*filesystem.File, error) { ) (map[string]any, map[string][]*filesystem.File, error) {
@ -219,12 +219,12 @@ func (form *RecordUpsert) extractMultipartFormData(
// //
// File upload is supported only via multipart/form-data. // File upload is supported only via multipart/form-data.
func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error { func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error {
requestData, uploadedFiles, err := form.extractRequestData(r, keyPrefix) requestInfo, uploadedFiles, err := form.extractRequestInfo(r, keyPrefix)
if err != nil { if err != nil {
return err return err
} }
if err := form.LoadData(requestData); err != nil { if err := form.LoadData(requestInfo); err != nil {
return err return err
} }
@ -349,39 +349,39 @@ func (form *RecordUpsert) RemoveFiles(key string, toDelete ...string) error {
} }
// LoadData loads and normalizes the provided regular record data fields into the form. // LoadData loads and normalizes the provided regular record data fields into the form.
func (form *RecordUpsert) LoadData(requestData map[string]any) error { func (form *RecordUpsert) LoadData(requestInfo map[string]any) error {
// load base system fields // load base system fields
if v, ok := requestData[schema.FieldNameId]; ok { if v, ok := requestInfo[schema.FieldNameId]; ok {
form.Id = cast.ToString(v) form.Id = cast.ToString(v)
} }
// load auth system fields // load auth system fields
if form.record.Collection().IsAuth() { if form.record.Collection().IsAuth() {
if v, ok := requestData[schema.FieldNameUsername]; ok { if v, ok := requestInfo[schema.FieldNameUsername]; ok {
form.Username = cast.ToString(v) form.Username = cast.ToString(v)
} }
if v, ok := requestData[schema.FieldNameEmail]; ok { if v, ok := requestInfo[schema.FieldNameEmail]; ok {
form.Email = cast.ToString(v) form.Email = cast.ToString(v)
} }
if v, ok := requestData[schema.FieldNameEmailVisibility]; ok { if v, ok := requestInfo[schema.FieldNameEmailVisibility]; ok {
form.EmailVisibility = cast.ToBool(v) form.EmailVisibility = cast.ToBool(v)
} }
if v, ok := requestData[schema.FieldNameVerified]; ok { if v, ok := requestInfo[schema.FieldNameVerified]; ok {
form.Verified = cast.ToBool(v) form.Verified = cast.ToBool(v)
} }
if v, ok := requestData["password"]; ok { if v, ok := requestInfo["password"]; ok {
form.Password = cast.ToString(v) form.Password = cast.ToString(v)
} }
if v, ok := requestData["passwordConfirm"]; ok { if v, ok := requestInfo["passwordConfirm"]; ok {
form.PasswordConfirm = cast.ToString(v) form.PasswordConfirm = cast.ToString(v)
} }
if v, ok := requestData["oldPassword"]; ok { if v, ok := requestInfo["oldPassword"]; ok {
form.OldPassword = cast.ToString(v) form.OldPassword = cast.ToString(v)
} }
} }
// replace modifiers (if any) // replace modifiers (if any)
requestData = form.record.ReplaceModifers(requestData) requestInfo = form.record.ReplaceModifers(requestInfo)
// create a shallow copy of form.data // create a shallow copy of form.data
var extendedData = make(map[string]any, len(form.data)) var extendedData = make(map[string]any, len(form.data))
@ -390,7 +390,7 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
} }
// extend form.data with the request data // extend form.data with the request data
rawData, err := json.Marshal(requestData) rawData, err := json.Marshal(requestInfo)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,12 +6,11 @@ import (
"github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/models/schema"
) )
// RequestData defines a HTTP request data struct, usually used // RequestInfo defines a HTTP request data struct, usually used
// as part of the `@request.*` filter resolver. // as part of the `@request.*` filter resolver.
type RequestData struct { type RequestInfo struct {
Method string `json:"method"` Method string `json:"method"`
Query map[string]any `json:"query"` Query map[string]any `json:"query"`
// @todo consider changing to Body?
Data map[string]any `json:"data"` Data map[string]any `json:"data"`
Headers map[string]any `json:"headers"` Headers map[string]any `json:"headers"`
AuthRecord *Record `json:"authRecord"` AuthRecord *Record `json:"authRecord"`
@ -19,7 +18,7 @@ type RequestData struct {
} }
// HasModifierDataKeys loosely checks if the current struct has any modifier Data keys. // HasModifierDataKeys loosely checks if the current struct has any modifier Data keys.
func (r *RequestData) HasModifierDataKeys() bool { func (r *RequestInfo) HasModifierDataKeys() bool {
allModifiers := schema.FieldValueModifiers() allModifiers := schema.FieldValueModifiers()
for key := range r.Data { for key := range r.Data {

View File

@ -6,20 +6,20 @@ import (
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
) )
func TestRequestDataHasModifierDataKeys(t *testing.T) { func TestRequestInfoHasModifierDataKeys(t *testing.T) {
scenarios := []struct { scenarios := []struct {
name string name string
requestData *models.RequestData requestInfo *models.RequestInfo
expected bool expected bool
}{ }{
{ {
"empty", "empty",
&models.RequestData{}, &models.RequestInfo{},
false, false,
}, },
{ {
"Data with regular fields", "Data with regular fields",
&models.RequestData{ &models.RequestInfo{
Query: map[string]any{"data+": "demo"}, // should be ignored Query: map[string]any{"data+": "demo"}, // should be ignored
Data: map[string]any{"a": 123, "b": "test", "c.d": false}, Data: map[string]any{"a": 123, "b": "test", "c.d": false},
}, },
@ -27,21 +27,21 @@ func TestRequestDataHasModifierDataKeys(t *testing.T) {
}, },
{ {
"Data with +modifier fields", "Data with +modifier fields",
&models.RequestData{ &models.RequestInfo{
Data: map[string]any{"a+": 123, "b": "test", "c.d": false}, Data: map[string]any{"a+": 123, "b": "test", "c.d": false},
}, },
true, true,
}, },
{ {
"Data with -modifier fields", "Data with -modifier fields",
&models.RequestData{ &models.RequestInfo{
Data: map[string]any{"a": 123, "b-": "test", "c.d": false}, Data: map[string]any{"a": 123, "b-": "test", "c.d": false},
}, },
true, true,
}, },
{ {
"Data with mixed modifier fields", "Data with mixed modifier fields",
&models.RequestData{ &models.RequestInfo{
Data: map[string]any{"a": 123, "b-": "test", "c.d+": false}, Data: map[string]any{"a": 123, "b-": "test", "c.d+": false},
}, },
true, true,
@ -49,7 +49,7 @@ func TestRequestDataHasModifierDataKeys(t *testing.T) {
} }
for _, s := range scenarios { for _, s := range scenarios {
result := s.requestData.HasModifierDataKeys() result := s.requestInfo.HasModifierDataKeys()
if result != s.expected { if result != s.expected {
t.Fatalf("[%s] Expected %v, got %v", s.name, s.expected, result) t.Fatalf("[%s] Expected %v, got %v", s.name, s.expected, result)

View File

@ -319,6 +319,25 @@ func baseBinds(vm *goja.Runtime) {
return structConstructor(vm, call, instance) return structConstructor(vm, call, instance)
}) })
vm.Set("RequestInfo", func(call goja.ConstructorCall) *goja.Object {
instance := &models.RequestInfo{}
return structConstructor(vm, call, instance)
})
vm.Set("DateTime", func(call goja.ConstructorCall) *goja.Object {
instance := types.NowDateTime()
val, _ := call.Argument(0).Export().(string)
if val != "" {
instance, _ = types.ParseDateTime(val)
}
instanceValue := vm.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return structConstructor(vm, call, instance)
})
vm.Set("ValidationError", func(call goja.ConstructorCall) *goja.Object { vm.Set("ValidationError", func(call goja.ConstructorCall) *goja.Object {
code, _ := call.Argument(0).Export().(string) code, _ := call.Argument(0).Export().(string)
message, _ := call.Argument(1).Export().(string) message, _ := call.Argument(1).Export().(string)
@ -462,7 +481,7 @@ func apisBinds(vm *goja.Runtime) {
obj.Set("activityLogger", apis.ActivityLogger) obj.Set("activityLogger", apis.ActivityLogger)
// record helpers // record helpers
obj.Set("requestData", apis.RequestData) obj.Set("requestInfo", apis.RequestInfo)
obj.Set("recordAuthResponse", apis.RecordAuthResponse) obj.Set("recordAuthResponse", apis.RecordAuthResponse)
obj.Set("enrichRecord", apis.EnrichRecord) obj.Set("enrichRecord", apis.EnrichRecord)
obj.Set("enrichRecords", apis.EnrichRecords) obj.Set("enrichRecords", apis.EnrichRecords)

View File

@ -45,7 +45,7 @@ func TestBaseBindsCount(t *testing.T) {
vm := goja.New() vm := goja.New()
baseBinds(vm) baseBinds(vm)
testBindsCount(vm, "this", 14, t) testBindsCount(vm, "this", 16, t)
} }
func TestBaseBindsRecord(t *testing.T) { func TestBaseBindsRecord(t *testing.T) {
@ -248,6 +248,50 @@ func TestBaseBindsCommand(t *testing.T) {
} }
} }
func TestBaseBindsRequestInfo(t *testing.T) {
vm := goja.New()
baseBinds(vm)
_, err := vm.RunString(`
let info = new RequestInfo({
admin: new Admin({id: "test1"}),
data: {"name": "test2"}
});
if (info.admin?.id != "test1") {
throw new Error('Expected info.admin.id to be test1, got: ' + info.admin?.id);
}
if (info.data?.name != "test2") {
throw new Error('Expected info.data.name to be test2, got: ' + info.data?.name);
}
`)
if err != nil {
t.Fatal(err)
}
}
func TestBaseBindsDateTime(t *testing.T) {
vm := goja.New()
baseBinds(vm)
_, err := vm.RunString(`
const v0 = new DateTime();
if (v0.isZero()) {
throw new Error('Expected to fallback to now, got zero value');
}
const v1 = new DateTime('2023-01-01 00:00:00.000Z');
const expected = "2023-01-01 00:00:00.000Z"
if (v1.string() != expected) {
throw new Error('Expected ' + expected + ', got ', v1.string());
}
`)
if err != nil {
t.Fatal(err)
}
}
func TestBaseBindsValidationError(t *testing.T) { func TestBaseBindsValidationError(t *testing.T) {
vm := goja.New() vm := goja.New()
baseBinds(vm) baseBinds(vm)

File diff suppressed because it is too large Load Diff

View File

@ -308,6 +308,8 @@ interface Command extends cobra.Command{} // merge
/** /**
* Command defines a single console command. * Command defines a single console command.
* *
* Example:
*
* ` + "```" + `js * ` + "```" + `js
* const command = new Command({ * const command = new Command({
* use: "hello", * use: "hello",
@ -323,6 +325,51 @@ declare class Command implements cobra.Command {
constructor(cmd?: Partial<cobra.Command>) constructor(cmd?: Partial<cobra.Command>)
} }
interface RequestInfo extends models.RequestInfo{} // merge
/**
* RequestInfo defines a single models.RequestInfo instance, usually used
* as part of various filter checks.
*
* Example:
*
* ` + "```" + `js
* const authRecord = $app.dao().findAuthRecordByEmail("users", "test@example.com")
*
* const info = new RequestInfo({
* authRecord: authRecord,
* data: {"name": 123},
* headers: {"x-token": "..."},
* })
*
* const record = $app.dao().findFirstRecordByData("articles", "slug", "hello")
*
* const canAccess = $app.dao().canAccessRecord(record, info, "@request.auth.id != '' && @request.data.name = 123")
* ` + "```" + `
*
* @group PocketBase
*/
declare class RequestInfo implements models.RequestInfo {
constructor(date?: Partial<models.RequestInfo>)
}
interface DateTime extends types.DateTime{} // merge
/**
* DateTime defines a single DateTime type instance.
*
* Example:
*
* ` + "```" + `js
* const dt0 = new DateTime() // now
*
* const dt1 = new DateTime('2023-07-01 00:00:00.000Z')
* ` + "```" + `
*
* @group PocketBase
*/
declare class DateTime implements types.DateTime {
constructor(date?: string)
}
interface ValidationError extends ozzo_validation.Error{} // merge interface ValidationError extends ozzo_validation.Error{} // merge
/** /**
* ValidationError defines a single formatted data validation error, * ValidationError defines a single formatted data validation error,
@ -690,7 +737,7 @@ declare namespace $apis {
let requireAdminOrRecordAuth: apis.requireAdminOrRecordAuth let requireAdminOrRecordAuth: apis.requireAdminOrRecordAuth
let requireAdminOrOwnerAuth: apis.requireAdminOrOwnerAuth let requireAdminOrOwnerAuth: apis.requireAdminOrOwnerAuth
let activityLogger: apis.activityLogger let activityLogger: apis.activityLogger
let requestData: apis.requestData let requestInfo: apis.requestInfo
let recordAuthResponse: apis.recordAuthResponse let recordAuthResponse: apis.recordAuthResponse
let enrichRecord: apis.enrichRecord let enrichRecord: apis.enrichRecord
let enrichRecords: apis.enrichRecords let enrichRecords: apis.enrichRecords

View File

@ -64,7 +64,7 @@ func (r *runner) run() (*search.ResolverResult, error) {
} }
if r.activeProps[0] == "@request" { if r.activeProps[0] == "@request" {
if r.resolver.requestData == nil { if r.resolver.requestInfo == nil {
return &search.ResolverResult{Identifier: "NULL"}, nil return &search.ResolverResult{Identifier: "NULL"}, nil
} }
@ -87,17 +87,17 @@ func (r *runner) run() (*search.ResolverResult, error) {
// check for data relation field // check for data relation field
if dataField.Type == schema.FieldTypeRelation && len(r.activeProps) > 3 { if dataField.Type == schema.FieldTypeRelation && len(r.activeProps) > 3 {
return r.processRequestDataRelationField(dataField) return r.processRequestInfoRelationField(dataField)
} }
// check for select:each field // check for select:each field
if modifier == eachModifier && dataField.Type == schema.FieldTypeSelect && len(r.activeProps) == 3 { if modifier == eachModifier && dataField.Type == schema.FieldTypeSelect && len(r.activeProps) == 3 {
return r.processRequestDataSelectEachModifier(dataField) return r.processRequestInfoSelectEachModifier(dataField)
} }
// check for data arrayble fields ":length" modifier // check for data arrayble fields ":length" modifier
if modifier == lengthModifier && list.ExistInSlice(dataField.Type, schema.ArraybleFieldTypes()) && len(r.activeProps) == 3 { if modifier == lengthModifier && list.ExistInSlice(dataField.Type, schema.ArraybleFieldTypes()) && len(r.activeProps) == 3 {
return r.processRequestDataLengthModifier(dataField) return r.processRequestInfoLengthModifier(dataField)
} }
} }
@ -177,11 +177,11 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) {
// resolve the auth collection field // resolve the auth collection field
// --- // ---
if r.resolver.requestData == nil || r.resolver.requestData.AuthRecord == nil || r.resolver.requestData.AuthRecord.Collection() == nil { if r.resolver.requestInfo == nil || r.resolver.requestInfo.AuthRecord == nil || r.resolver.requestInfo.AuthRecord.Collection() == nil {
return &search.ResolverResult{Identifier: "NULL"}, nil return &search.ResolverResult{Identifier: "NULL"}, nil
} }
collection := r.resolver.requestData.AuthRecord.Collection() collection := r.resolver.requestInfo.AuthRecord.Collection()
r.resolver.loadedCollections = append(r.resolver.loadedCollections, collection) r.resolver.loadedCollections = append(r.resolver.loadedCollections, collection)
r.activeCollectionName = collection.Name r.activeCollectionName = collection.Name
@ -193,7 +193,7 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) {
r.activeTableAlias, r.activeTableAlias,
dbx.HashExp{ dbx.HashExp{
// aka. __auth_users.id = :userId // aka. __auth_users.id = :userId
(r.activeTableAlias + ".id"): r.resolver.requestData.AuthRecord.Id, (r.activeTableAlias + ".id"): r.resolver.requestInfo.AuthRecord.Id,
}, },
) )
@ -205,7 +205,7 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) {
tableName: inflector.Columnify(r.activeCollectionName), tableName: inflector.Columnify(r.activeCollectionName),
tableAlias: r.multiMatchActiveTableAlias, tableAlias: r.multiMatchActiveTableAlias,
on: dbx.HashExp{ on: dbx.HashExp{
(r.multiMatchActiveTableAlias + ".id"): r.resolver.requestData.AuthRecord.Id, (r.multiMatchActiveTableAlias + ".id"): r.resolver.requestInfo.AuthRecord.Id,
}, },
}, },
) )
@ -217,8 +217,8 @@ func (r *runner) processRequestAuthField() (*search.ResolverResult, error) {
return r.processActiveProps() return r.processActiveProps()
} }
func (r *runner) processRequestDataLengthModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) { func (r *runner) processRequestInfoLengthModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) {
dataItems := list.ToUniqueStringSlice(r.resolver.requestData.Data[dataField.Name]) dataItems := list.ToUniqueStringSlice(r.resolver.requestInfo.Data[dataField.Name])
result := &search.ResolverResult{ result := &search.ResolverResult{
Identifier: fmt.Sprintf("%d", len(dataItems)), Identifier: fmt.Sprintf("%d", len(dataItems)),
@ -227,13 +227,13 @@ func (r *runner) processRequestDataLengthModifier(dataField *schema.SchemaField)
return result, nil return result, nil
} }
func (r *runner) processRequestDataSelectEachModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) { func (r *runner) processRequestInfoSelectEachModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) {
options, ok := dataField.Options.(*schema.SelectOptions) options, ok := dataField.Options.(*schema.SelectOptions)
if !ok { if !ok {
return nil, fmt.Errorf("failed to initialize field %q options", dataField.Name) return nil, fmt.Errorf("failed to initialize field %q options", dataField.Name)
} }
dataItems := list.ToUniqueStringSlice(r.resolver.requestData.Data[dataField.Name]) dataItems := list.ToUniqueStringSlice(r.resolver.requestInfo.Data[dataField.Name])
rawJson, err := json.Marshal(dataItems) rawJson, err := json.Marshal(dataItems)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot marshalize the data select item for field %q", r.activeProps[2]) return nil, fmt.Errorf("cannot marshalize the data select item for field %q", r.activeProps[2])
@ -272,7 +272,7 @@ func (r *runner) processRequestDataSelectEachModifier(dataField *schema.SchemaFi
return result, nil return result, nil
} }
func (r *runner) processRequestDataRelationField(dataField *schema.SchemaField) (*search.ResolverResult, error) { func (r *runner) processRequestInfoRelationField(dataField *schema.SchemaField) (*search.ResolverResult, error) {
options, ok := dataField.Options.(*schema.RelationOptions) options, ok := dataField.Options.(*schema.RelationOptions)
if !ok { if !ok {
return nil, fmt.Errorf("failed to initialize data field %q options", dataField.Name) return nil, fmt.Errorf("failed to initialize data field %q options", dataField.Name)
@ -284,8 +284,8 @@ func (r *runner) processRequestDataRelationField(dataField *schema.SchemaField)
} }
var dataRelIds []string var dataRelIds []string
if r.resolver.requestData != nil && len(r.resolver.requestData.Data) != 0 { if r.resolver.requestInfo != nil && len(r.resolver.requestInfo.Data) != 0 {
dataRelIds = list.ToUniqueStringSlice(r.resolver.requestData.Data[dataField.Name]) dataRelIds = list.ToUniqueStringSlice(r.resolver.requestInfo.Data[dataField.Name])
} }
if len(dataRelIds) == 0 { if len(dataRelIds) == 0 {
return &search.ResolverResult{Identifier: "NULL"}, nil return &search.ResolverResult{Identifier: "NULL"}, nil

View File

@ -56,7 +56,7 @@ type CollectionsFinder interface {
// resolver := resolvers.NewRecordFieldResolver( // resolver := resolvers.NewRecordFieldResolver(
// app.Dao(), // app.Dao(),
// myCollection, // myCollection,
// &models.RequestData{...}, // &models.RequestInfo{...},
// true, // true,
// ) // )
// provider := search.NewProvider(resolver) // provider := search.NewProvider(resolver)
@ -68,21 +68,21 @@ type RecordFieldResolver struct {
allowedFields []string allowedFields []string
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
requestData *models.RequestData requestInfo *models.RequestInfo
staticRequestData map[string]any staticRequestInfo map[string]any
} }
// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`. // NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`.
func NewRecordFieldResolver( func NewRecordFieldResolver(
dao CollectionsFinder, dao CollectionsFinder,
baseCollection *models.Collection, baseCollection *models.Collection,
requestData *models.RequestData, requestInfo *models.RequestInfo,
allowHiddenFields bool, allowHiddenFields bool,
) *RecordFieldResolver { ) *RecordFieldResolver {
r := &RecordFieldResolver{ r := &RecordFieldResolver{
dao: dao, dao: dao,
baseCollection: baseCollection, baseCollection: baseCollection,
requestData: requestData, requestInfo: requestInfo,
allowHiddenFields: allowHiddenFields, allowHiddenFields: allowHiddenFields,
joins: []*join{}, joins: []*join{},
loadedCollections: []*models.Collection{baseCollection}, loadedCollections: []*models.Collection{baseCollection},
@ -97,17 +97,17 @@ func NewRecordFieldResolver(
}, },
} }
r.staticRequestData = map[string]any{} r.staticRequestInfo = map[string]any{}
if r.requestData != nil { if r.requestInfo != nil {
r.staticRequestData["method"] = r.requestData.Method r.staticRequestInfo["method"] = r.requestInfo.Method
r.staticRequestData["query"] = r.requestData.Query r.staticRequestInfo["query"] = r.requestInfo.Query
r.staticRequestData["headers"] = r.requestData.Headers r.staticRequestInfo["headers"] = r.requestInfo.Headers
r.staticRequestData["data"] = r.requestData.Data r.staticRequestInfo["data"] = r.requestInfo.Data
r.staticRequestData["auth"] = nil r.staticRequestInfo["auth"] = nil
if r.requestData.AuthRecord != nil { if r.requestInfo.AuthRecord != nil {
r.requestData.AuthRecord.IgnoreEmailVisibility(true) r.requestInfo.AuthRecord.IgnoreEmailVisibility(true)
r.staticRequestData["auth"] = r.requestData.AuthRecord.PublicExport() r.staticRequestInfo["auth"] = r.requestInfo.AuthRecord.PublicExport()
r.requestData.AuthRecord.IgnoreEmailVisibility(false) r.requestInfo.AuthRecord.IgnoreEmailVisibility(false)
} }
} }
@ -166,7 +166,7 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search
path[len(path)-1] = lastProp path[len(path)-1] = lastProp
// extract value // extract value
resultVal, err := extractNestedMapVal(r.staticRequestData, path...) resultVal, err := extractNestedMapVal(r.staticRequestInfo, path...)
if modifier == issetModifier { if modifier == issetModifier {
if err != nil { if err != nil {
@ -175,7 +175,7 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search
return &search.ResolverResult{Identifier: "TRUE"}, nil return &search.ResolverResult{Identifier: "TRUE"}, nil
} }
// note: we are ignoring the error because requestData is dynamic // note: we are ignoring the error because requestInfo is dynamic
// and some of the lookup keys may not be defined for the request // and some of the lookup keys may not be defined for the request
switch v := resultVal.(type) { switch v := resultVal.(type) {

View File

@ -22,7 +22,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
requestData := &models.RequestData{ requestInfo := &models.RequestInfo{
Headers: map[string]any{ Headers: map[string]any{
"a": "123", "a": "123",
"b": "456", "b": "456",
@ -313,7 +313,7 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
query := app.Dao().RecordQuery(collection) query := app.Dao().RecordQuery(collection)
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, s.allowHiddenFields) r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestInfo, s.allowHiddenFields)
expr, err := search.FilterData(s.rule).BuildExpr(r) expr, err := search.FilterData(s.rule).BuildExpr(r)
if err != nil { if err != nil {
@ -353,11 +353,11 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
requestData := &models.RequestData{ requestInfo := &models.RequestInfo{
AuthRecord: authRecord, AuthRecord: authRecord,
} }
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true) r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestInfo, true)
scenarios := []struct { scenarios := []struct {
fieldName string fieldName string
@ -424,7 +424,7 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
} }
} }
func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) { func TestRecordFieldResolverResolveStaticRequestInfoFields(t *testing.T) {
app, _ := tests.NewTestApp() app, _ := tests.NewTestApp()
defer app.Cleanup() defer app.Cleanup()
@ -438,7 +438,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
requestData := &models.RequestData{ requestInfo := &models.RequestInfo{
Method: "get", Method: "get",
Query: map[string]any{ Query: map[string]any{
"a": 123, "a": 123,
@ -455,7 +455,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
AuthRecord: authRecord, AuthRecord: authRecord,
} }
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true) r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestInfo, true)
scenarios := []struct { scenarios := []struct {
fieldName string fieldName string