Skip to content

Commit

Permalink
filter enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Jan 7, 2023
1 parent d5775ff commit 9b880f5
Show file tree
Hide file tree
Showing 102 changed files with 3,664 additions and 957 deletions.
84 changes: 80 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,94 @@
## (WIP)
## (WIP) v0.11.0

- Added `+` and `-` body field modifiers for `number`, `files`, `select` and `relation` fields.
```js
{
// oldValue + 2
"someNumber+": 2,

// oldValue + ["id1", "id2"] - ["id3"]
"someRelation+": ["id1", "id2"],
"someRelation-": ["id3"],

// delete single file by its name (file fields supports only the "-" modifier!)
"someFile-": "filename.png",
}
```
_Note1: `@request.data.someField` will contain the final resolved value._

_Note2: The old index (`"field.0":null`) and filename (`"field.filename.png":null`) based suffixed syntax for deleting files is still supported._

- ! Added support for multi-match/match-all request data and collection multi-valued fields (`select`, `relation`) conditions.
If you want a "at least one of" type of condition, you can prefix the operator with `?`.
```js
// for each someRelA.someRelB record require the "status" field to be "active"
someRelA.someRelB.status = "active"

// OR for "at least one of" condition
someRelA.someRelB.status ?= "active"
```
_**Note: Previously the behavior for multi-valued fields was as the "at least one of" type.
The release comes with system db migration that will update your existing API rules (if needed) to preserve the compatibility.
If you have multi-select or multi-relation filter checks in your client-side code and want to preserve the old behavior, you'll have to prefix with `?` your operators.**_
- Added support for querying `@request.data.someRelField.*` relation fields.
```js
// example submitted data: {"someRel": "REL_RECORD_ID"}
@request.data.someRel.status = "active"
```
- Added `:isset` modifier for the static request data fields.
```js
// prevent changing the "role" field
@request.data.role:isset = false
```

- Added `:length` modifier for the arrayable request data and collection fields (`select`, `file`, `relation`).
```js
// example submitted data: {"someSelectField": ["val1", "val2"]}
@request.data.someSelectField:length = 2

// check existing record field length
someSelectField:length = 2
```

- Added `:each` modifier support for the multi-`select` request data and collection field.
```js
// check if all selected rows has "pb_" prefix
roles:each ~ 'pb_%'
```

- Improved the Admin UI filters autocomplete.

- Added `@random` sort key for `RANDOM()` sorted list results.

- Added Strava OAuth2 provider ([#1443](https://github.com/pocketbase/pocketbase/pull/1443); thanks @szsascha).

- Added Gitee OAuth2 provider ([#1448](https://github.com/pocketbase/pocketbase/pull/1448); thanks @yuxiang-gao).

- Added IME status check to the textarea keydown handler ([#1370](https://github.com/pocketbase/pocketbase/pull/1370); thanks @tenthree).

- Fixed the text wrapping in the Admin UI listing searchbar ([#1416](https://github.com/pocketbase/pocketbase/issues/1416)).

- Added `filesystem.NewFileFromBytes()` helper ([#1420](https://github.com/pocketbase/pocketbase/pull/1420); thanks @dschissler).

- Added support for reordering uploaded multiple files.

- Added `webp` to the default images mime type presets list ([#1469](https://github.com/pocketbase/pocketbase/pull/1469); thanks @khairulhaaziq).

- Added the OAuth2 refresh token to the auth meta response ([#1487](https://github.com/pocketbase/pocketbase/issues/1487)).

- Fixed the text wrapping in the Admin UI listing searchbar ([#1416](https://github.com/pocketbase/pocketbase/issues/1416)).

- Fixed number field value output in the records listing ([#1447](https://github.com/pocketbase/pocketbase/issues/1447)).

- Added webp to the default images mime type presets list ([#1469](https://github.com/pocketbase/pocketbase/pull/1469); thanks @khairulhaaziq).
- Fixed duplicated settings view pages caused by uncompleted transitions ([#1498](https://github.com/pocketbase/pocketbase/issues/1498)).

- Allowed sending `Authorization` header with the `/auth-with-password` record and admin login requests ([#1494](https://github.com/pocketbase/pocketbase/discussions/1494)).

- `migrate down` now reverts migrations in the applied order.

- Added additional list-bucket check in the S3 config test API.

- Other minor improvements.


## v0.10.4
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright (c) 2022, Gani Georgiev
Copyright (c) 2022 - present, Gani Georgiev

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
Expand Down
2 changes: 1 addition & 1 deletion apis/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func bindAdminApi(app core.App, rg *echo.Group) {
api := adminApi{app: app}

subGroup := rg.Group("/admins", ActivityLogger(app))
subGroup.POST("/auth-with-password", api.authWithPassword, RequireGuestOnly())
subGroup.POST("/auth-with-password", api.authWithPassword)
subGroup.POST("/request-password-reset", api.requestPasswordReset)
subGroup.POST("/confirm-password-reset", api.confirmPasswordReset)
subGroup.POST("/auth-refresh", api.authRefresh, RequireAdminAuth())
Expand Down
22 changes: 14 additions & 8 deletions apis/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ func TestAdminAuthWithEmail(t *testing.T) {
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "valid email/password (guest)",
Method: http.MethodPost,
Url: "/api/admins/auth-with-password",
Body: strings.NewReader(`{"identity":"[email protected]","password":"1234567890"}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"admin":{"id":"sywbhecnh46rhm0"`,
`"token":`,
},
ExpectedEvents: map[string]int{
"OnAdminAuthRequest": 1,
},
},
{
Name: "valid email/password (already authorized)",
Method: http.MethodPost,
Expand All @@ -56,14 +70,6 @@ func TestAdminAuthWithEmail(t *testing.T) {
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"message":"The request can be accessed only by guests.","data":{}`},
},
{
Name: "valid email/password (guest)",
Method: http.MethodPost,
Url: "/api/admins/auth-with-password",
Body: strings.NewReader(`{"identity":"[email protected]","password":"1234567890"}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"admin":{"id":"sywbhecnh46rhm0"`,
Expand Down
22 changes: 12 additions & 10 deletions apis/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":7`,
`"totalItems":8`,
`"items":[{`,
`"id":"_pb_users_auth_"`,
`"id":"v851q4r790rhknl"`,
Expand All @@ -51,6 +51,7 @@ func TestCollectionsList(t *testing.T) {
`"id":"sz5l5z67tg7gku0"`,
`"id":"wzlqyes4orhoygb"`,
`"id":"4d1blo5cuycfaca"`,
`"id":"9n89pl5vkct6330"`,
`"type":"auth"`,
`"type":"base"`,
},
Expand All @@ -69,10 +70,10 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":2`,
`"perPage":2`,
`"totalItems":7`,
`"totalItems":8`,
`"items":[{`,
`"id":"v851q4r790rhknl"`,
`"id":"4d1blo5cuycfaca"`,
`"id":"wzlqyes4orhoygb"`,
},
ExpectedEvents: map[string]int{
"OnCollectionsListRequest": 1,
Expand All @@ -99,12 +100,13 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":4`,
`"totalItems":5`,
`"items":[{`,
`"id":"wsmn24bux7wo113"`,
`"id":"sz5l5z67tg7gku0"`,
`"id":"wzlqyes4orhoygb"`,
`"id":"4d1blo5cuycfaca"`,
`"id":"9n89pl5vkct6330"`,
},
ExpectedEvents: map[string]int{
"OnCollectionsListRequest": 1,
Expand Down Expand Up @@ -786,7 +788,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 7
expected := 8
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -814,7 +816,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 7
expected := 8
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -856,7 +858,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 7
expected := 8
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -909,7 +911,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 10
expected := 11
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
Expand Down Expand Up @@ -996,8 +998,8 @@ func TestCollectionImport(t *testing.T) {
ExpectedEvents: map[string]int{
"OnCollectionsAfterImportRequest": 1,
"OnCollectionsBeforeImportRequest": 1,
"OnModelBeforeDelete": 5,
"OnModelAfterDelete": 5,
"OnModelBeforeDelete": 6,
"OnModelAfterDelete": 6,
"OnModelBeforeUpdate": 2,
"OnModelAfterUpdate": 2,
"OnModelBeforeCreate": 1,
Expand Down
4 changes: 2 additions & 2 deletions apis/record_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func bindRecordAuthApi(app core.App, rg *echo.Group) {

subGroup.GET("/auth-methods", api.authMethods)
subGroup.POST("/auth-refresh", api.authRefresh, RequireSameContextRecordAuth())
subGroup.POST("/auth-with-oauth2", api.authWithOAuth2) // allow anyone so that we can link the OAuth2 profile with the authenticated record
subGroup.POST("/auth-with-password", api.authWithPassword, RequireGuestOnly())
subGroup.POST("/auth-with-oauth2", api.authWithOAuth2)
subGroup.POST("/auth-with-password", api.authWithPassword)
subGroup.POST("/request-password-reset", api.requestPasswordReset)
subGroup.POST("/confirm-password-reset", api.confirmPasswordReset)
subGroup.POST("/request-verification", api.requestVerification)
Expand Down
66 changes: 46 additions & 20 deletions apis/record_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,6 @@ func TestRecordAuthMethodsList(t *testing.T) {

func TestRecordAuthWithPassword(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "authenticated record",
Method: http.MethodPost,
Url: "/api/collections/users/auth-with-password",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authenticated admin",
Method: http.MethodPost,
Url: "/api/collections/users/auth-with-password",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "invalid body format",
Method: http.MethodPost,
Expand Down Expand Up @@ -226,6 +206,52 @@ func TestRecordAuthWithPassword(t *testing.T) {
"OnRecordAuthRequest": 1,
},
},

// with already authenticated record or admin
{
Name: "authenticated record",
Method: http.MethodPost,
Url: "/api/collections/users/auth-with-password",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
},
Body: strings.NewReader(`{
"identity":"[email protected]",
"password":"1234567890"
}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"record":{`,
`"token":"`,
`"id":"4q1xlclmfloku33"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"OnRecordAuthRequest": 1,
},
},
{
Name: "authenticated admin",
Method: http.MethodPost,
Url: "/api/collections/users/auth-with-password",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
Body: strings.NewReader(`{
"identity":"[email protected]",
"password":"1234567890"
}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"record":{`,
`"token":"`,
`"id":"4q1xlclmfloku33"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"OnRecordAuthRequest": 1,
},
},
}

for _, scenario := range scenarios {
Expand Down
31 changes: 24 additions & 7 deletions apis/record_crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,20 @@ func (api *recordApi) create(c echo.Context) error {

// temporary save the record and check it against the create rule
if requestData.Admin == nil && collection.CreateRule != nil {
testRecord := models.NewRecord(collection)

// replace modifiers fields so that the resolved value is always
// available when accessing requestData.Data using just the field name
if requestData.HasModifierDataKeys() {
requestData.Data = testRecord.ReplaceModifers(requestData.Data)
}

testForm := forms.NewRecordUpsert(api.app, testRecord)
testForm.SetFullManageAccess(true)
if err := testForm.LoadRequest(c.Request(), ""); err != nil {
return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}

createRuleFunc := func(q *dbx.SelectQuery) error {
if *collection.CreateRule == "" {
return nil // no create rule to resolve
Expand All @@ -181,13 +195,6 @@ func (api *recordApi) create(c echo.Context) error {
return nil
}

testRecord := models.NewRecord(collection)
testForm := forms.NewRecordUpsert(api.app, testRecord)
testForm.SetFullManageAccess(true)
if err := testForm.LoadRequest(c.Request(), ""); err != nil {
return NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
}

testErr := testForm.DrySubmit(func(txDao *daos.Dao) error {
foundRecord, err := txDao.FindRecordById(collection.Id, testRecord.Id, createRuleFunc)
if err != nil {
Expand Down Expand Up @@ -258,6 +265,16 @@ func (api *recordApi) update(c echo.Context) error {
return NewForbiddenError("Only admins can perform this action.", nil)
}

// eager fetch the record so that the modifier field values are replaced
// and available when accessing requestData.Data using just the field name
if requestData.HasModifierDataKeys() {
record, err := api.app.Dao().FindRecordById(collection.Id, recordId)
if err != nil || record == nil {
return NewNotFoundError("", err)
}
requestData.Data = record.ReplaceModifers(requestData.Data)
}

ruleFunc := func(q *dbx.SelectQuery) error {
if requestData.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
Expand Down
Loading

0 comments on commit 9b880f5

Please sign in to comment.