mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-09-30 11:31:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1921aeef8 | ||
|
|
012546e838 | ||
|
|
f792a9e08d | ||
|
|
a8b2f0f6f1 | ||
|
|
f7d4722052 | ||
|
|
3dc1bf6fa7 | ||
|
|
d3cc87abee | ||
|
|
19ad827302 | ||
|
|
40830b6c43 | ||
|
|
f234132629 | ||
|
|
9b880f5ab4 | ||
|
|
d5775ff657 | ||
|
|
41ba7e12e1 | ||
|
|
5ef6b3a8be | ||
|
|
c673d9d314 | ||
|
|
a7aa3da67e | ||
|
|
0439af458f | ||
|
|
6f3241399c | ||
|
|
19ccc70fe5 | ||
|
|
9033cd109e | ||
|
|
e1773eead0 | ||
|
|
f6fff85d07 | ||
|
|
166d2eafc7 | ||
|
|
d710446c71 | ||
|
|
8f6cb5ad2d | ||
|
|
775417ac2b | ||
|
|
079616ee8e | ||
|
|
26b794aa08 | ||
|
|
d55610511d |
No files matched your search
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '>=1.19.4'
|
||||
go-version: '>=1.19.5'
|
||||
|
||||
# This step usually is not needed because the /ui/dist is pregenerated locally
|
||||
# but its here to ensure that each release embeds the latest admin ui artifacts.
|
||||
|
||||
+107
@@ -1,3 +1,110 @@
|
||||
## v0.11.2
|
||||
|
||||
- Fixed `fs.DeleteByPrefix()` hang on invalid S3 settings ([#1575](https://github.com/pocketbase/pocketbase/discussions/1575#discussioncomment-4661089)).
|
||||
|
||||
- Updated file(s) delete to run in the background on record/collection delete to avoid blocking the delete model transaction.
|
||||
_Currently the cascade files delete operation is treated as "non-critical" and in case of an error it is just logged during debug._
|
||||
_This will be improved in the near future with the planned async job queue implementation._
|
||||
|
||||
|
||||
## v0.11.1
|
||||
|
||||
- Unescaped path parameter values ([#1552](https://github.com/pocketbase/pocketbase/issues/1552)).
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
- 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)).
|
||||
|
||||
- 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
|
||||
|
||||
- Fixed `Record.MergeExpand` panic when the main model expand map is not initialized ([#1365](https://github.com/pocketbase/pocketbase/issues/1365)).
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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())
|
||||
|
||||
+14
-8
@@ -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":"test@example.com","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,
|
||||
@@ -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":"test@example.com","password":"1234567890"}`),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"admin":{"id":"sywbhecnh46rhm0"`,
|
||||
|
||||
@@ -26,6 +26,13 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||
e := echo.New()
|
||||
e.Debug = app.IsDebug()
|
||||
|
||||
// configure a custom router
|
||||
e.ResetRouterCreator(func(ec *echo.Echo) echo.Router {
|
||||
return echo.NewRouter(echo.RouterConfig{
|
||||
UnescapePathParamValues: true,
|
||||
})
|
||||
})
|
||||
|
||||
// default middlewares
|
||||
e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{
|
||||
Skipper: func(c echo.Context) bool {
|
||||
|
||||
@@ -66,6 +66,22 @@ func TestCustomRoutesAndErrorsHandling(t *testing.T) {
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
},
|
||||
{
|
||||
Name: "custom route with url encoded parameter",
|
||||
Method: http.MethodGet,
|
||||
Url: "/a%2Bb%2Bc",
|
||||
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
|
||||
e.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:param",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(200, c.PathParam("param"))
|
||||
},
|
||||
})
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"a+b+c"},
|
||||
},
|
||||
{
|
||||
Name: "route with HTTPError",
|
||||
Method: http.MethodGet,
|
||||
|
||||
+15
-10
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
@@ -42,7 +43,7 @@ func TestCollectionsList(t *testing.T) {
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalItems":7`,
|
||||
`"totalItems":8`,
|
||||
`"items":[{`,
|
||||
`"id":"_pb_users_auth_"`,
|
||||
`"id":"v851q4r790rhknl"`,
|
||||
@@ -51,6 +52,7 @@ func TestCollectionsList(t *testing.T) {
|
||||
`"id":"sz5l5z67tg7gku0"`,
|
||||
`"id":"wzlqyes4orhoygb"`,
|
||||
`"id":"4d1blo5cuycfaca"`,
|
||||
`"id":"9n89pl5vkct6330"`,
|
||||
`"type":"auth"`,
|
||||
`"type":"base"`,
|
||||
},
|
||||
@@ -69,10 +71,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,
|
||||
@@ -99,12 +101,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,
|
||||
@@ -230,6 +233,7 @@ func TestCollectionDelete(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeDelete": 1,
|
||||
@@ -248,6 +252,7 @@ func TestCollectionDelete(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeDelete": 1,
|
||||
@@ -786,7 +791,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))
|
||||
}
|
||||
@@ -814,7 +819,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))
|
||||
}
|
||||
@@ -856,7 +861,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))
|
||||
}
|
||||
@@ -909,7 +914,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))
|
||||
}
|
||||
@@ -996,8 +1001,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,
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+46
-20
@@ -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,
|
||||
@@ -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":"test@example.com",
|
||||
"password":"1234567890"
|
||||
}`),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"record":{`,
|
||||
`"token":"`,
|
||||
`"id":"4q1xlclmfloku33"`,
|
||||
`"email":"test@example.com"`,
|
||||
},
|
||||
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":"test@example.com",
|
||||
"password":"1234567890"
|
||||
}`),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"record":{`,
|
||||
`"token":"`,
|
||||
`"id":"4q1xlclmfloku33"`,
|
||||
`"email":"test@example.com"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordAuthRequest": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
+24
-7
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
|
||||
+162
-1
@@ -2,10 +2,12 @@ package apis_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
@@ -209,6 +211,50 @@ func TestRecordCrudList(t *testing.T) {
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
|
||||
},
|
||||
{
|
||||
Name: ":rule modifer",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/collections/demo5/records",
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalPages":1`,
|
||||
`"totalItems":1`,
|
||||
`"items":[{`,
|
||||
`"id":"qjeql998mtp1azp"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
|
||||
},
|
||||
{
|
||||
Name: "multi-match - at least one of",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalPages":1`,
|
||||
`"totalItems":1`,
|
||||
`"items":[{`,
|
||||
`"id":"qzaqccwrmva4o1n"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
|
||||
},
|
||||
{
|
||||
Name: "multi-match - all",
|
||||
Method: http.MethodGet,
|
||||
Url: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length=2"),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalPages":0`,
|
||||
`"totalItems":0`,
|
||||
`"items":[]`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
|
||||
},
|
||||
|
||||
// auth collection checks
|
||||
// -----------------------------------------------------------
|
||||
@@ -691,6 +737,7 @@ func TestRecordCrudDelete(t *testing.T) {
|
||||
// users, test@example.com
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
|
||||
},
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelAfterDelete": 3, // +2 because of the external auths
|
||||
@@ -716,6 +763,25 @@ func TestRecordCrudDelete(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "@request :isset (rule failure check)",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/collections/demo5/records/la4y2w4o98acwuj",
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
},
|
||||
{
|
||||
Name: "@request :isset (rule pass check)",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/collections/demo5/records/la4y2w4o98acwuj?test=1",
|
||||
ExpectedStatus: 204,
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelAfterDelete": 1,
|
||||
"OnModelBeforeDelete": 1,
|
||||
"OnRecordAfterDeleteRequest": 1,
|
||||
"OnRecordBeforeDeleteRequest": 1,
|
||||
},
|
||||
},
|
||||
|
||||
// cascade delete checks
|
||||
// -----------------------------------------------------------
|
||||
@@ -730,7 +796,7 @@ func TestRecordCrudDelete(t *testing.T) {
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeDeleteRequest": 1,
|
||||
"OnModelBeforeUpdate": 1, // self_rel_many update of test1 record
|
||||
"OnModelBeforeUpdate": 2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5
|
||||
"OnModelBeforeDelete": 2, // the record itself + rel_one_cascade of test1 record
|
||||
},
|
||||
},
|
||||
@@ -758,6 +824,7 @@ func TestRecordCrudDelete(t *testing.T) {
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
Delay: 100 * time.Millisecond,
|
||||
ExpectedStatus: 204,
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelBeforeDelete": 2,
|
||||
@@ -1092,6 +1159,63 @@ func TestRecordCrudCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
|
||||
// fields modifier checks
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
Name: "trying to delete a record while being part of a non-cascade required relation",
|
||||
Method: http.MethodDelete,
|
||||
Url: "/api/collections/demo3/records/7nwo8tuiatetxdm",
|
||||
RequestHeaders: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnRecordBeforeDeleteRequest": 1,
|
||||
"OnModelBeforeUpdate": 2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5
|
||||
"OnModelBeforeDelete": 2, // the record itself + rel_one_cascade of test1 record
|
||||
},
|
||||
},
|
||||
|
||||
// check whether if @request.data modifer fields are properly resolved
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
Name: "@request.data.field with compute modifers (rule failure check)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections/demo5/records",
|
||||
Body: strings.NewReader(`{
|
||||
"total":1,
|
||||
"total+":4,
|
||||
"total-":1
|
||||
}`),
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "@request.data.field with compute modifers (rule pass check)",
|
||||
Method: http.MethodPost,
|
||||
Url: "/api/collections/demo5/records",
|
||||
Body: strings.NewReader(`{
|
||||
"total":1,
|
||||
"total+":3,
|
||||
"total-":1
|
||||
}`),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":"`,
|
||||
`"collectionName":"demo5"`,
|
||||
`"total":3`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelAfterCreate": 1,
|
||||
"OnModelBeforeCreate": 1,
|
||||
"OnRecordAfterCreateRequest": 1,
|
||||
"OnRecordBeforeCreateRequest": 1,
|
||||
},
|
||||
},
|
||||
|
||||
// auth records
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
@@ -1501,6 +1625,43 @@ func TestRecordCrudUpdate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
|
||||
// check whether if @request.data modifer fields are properly resolved
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
Name: "@request.data.field with compute modifers (rule failure check)",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo5/records/la4y2w4o98acwuj",
|
||||
Body: strings.NewReader(`{
|
||||
"total+":3,
|
||||
"total-":1
|
||||
}`),
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{
|
||||
`"data":{}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "@request.data.field with compute modifers (rule pass check)",
|
||||
Method: http.MethodPatch,
|
||||
Url: "/api/collections/demo5/records/la4y2w4o98acwuj",
|
||||
Body: strings.NewReader(`{
|
||||
"total+":2,
|
||||
"total-":1
|
||||
}`),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"id":"la4y2w4o98acwuj"`,
|
||||
`"collectionName":"demo5"`,
|
||||
`"total":3`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"OnModelAfterUpdate": 1,
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnRecordAfterUpdateRequest": 1,
|
||||
"OnRecordBeforeUpdateRequest": 1,
|
||||
},
|
||||
},
|
||||
|
||||
// auth records
|
||||
// -----------------------------------------------------------
|
||||
{
|
||||
|
||||
@@ -15,11 +15,6 @@ import (
|
||||
|
||||
const ContextRequestDataKey = "requestData"
|
||||
|
||||
// Deprecated: Will be removed after v0.9. Use apis.RequestData(c) instead.
|
||||
func GetRequestData(c echo.Context) *models.RequestData {
|
||||
return RequestData(c)
|
||||
}
|
||||
|
||||
// RequestData exports cached common request data fields
|
||||
// (query, body, logged auth state, etc.) from the provided context.
|
||||
func RequestData(c echo.Context) *models.RequestData {
|
||||
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@@ -91,14 +92,17 @@ func (api *settingsApi) testS3(c echo.Context) error {
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
testFileKey := "pb_test_" + security.PseudorandomString(5) + "/test.txt"
|
||||
testPrefix := "pb_settings_test_" + security.PseudorandomString(5)
|
||||
testFileKey := testPrefix + "/test.txt"
|
||||
|
||||
// try to upload a test file
|
||||
if err := fs.Upload([]byte("test"), testFileKey); err != nil {
|
||||
return NewBadRequestError("Failed to upload a test file. Raw error: \n"+err.Error(), nil)
|
||||
}
|
||||
|
||||
if err := fs.Delete(testFileKey); err != nil {
|
||||
return NewBadRequestError("Failed to delete a test file. Raw error: \n"+err.Error(), nil)
|
||||
// test prefix deletion (ensures that both bucket list and delete works)
|
||||
if errs := fs.DeletePrefix(testPrefix); len(errs) > 0 {
|
||||
return NewBadRequestError(fmt.Sprintf("Failed to delete a test file. Raw error: %v", errs), nil)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
|
||||
@@ -58,6 +58,8 @@ func TestSettingsList(t *testing.T) {
|
||||
`"spotifyAuth":{`,
|
||||
`"kakaoAuth":{`,
|
||||
`"twitchAuth":{`,
|
||||
`"stravaAuth":{`,
|
||||
`"giteeAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
},
|
||||
@@ -125,6 +127,8 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"spotifyAuth":{`,
|
||||
`"kakaoAuth":{`,
|
||||
`"twitchAuth":{`,
|
||||
`"stravaAuth":{`,
|
||||
`"giteeAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
`"appName":"acme_test"`,
|
||||
@@ -181,6 +185,8 @@ func TestSettingsSet(t *testing.T) {
|
||||
`"spotifyAuth":{`,
|
||||
`"kakaoAuth":{`,
|
||||
`"twitchAuth":{`,
|
||||
`"stravaAuth":{`,
|
||||
`"giteeAuth":{`,
|
||||
`"secret":"******"`,
|
||||
`"clientSecret":"******"`,
|
||||
`"appName":"update_test"`,
|
||||
|
||||
+2
-1
@@ -78,7 +78,8 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
NextProtos: []string{acme.ALPNProto},
|
||||
},
|
||||
ReadTimeout: 60 * time.Second,
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
ReadHeaderTimeout: 30 * time.Second,
|
||||
// WriteTimeout: 60 * time.Second, // breaks sse!
|
||||
Handler: router,
|
||||
Addr: mainAddr,
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import (
|
||||
type App interface {
|
||||
// Deprecated:
|
||||
// This method may get removed in the near future.
|
||||
// It is recommended to access the logs db instance from app.Dao().DB() or
|
||||
// It is recommended to access the app db instance from app.Dao().DB() or
|
||||
// if you want more flexibility - app.Dao().ConcurrentDB() and app.Dao().NonconcurrentDB().
|
||||
//
|
||||
// DB returns the default app database instance.
|
||||
|
||||
+17
-7
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||
"github.com/pocketbase/pocketbase/tools/routine"
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
||||
)
|
||||
@@ -939,20 +940,29 @@ func (app *BaseApp) registerDefaultHooks() {
|
||||
|
||||
failed := fs.DeletePrefix(prefix)
|
||||
if len(failed) > 0 {
|
||||
return errors.New("Failed to delete the files at " + prefix)
|
||||
return errors.New("failed to delete the files at " + prefix)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete storage files from deleted Collection, Records, etc.
|
||||
// try to delete the storage files from deleted Collection, Records, etc. model
|
||||
app.OnModelAfterDelete().Add(func(e *ModelEvent) error {
|
||||
if m, ok := e.Model.(models.FilesManager); ok && m.BaseFilesPath() != "" {
|
||||
if err := deletePrefix(m.BaseFilesPath()); err != nil && app.IsDebug() {
|
||||
// non critical error - only log for debug
|
||||
// (usually could happen because of S3 api limits)
|
||||
log.Println(err)
|
||||
}
|
||||
prefix := m.BaseFilesPath()
|
||||
|
||||
// run in the background for "optimistic" delete to avoid
|
||||
// blocking the delete transaction
|
||||
//
|
||||
// @todo consider creating a bg process queue so that the
|
||||
// call could be "retried" in case of a failure.
|
||||
routine.FireAndForget(func() {
|
||||
if err := deletePrefix(prefix); err != nil && app.IsDebug() {
|
||||
// non critical error - only log for debug
|
||||
// (usually could happen because of S3 api limits)
|
||||
log.Println(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+2
-2
@@ -97,8 +97,8 @@ func (dao *Dao) IsAdminEmailUnique(email string, excludeIds ...string) bool {
|
||||
AndWhere(dbx.HashExp{"email": email}).
|
||||
Limit(1)
|
||||
|
||||
if len(excludeIds) > 0 {
|
||||
query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(excludeIds)...))
|
||||
if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
|
||||
query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...))
|
||||
}
|
||||
|
||||
var exists bool
|
||||
|
||||
+5
-4
@@ -65,8 +65,7 @@ func (dao *Dao) IsCollectionNameUnique(name string, excludeIds ...string) bool {
|
||||
AndWhere(dbx.NewExp("LOWER([[name]])={:name}", dbx.Params{"name": strings.ToLower(name)})).
|
||||
Limit(1)
|
||||
|
||||
if len(excludeIds) > 0 {
|
||||
uniqueExcludeIds := list.NonzeroUniques(excludeIds)
|
||||
if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
|
||||
query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...))
|
||||
}
|
||||
|
||||
@@ -85,15 +84,17 @@ func (dao *Dao) FindCollectionReferences(collection *models.Collection, excludeI
|
||||
collections := []*models.Collection{}
|
||||
|
||||
query := dao.CollectionQuery()
|
||||
if len(excludeIds) > 0 {
|
||||
uniqueExcludeIds := list.NonzeroUniques(excludeIds)
|
||||
|
||||
if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
|
||||
query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...))
|
||||
}
|
||||
|
||||
if err := query.All(&collections); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[*models.Collection][]*schema.SchemaField{}
|
||||
|
||||
for _, c := range collections {
|
||||
for _, f := range c.Schema.Fields() {
|
||||
if f.Type != schema.FieldTypeRelation {
|
||||
|
||||
+15
-8
@@ -37,7 +37,7 @@ func TestFindCollectionsByType(t *testing.T) {
|
||||
{"", false, 0},
|
||||
{"unknown", false, 0},
|
||||
{models.CollectionTypeAuth, false, 3},
|
||||
{models.CollectionTypeBase, false, 4},
|
||||
{models.CollectionTypeBase, false, 5},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
@@ -122,7 +122,13 @@ func TestFindCollectionReferences(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := app.Dao().FindCollectionReferences(collection, collection.Id)
|
||||
result, err := app.Dao().FindCollectionReferences(
|
||||
collection,
|
||||
collection.Id,
|
||||
// test whether "nonempty" exclude ids condition will be skipped
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -296,7 +302,7 @@ func TestImportCollections(t *testing.T) {
|
||||
name: "empty collections",
|
||||
jsonData: `[]`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
},
|
||||
{
|
||||
name: "minimal collection import",
|
||||
@@ -306,7 +312,7 @@ func TestImportCollections(t *testing.T) {
|
||||
]`,
|
||||
deleteMissing: false,
|
||||
expectError: false,
|
||||
expectCollectionsCount: 9,
|
||||
expectCollectionsCount: 10,
|
||||
},
|
||||
{
|
||||
name: "minimal collection import + failed beforeRecordsSync",
|
||||
@@ -318,7 +324,7 @@ func TestImportCollections(t *testing.T) {
|
||||
},
|
||||
deleteMissing: false,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
},
|
||||
{
|
||||
name: "minimal collection import + successful beforeRecordsSync",
|
||||
@@ -330,7 +336,7 @@ func TestImportCollections(t *testing.T) {
|
||||
},
|
||||
deleteMissing: false,
|
||||
expectError: false,
|
||||
expectCollectionsCount: 8,
|
||||
expectCollectionsCount: 9,
|
||||
},
|
||||
{
|
||||
name: "new + update + delete system collection",
|
||||
@@ -366,7 +372,7 @@ func TestImportCollections(t *testing.T) {
|
||||
]`,
|
||||
deleteMissing: true,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
},
|
||||
{
|
||||
name: "new + update + delete non-system collection",
|
||||
@@ -495,7 +501,7 @@ func TestImportCollections(t *testing.T) {
|
||||
]`,
|
||||
deleteMissing: false,
|
||||
expectError: false,
|
||||
expectCollectionsCount: 8,
|
||||
expectCollectionsCount: 9,
|
||||
afterTestFunc: func(testApp *tests.TestApp, resultCollections []*models.Collection) {
|
||||
expectedCollectionFields := map[string]int{
|
||||
"nologin": 1,
|
||||
@@ -503,6 +509,7 @@ func TestImportCollections(t *testing.T) {
|
||||
"demo2": 2,
|
||||
"demo3": 2,
|
||||
"demo4": 11,
|
||||
"demo5": 5,
|
||||
"new_import": 1,
|
||||
}
|
||||
for name, expectedCount := range expectedCollectionFields {
|
||||
|
||||
+18
-47
@@ -3,7 +3,6 @@ package daos
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -84,7 +83,7 @@ func (dao *Dao) FindRecordsByIds(
|
||||
}
|
||||
}
|
||||
|
||||
rows := []dbx.NullStringMap{}
|
||||
rows := make([]dbx.NullStringMap, 0, len(recordIds))
|
||||
if err := query.All(&rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -192,8 +191,7 @@ func (dao *Dao) IsRecordValueUnique(
|
||||
AndWhere(expr).
|
||||
Limit(1)
|
||||
|
||||
if len(excludeIds) > 0 {
|
||||
uniqueExcludeIds := list.NonzeroUniques(excludeIds)
|
||||
if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
|
||||
query.AndWhere(dbx.NotIn(collection.Name+".id", list.ToInterfaceSlice(uniqueExcludeIds)...))
|
||||
}
|
||||
|
||||
@@ -390,8 +388,7 @@ func (dao *Dao) DeleteRecord(record *models.Record) error {
|
||||
})
|
||||
}
|
||||
|
||||
// cascadeRecordDelete triggers cascade deletion for the provided references
|
||||
// and split the work to a batched set of go routines.
|
||||
// cascadeRecordDelete triggers cascade deletion for the provided references.
|
||||
//
|
||||
// NB! This method is expected to be called inside a transaction.
|
||||
func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.Collection][]*schema.SchemaField) error {
|
||||
@@ -401,20 +398,21 @@ func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.
|
||||
for _, field := range fields {
|
||||
recordTableName := inflector.Columnify(refCollection.Name)
|
||||
prefixedFieldName := recordTableName + "." + inflector.Columnify(field.Name)
|
||||
|
||||
// @todo optimize single relation lookup in v0.12+
|
||||
query := dao.RecordQuery(refCollection).
|
||||
Distinct(true).
|
||||
LeftJoin(fmt.Sprintf(
|
||||
// note: the case is used to normalize value access for single and multiple relations.
|
||||
AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id})).
|
||||
InnerJoin(fmt.Sprintf(
|
||||
// note: the case is used to normalize the value access
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{%s}}`,
|
||||
prefixedFieldName, prefixedFieldName, prefixedFieldName, uniqueJsonEachAlias,
|
||||
), nil).
|
||||
AndWhere(dbx.Not(dbx.HashExp{recordTableName + ".id": mainRecord.Id})).
|
||||
AndWhere(dbx.HashExp{uniqueJsonEachAlias + ".value": mainRecord.Id})
|
||||
), dbx.HashExp{uniqueJsonEachAlias + ".value": mainRecord.Id})
|
||||
|
||||
// trigger cascade for each 1000 rel items until there is none
|
||||
batchSize := 1000
|
||||
// trigger cascade for each batchSize rel items until there is none
|
||||
batchSize := 4000
|
||||
rows := make([]dbx.NullStringMap, 0, batchSize)
|
||||
for {
|
||||
rows := make([]dbx.NullStringMap, 0, batchSize)
|
||||
if err := query.Limit(int64(batchSize)).All(&rows); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -424,45 +422,18 @@ func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.
|
||||
break
|
||||
}
|
||||
|
||||
perWorker := 50
|
||||
workers := int(math.Ceil(float64(total) / float64(perWorker)))
|
||||
refRecords := models.NewRecordsFromNullStringMaps(refCollection, rows)
|
||||
|
||||
batchErr := func() error {
|
||||
ch := make(chan error)
|
||||
defer close(ch)
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
var chunks []dbx.NullStringMap
|
||||
if len(rows) <= perWorker {
|
||||
chunks = rows
|
||||
rows = nil
|
||||
} else {
|
||||
chunks = rows[:perWorker]
|
||||
rows = rows[perWorker:]
|
||||
}
|
||||
|
||||
go func() {
|
||||
refRecords := models.NewRecordsFromNullStringMaps(refCollection, chunks)
|
||||
ch <- dao.deleteRefRecords(mainRecord, refRecords, field)
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
if err := <-ch; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
if batchErr != nil {
|
||||
return batchErr
|
||||
err := dao.deleteRefRecords(mainRecord, refRecords, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if total < batchSize {
|
||||
break // no more items
|
||||
}
|
||||
|
||||
rows = rows[:0] // keep allocated memory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -663,11 +663,11 @@ func TestDeleteRecord(t *testing.T) {
|
||||
}
|
||||
// ensure that the json rel fields were prefixed
|
||||
joinedQueries := strings.Join(calledQueries, " ")
|
||||
expectedRelManyJoin := "`demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END)"
|
||||
expectedRelManyJoin := "`demo1` INNER JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END)"
|
||||
if !strings.Contains(joinedQueries, expectedRelManyJoin) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyJoin, calledQueries)
|
||||
}
|
||||
expectedRelOneJoin := "`demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_one]]) THEN [[demo1.rel_one]] ELSE json_array([[demo1.rel_one]]) END)"
|
||||
expectedRelOneJoin := "`demo1` INNER JOIN json_each(CASE WHEN json_valid([[demo1.rel_one]]) THEN [[demo1.rel_one]] ELSE json_array([[demo1.rel_one]]) END)"
|
||||
if !strings.Contains(joinedQueries, expectedRelOneJoin) {
|
||||
t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOneJoin, calledQueries)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func (form *CollectionUpsert) ensureExistingRelationCollectionId(value any) erro
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := form.dao.FindCollectionByNameOrId(options.CollectionId); err != nil {
|
||||
if err := form.dao.FindById(&models.Collection{}, options.CollectionId); err != nil {
|
||||
return validation.Errors{fmt.Sprint(i): validation.NewError(
|
||||
"validation_field_invalid_relation",
|
||||
"The relation collection doesn't exist.",
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
"collections": []
|
||||
}`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
expectEvents: nil,
|
||||
},
|
||||
{
|
||||
@@ -82,7 +82,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
]
|
||||
}`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 2,
|
||||
},
|
||||
@@ -101,7 +101,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
]
|
||||
}`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 2,
|
||||
},
|
||||
@@ -137,7 +137,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
]
|
||||
}`,
|
||||
expectError: false,
|
||||
expectCollectionsCount: 10,
|
||||
expectCollectionsCount: 11,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 3,
|
||||
"OnModelAfterCreate": 3,
|
||||
@@ -160,7 +160,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
]
|
||||
}`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeCreate": 1,
|
||||
},
|
||||
@@ -202,7 +202,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
]
|
||||
}`,
|
||||
expectError: true,
|
||||
expectCollectionsCount: 7,
|
||||
expectCollectionsCount: 8,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeDelete": 5,
|
||||
},
|
||||
@@ -253,7 +253,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
]
|
||||
}`,
|
||||
expectError: false,
|
||||
expectCollectionsCount: 9,
|
||||
expectCollectionsCount: 10,
|
||||
expectEvents: map[string]int{
|
||||
"OnModelBeforeUpdate": 1,
|
||||
"OnModelAfterUpdate": 1,
|
||||
@@ -341,8 +341,8 @@ func TestCollectionsImportSubmit(t *testing.T) {
|
||||
"OnModelAfterUpdate": 2,
|
||||
"OnModelBeforeCreate": 1,
|
||||
"OnModelAfterCreate": 1,
|
||||
"OnModelBeforeDelete": 5,
|
||||
"OnModelAfterDelete": 5,
|
||||
"OnModelBeforeDelete": 6,
|
||||
"OnModelAfterDelete": 6,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+40
-36
@@ -7,7 +7,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@@ -219,11 +218,6 @@ func (form *RecordUpsert) extractMultipartFormData(
|
||||
// and lods it into the form.
|
||||
//
|
||||
// File upload is supported only via multipart/form-data.
|
||||
//
|
||||
// To DELETE previously uploaded file(s) you can suffix the field name
|
||||
// with the file index or filename (eg. `myfile.0`) and set it to null or empty string.
|
||||
// For single file upload fields, you can skip the index and directly
|
||||
// reset the field using its field name (eg. `myfile = null`).
|
||||
func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error {
|
||||
requestData, uploadedFiles, err := form.extractRequestData(r, keyPrefix)
|
||||
if err != nil {
|
||||
@@ -345,29 +339,24 @@ func (form *RecordUpsert) RemoveFiles(key string, toDelete ...string) error {
|
||||
}
|
||||
|
||||
// LoadData loads and normalizes the provided regular record data fields into the form.
|
||||
//
|
||||
// To DELETE previously uploaded file(s) you can suffix the field name
|
||||
// with the file index or filename (eg. `myfile.0`) and set it to null or empty string.
|
||||
// For single file upload fields, you can skip the index and directly
|
||||
// reset the field using its field name (eg. `myfile = null`).
|
||||
func (form *RecordUpsert) LoadData(requestData map[string]any) error {
|
||||
// load base system fields
|
||||
if v, ok := requestData["id"]; ok {
|
||||
if v, ok := requestData[schema.FieldNameId]; ok {
|
||||
form.Id = cast.ToString(v)
|
||||
}
|
||||
|
||||
// load auth system fields
|
||||
if form.record.Collection().IsAuth() {
|
||||
if v, ok := requestData["username"]; ok {
|
||||
if v, ok := requestData[schema.FieldNameUsername]; ok {
|
||||
form.Username = cast.ToString(v)
|
||||
}
|
||||
if v, ok := requestData["email"]; ok {
|
||||
if v, ok := requestData[schema.FieldNameEmail]; ok {
|
||||
form.Email = cast.ToString(v)
|
||||
}
|
||||
if v, ok := requestData["emailVisibility"]; ok {
|
||||
if v, ok := requestData[schema.FieldNameEmailVisibility]; ok {
|
||||
form.EmailVisibility = cast.ToBool(v)
|
||||
}
|
||||
if v, ok := requestData["verified"]; ok {
|
||||
if v, ok := requestData[schema.FieldNameVerified]; ok {
|
||||
form.Verified = cast.ToBool(v)
|
||||
}
|
||||
if v, ok := requestData["password"]; ok {
|
||||
@@ -381,8 +370,16 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
|
||||
}
|
||||
}
|
||||
|
||||
// extend the record schema data with the request data
|
||||
extendedData := form.record.SchemaData()
|
||||
// replace modifiers (if any)
|
||||
requestData = form.record.ReplaceModifers(requestData)
|
||||
|
||||
// create a shallow copy of form.data
|
||||
var extendedData = make(map[string]any, len(form.data))
|
||||
for k, v := range form.data {
|
||||
extendedData[k] = v
|
||||
}
|
||||
|
||||
// extend form.data with the request data
|
||||
rawData, err := json.Marshal(requestData)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -393,8 +390,7 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
|
||||
|
||||
for _, field := range form.record.Collection().Schema.Fields() {
|
||||
key := field.Name
|
||||
value := extendedData[key]
|
||||
value = field.PrepareValue(value)
|
||||
value := field.PrepareValue(extendedData[key])
|
||||
|
||||
if field.Type != schema.FieldTypeFile {
|
||||
form.data[key] = value
|
||||
@@ -405,30 +401,32 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
|
||||
// Delete previously uploaded file(s)
|
||||
// -----------------------------------------------------------
|
||||
|
||||
oldNames := list.ToUniqueStringSlice(form.data[key])
|
||||
oldNames := form.record.GetStringSlice(key)
|
||||
submittedNames := list.ToUniqueStringSlice(value)
|
||||
|
||||
// ensure that all submitted names are existing to prevent accidental files deletions
|
||||
if len(submittedNames) > len(oldNames) || len(list.SubtractSlice(submittedNames, oldNames)) != 0 {
|
||||
return validation.Errors{
|
||||
key: validation.NewError(
|
||||
"validation_unknown_filenames",
|
||||
"The field contains unknown filenames.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// if empty value was set, mark all previously uploaded files for deletion
|
||||
if len(list.ToUniqueStringSlice(value)) == 0 && len(oldNames) > 0 {
|
||||
// otherwise check for "deleted" (aka. unsubmitted) file names
|
||||
if len(submittedNames) == 0 && len(oldNames) > 0 {
|
||||
form.RemoveFiles(key)
|
||||
} else if len(oldNames) > 0 {
|
||||
|
||||
toDelete := []string{}
|
||||
|
||||
// search for individual file name to delete (eg. "file.test.png = null")
|
||||
for _, name := range oldNames {
|
||||
if v, ok := extendedData[key+"."+name]; ok && cast.ToString(v) == "" {
|
||||
// submitted as a modifier or a new array
|
||||
if !list.ExistInSlice(name, submittedNames) {
|
||||
toDelete = append(toDelete, name)
|
||||
}
|
||||
}
|
||||
|
||||
// search for individual file index to delete (eg. "file.0 = null")
|
||||
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
|
||||
for indexedKey := range extendedData {
|
||||
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
|
||||
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
|
||||
if indexErr != nil || index >= len(oldNames) {
|
||||
continue
|
||||
}
|
||||
toDelete = append(toDelete, oldNames[index])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +434,12 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
|
||||
form.RemoveFiles(key, toDelete...)
|
||||
}
|
||||
}
|
||||
|
||||
// allow file key reasignments for file names sorting
|
||||
// (only if all submitted values already exists)
|
||||
if len(submittedNames) > 0 && len(list.SubtractSlice(submittedNames, oldNames)) == 0 {
|
||||
form.data[key] = submittedNames
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+16
-14
@@ -89,9 +89,10 @@ func TestRecordUpsertLoadRequestJson(t *testing.T) {
|
||||
"unknown": "test456",
|
||||
// file fields unset/delete
|
||||
"file_one": nil,
|
||||
"file_many.0": "", // delete by index
|
||||
"file_many.1": "test.png", // should be ignored
|
||||
"file_many.300_WlbFWSGmW9.png": nil, // delete by filename
|
||||
"file_many.0": "", // delete by index
|
||||
"file_many-": []string{"test_MaWC6mWyrP.txt", "test_tC1Yc87DfC.txt"}, // multiple delete with modifier
|
||||
"file_many.300_WlbFWSGmW9.png": nil, // delete by filename
|
||||
"file_many.2": "test.png", // should be ignored
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -149,11 +150,12 @@ func TestRecordUpsertLoadRequestMultipart(t *testing.T) {
|
||||
"a.b.text": "test123",
|
||||
"a.b.unknown": "test456",
|
||||
// file fields unset/delete
|
||||
"a.b.file_one": "",
|
||||
"a.b.file_many.0": "",
|
||||
"a.b.file_many.300_WlbFWSGmW9.png": "test.png", // delete by name
|
||||
"a.b.file_many.1": "test.png", // should be ignored
|
||||
}, "file_many")
|
||||
"a.b.file_one-": "test_d61b33QdDU.txt", // delete with modifier
|
||||
"a.b.file_many.0": "", // delete by index
|
||||
"a.b.file_many-": "test_tC1Yc87DfC.txt", // delete with modifier
|
||||
"a.b.file_many.300_WlbFWSGmW9.png": "", // delete by filename
|
||||
"a.b.file_many.2": "test.png", // should be ignored
|
||||
}, "a.b.file_many")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -191,7 +193,7 @@ func TestRecordUpsertLoadRequestMultipart(t *testing.T) {
|
||||
t.Fatal("Expect file_many field to be set")
|
||||
}
|
||||
manyfilesRemains := len(list.ToUniqueStringSlice(fileMany))
|
||||
expectedRemains := 2 // -2 from 3 removed + 1 new upload
|
||||
expectedRemains := 3 // 5 old; 3 deleted and 1 new uploaded
|
||||
if manyfilesRemains != expectedRemains {
|
||||
t.Fatalf("Expect file_many to be %d, got %d (%v)", expectedRemains, manyfilesRemains, fileMany)
|
||||
}
|
||||
@@ -465,7 +467,7 @@ func TestRecordUpsertSubmitFailure(t *testing.T) {
|
||||
if v := recordAfter.Get("email"); v == "invalid" {
|
||||
t.Fatalf("Expected record.email not to change, got %v", v)
|
||||
}
|
||||
if v := recordAfter.GetStringSlice("file_many"); len(v) != 3 {
|
||||
if v := recordAfter.GetStringSlice("file_many"); len(v) != 5 {
|
||||
t.Fatalf("Expected record.file_many not to change, got %v", v)
|
||||
}
|
||||
|
||||
@@ -537,8 +539,8 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
fileMany := (recordAfter.GetStringSlice("file_many"))
|
||||
if len(fileMany) != 4 { // 1 replace + 1 new
|
||||
t.Fatalf("Expected 4 record.file_many, got %d (%v)", len(fileMany), fileMany)
|
||||
if len(fileMany) != 6 { // 1 replace + 1 new
|
||||
t.Fatalf("Expected 6 record.file_many, got %d (%v)", len(fileMany), fileMany)
|
||||
}
|
||||
for _, f := range fileMany {
|
||||
if !hasRecordFile(app, recordAfter, f) {
|
||||
@@ -1009,7 +1011,7 @@ func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
fileMany := recordAfter.GetStringSlice("file_many")
|
||||
if len(fileMany) != 3 {
|
||||
t.Fatalf("Expected file_many to be 3, got %v", fileMany)
|
||||
if len(fileMany) != 5 {
|
||||
t.Fatalf("Expected file_many to be 5, got %v", fileMany)
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/aws/aws-sdk-go v1.44.165
|
||||
github.com/aws/aws-sdk-go v1.44.178
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/domodwyer/mailyak/v3 v3.3.4
|
||||
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86
|
||||
github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/ganigeorgiev/fexpr v0.1.1
|
||||
github.com/ganigeorgiev/fexpr v0.3.0
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
|
||||
@@ -19,22 +19,22 @@ require (
|
||||
github.com/pocketbase/dbx v1.8.0
|
||||
github.com/spf13/cast v1.5.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
gocloud.dev v0.27.0
|
||||
golang.org/x/crypto v0.4.0
|
||||
golang.org/x/net v0.4.0
|
||||
golang.org/x/oauth2 v0.3.0
|
||||
gocloud.dev v0.28.0
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/net v0.5.0
|
||||
golang.org/x/oauth2 v0.4.0
|
||||
golang.org/x/sync v0.1.0
|
||||
modernc.org/sqlite v1.20.0
|
||||
modernc.org/sqlite v1.20.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.47 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect
|
||||
@@ -43,10 +43,10 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
@@ -60,25 +60,25 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/image v0.2.0 // indirect
|
||||
golang.org/x/image v0.3.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/term v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.4.0 // indirect
|
||||
golang.org/x/tools v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.105.0 // indirect
|
||||
google.golang.org/api v0.106.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect
|
||||
google.golang.org/grpc v1.51.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.52.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
|
||||
@@ -133,6 +133,7 @@ func init() {
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
// This migration replaces for backward compatibility the default operators
|
||||
// (=, !=, >, etc.) with their any/opt equivalent (?=, ?=, ?>, etc.)
|
||||
// in any muli-rel expression collection rule.
|
||||
func init() {
|
||||
AppMigrations.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
exprRegex := regexp.MustCompile(`([\@\'\"\w\.]+)\s*(=|!=|~|!~|>|>=|<|<=)\s*([\@\'\"\w\.]+)`)
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := dao.CollectionQuery().All(&collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
findCollection := func(nameOrId string) *models.Collection {
|
||||
for _, c := range collections {
|
||||
if c.Id == nameOrId || c.Name == nameOrId {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var isMultiRelLiteral func(mainCollection *models.Collection, literal string) bool
|
||||
isMultiRelLiteral = func(mainCollection *models.Collection, literal string) bool {
|
||||
if strings.HasPrefix(literal, "@collection.") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(literal, `"`) ||
|
||||
strings.HasPrefix(literal, `'`) ||
|
||||
strings.HasPrefix(literal, "@request.method") ||
|
||||
strings.HasPrefix(literal, "@request.data") ||
|
||||
strings.HasPrefix(literal, "@request.query") {
|
||||
return false
|
||||
}
|
||||
|
||||
parts := strings.Split(literal, ".")
|
||||
if len(parts) <= 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(literal, "@request.auth") && len(parts) >= 4 {
|
||||
// check each auth collection
|
||||
for _, c := range collections {
|
||||
if c.IsAuth() && isMultiRelLiteral(c, strings.Join(parts[2:], ".")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
activeCollection := mainCollection
|
||||
|
||||
for i, p := range parts {
|
||||
f := activeCollection.Schema.GetFieldByName(p)
|
||||
if f == nil || f.Type != schema.FieldTypeRelation {
|
||||
return false // not a relation field
|
||||
}
|
||||
|
||||
// is multi-relation and not the last prop
|
||||
opt, ok := f.Options.(*schema.RelationOptions)
|
||||
if ok && (opt.MaxSelect == nil || *opt.MaxSelect != 1) && i != len(parts)-1 {
|
||||
return true
|
||||
}
|
||||
|
||||
activeCollection = findCollection(opt.CollectionId)
|
||||
if activeCollection == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// replace all multi-match operators to their any/opt equivalent, eg. "=" => "?="
|
||||
migrateRule := func(collection *models.Collection, rule *string) (*string, error) {
|
||||
if rule == nil || *rule == "" {
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
newRule := *rule
|
||||
parts := exprRegex.FindAllStringSubmatch(newRule, -1)
|
||||
|
||||
for _, p := range parts {
|
||||
if isMultiRelLiteral(collection, p[1]) || isMultiRelLiteral(collection, p[3]) {
|
||||
newRule = strings.ReplaceAll(newRule, p[0], p[1]+" ?"+p[2]+" "+p[3])
|
||||
}
|
||||
}
|
||||
|
||||
return &newRule, nil
|
||||
}
|
||||
|
||||
var ruleErr error
|
||||
for _, c := range collections {
|
||||
c.ListRule, ruleErr = migrateRule(c, c.ListRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.ViewRule, ruleErr = migrateRule(c, c.ViewRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.CreateRule, ruleErr = migrateRule(c, c.CreateRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.UpdateRule, ruleErr = migrateRule(c, c.UpdateRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.DeleteRule, ruleErr = migrateRule(c, c.DeleteRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
if c.IsAuth() {
|
||||
opt := c.AuthOptions()
|
||||
opt.ManageRule, ruleErr = migrateRule(c, opt.ManageRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
c.SetOptions(opt)
|
||||
}
|
||||
|
||||
if err := dao.Save(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collections := []*models.Collection{}
|
||||
if err := dao.CollectionQuery().All(&collections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
anyOpRegex := regexp.MustCompile(`\?(=|!=|~|!~|>|>=|<|<=)`)
|
||||
|
||||
// replace any/opt operators to their old versions, eg. "?=" => "="
|
||||
revertRule := func(rule *string) (*string, error) {
|
||||
if rule == nil || *rule == "" {
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
newRule := *rule
|
||||
newRule = anyOpRegex.ReplaceAllString(newRule, "${1}")
|
||||
|
||||
return &newRule, nil
|
||||
}
|
||||
|
||||
var ruleErr error
|
||||
for _, c := range collections {
|
||||
c.ListRule, ruleErr = revertRule(c.ListRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.ViewRule, ruleErr = revertRule(c.ViewRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.CreateRule, ruleErr = revertRule(c.CreateRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.UpdateRule, ruleErr = revertRule(c.UpdateRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
c.DeleteRule, ruleErr = revertRule(c.DeleteRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
|
||||
if c.IsAuth() {
|
||||
opt := c.AuthOptions()
|
||||
opt.ManageRule, ruleErr = revertRule(opt.ManageRule)
|
||||
if ruleErr != nil {
|
||||
return ruleErr
|
||||
}
|
||||
c.SetOptions(opt)
|
||||
}
|
||||
|
||||
if err := dao.Save(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+103
-6
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -57,7 +59,7 @@ func nullStringMapValue(data dbx.NullStringMap, key string) any {
|
||||
// NewRecordFromNullStringMap initializes a single new Record model
|
||||
// with data loaded from the provided NullStringMap.
|
||||
func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record {
|
||||
resultMap := map[string]any{}
|
||||
resultMap := make(map[string]any, len(data))
|
||||
|
||||
// load schema fields
|
||||
for _, field := range collection.Schema.Fields() {
|
||||
@@ -140,7 +142,7 @@ func (m *Record) SetExpand(expand map[string]any) {
|
||||
// Otherwise the "old" expanded record will be replace with the "new" one (aka. a :merge: aNew => aNew).
|
||||
func (m *Record) MergeExpand(expand map[string]any) {
|
||||
if m.expand == nil && len(expand) > 0 {
|
||||
m.expand = make(map[string]any)
|
||||
m.expand = make(map[string]any, len(expand))
|
||||
}
|
||||
|
||||
for key, new := range expand {
|
||||
@@ -194,7 +196,7 @@ func (m *Record) MergeExpand(expand map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
if wasOldSlice || wasNewSlice {
|
||||
if wasOldSlice || wasNewSlice || len(oldSlice) == 0 {
|
||||
m.expand[key] = oldSlice
|
||||
} else {
|
||||
m.expand[key] = oldSlice[0]
|
||||
@@ -204,7 +206,7 @@ func (m *Record) MergeExpand(expand map[string]any) {
|
||||
|
||||
// SchemaData returns a shallow copy ONLY of the defined record schema fields data.
|
||||
func (m *Record) SchemaData() map[string]any {
|
||||
result := map[string]any{}
|
||||
result := make(map[string]any, len(m.collection.Schema.Fields()))
|
||||
|
||||
for _, field := range m.collection.Schema.Fields() {
|
||||
if v, ok := m.data[field.Name]; ok {
|
||||
@@ -370,7 +372,7 @@ func (m *Record) Load(data map[string]any) {
|
||||
|
||||
// ColumnValueMap implements [ColumnValueMapper] interface.
|
||||
func (m *Record) ColumnValueMap() map[string]any {
|
||||
result := map[string]any{}
|
||||
result := make(map[string]any, len(m.collection.Schema.Fields())+3)
|
||||
|
||||
// export schema field values
|
||||
for _, field := range m.collection.Schema.Fields() {
|
||||
@@ -396,7 +398,7 @@ func (m *Record) ColumnValueMap() map[string]any {
|
||||
//
|
||||
// Fields marked as hidden will be exported only if `m.IgnoreEmailVisibility(true)` is set.
|
||||
func (m *Record) PublicExport() map[string]any {
|
||||
result := map[string]any{}
|
||||
result := make(map[string]any, len(m.collection.Schema.Fields())+5)
|
||||
|
||||
// export unknown data fields if allowed
|
||||
if m.exportUnknown {
|
||||
@@ -457,6 +459,101 @@ func (m *Record) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceModifers returns a new map with applied modifier
|
||||
// values based on the current record and the specified data.
|
||||
//
|
||||
// The resolved modifier keys will be removed.
|
||||
//
|
||||
// Multiple modifiers will be applied one after another,
|
||||
// while reusing the previous base key value result (eg. 1; -5; +2 => -2).
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// newData := record.ReplaceModifers(data)
|
||||
// // record: {"field": 10}
|
||||
// // data: {"field+": 5}
|
||||
// // newData: {"field": 15}
|
||||
func (m *Record) ReplaceModifers(data map[string]any) map[string]any {
|
||||
var clone = shallowCopy(data)
|
||||
if len(clone) == 0 {
|
||||
return clone
|
||||
}
|
||||
|
||||
var recordDataCache map[string]any
|
||||
|
||||
// export recordData lazily
|
||||
recordData := func() map[string]any {
|
||||
if recordDataCache == nil {
|
||||
recordDataCache = m.SchemaData()
|
||||
}
|
||||
return recordDataCache
|
||||
}
|
||||
|
||||
modifiers := schema.FieldValueModifiers()
|
||||
|
||||
for _, field := range m.Collection().Schema.Fields() {
|
||||
key := field.Name
|
||||
|
||||
for _, m := range modifiers {
|
||||
if mv, mOk := clone[key+m]; mOk {
|
||||
if _, ok := clone[key]; !ok {
|
||||
// get base value from the merged data
|
||||
clone[key] = recordData()[key]
|
||||
}
|
||||
|
||||
clone[key] = field.PrepareValueWithModifier(clone[key], m, mv)
|
||||
delete(clone, key+m)
|
||||
}
|
||||
}
|
||||
|
||||
if field.Type != schema.FieldTypeFile {
|
||||
continue
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// legacy file field modifiers (kept for backward compatability)
|
||||
// -----------------------------------------------------------
|
||||
|
||||
var oldNames []string
|
||||
var toDelete []string
|
||||
if _, ok := clone[key]; ok {
|
||||
oldNames = list.ToUniqueStringSlice(clone[key])
|
||||
} else {
|
||||
// get oldNames from the model
|
||||
oldNames = list.ToUniqueStringSlice(recordData()[key])
|
||||
}
|
||||
|
||||
// search for individual file name to delete (eg. "file.test.png = null")
|
||||
for _, name := range oldNames {
|
||||
suffixedKey := key + "." + name
|
||||
if v, ok := clone[suffixedKey]; ok && cast.ToString(v) == "" {
|
||||
toDelete = append(toDelete, name)
|
||||
delete(clone, suffixedKey)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// search for individual file index to delete (eg. "file.0 = null")
|
||||
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
|
||||
for indexedKey := range clone {
|
||||
if keyExp.MatchString(indexedKey) && cast.ToString(clone[indexedKey]) == "" {
|
||||
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
|
||||
if indexErr != nil || index < 0 || index >= len(oldNames) {
|
||||
continue
|
||||
}
|
||||
toDelete = append(toDelete, oldNames[index])
|
||||
delete(clone, indexedKey)
|
||||
}
|
||||
}
|
||||
|
||||
if toDelete != nil {
|
||||
clone[key] = field.PrepareValue(list.SubtractSlice(oldNames, toDelete))
|
||||
}
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// getNormalizeDataValueForDB returns the "key" data value formatted for db storage.
|
||||
func (m *Record) getNormalizeDataValueForDB(key string) any {
|
||||
var val any
|
||||
|
||||
+106
-3
@@ -138,7 +138,7 @@ func TestNewRecordFromNullStringMap(t *testing.T) {
|
||||
Valid: true,
|
||||
},
|
||||
"field5": sql.NullString{
|
||||
String: `["test1","test2"]`, // will select only the first elem
|
||||
String: `["test1","test2"]`, // will select only the last elem
|
||||
Valid: true,
|
||||
},
|
||||
"field6": sql.NullString{
|
||||
@@ -157,11 +157,11 @@ func TestNewRecordFromNullStringMap(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
models.CollectionTypeBase,
|
||||
`{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test1","field6":["test"],"id":"test_id","updated":"2022-01-01 10:00:00.456Z"}`,
|
||||
`{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test2","field6":["test"],"id":"test_id","updated":"2022-01-01 10:00:00.456Z"}`,
|
||||
},
|
||||
{
|
||||
models.CollectionTypeAuth,
|
||||
`{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","email":"test_email","emailVisibility":true,"field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test1","field6":["test"],"id":"test_id","updated":"2022-01-01 10:00:00.456Z","username":"test_username","verified":false}`,
|
||||
`{"collectionId":"","collectionName":"test","created":"2022-01-01 10:00:00.123Z","email":"test_email","emailVisibility":true,"field1":"test","field2":"","field3":true,"field4":123.123,"field5":"test2","field6":["test"],"id":"test_id","updated":"2022-01-01 10:00:00.456Z","username":"test_username","verified":false}`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1425,6 +1425,109 @@ func TestRecordUnmarshalJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordReplaceModifers(t *testing.T) {
|
||||
collection := &models.Collection{
|
||||
Schema: schema.NewSchema(
|
||||
&schema.SchemaField{
|
||||
Name: "text",
|
||||
Type: schema.FieldTypeText,
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "number",
|
||||
Type: schema.FieldTypeNumber,
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "rel_one",
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "rel_many",
|
||||
Type: schema.FieldTypeRelation,
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "select_one",
|
||||
Type: schema.FieldTypeSelect,
|
||||
Options: &schema.SelectOptions{MaxSelect: 1},
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "select_many",
|
||||
Type: schema.FieldTypeSelect,
|
||||
Options: &schema.SelectOptions{MaxSelect: 10},
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "file_one",
|
||||
Type: schema.FieldTypeFile,
|
||||
Options: &schema.FileOptions{MaxSelect: 1},
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "file_one_index",
|
||||
Type: schema.FieldTypeFile,
|
||||
Options: &schema.FileOptions{MaxSelect: 1},
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "file_one_name",
|
||||
Type: schema.FieldTypeFile,
|
||||
Options: &schema.FileOptions{MaxSelect: 1},
|
||||
},
|
||||
&schema.SchemaField{
|
||||
Name: "file_many",
|
||||
Type: schema.FieldTypeFile,
|
||||
Options: &schema.FileOptions{MaxSelect: 10},
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
record := models.NewRecord(collection)
|
||||
|
||||
record.Load(map[string]any{
|
||||
"text": "test",
|
||||
"number": 10,
|
||||
"rel_one": "a",
|
||||
"rel_many": []string{"a", "b"},
|
||||
"select_one": "a",
|
||||
"select_many": []string{"a", "b", "c"},
|
||||
"file_one": "a",
|
||||
"file_one_index": "b",
|
||||
"file_one_name": "c",
|
||||
"file_many": []string{"a", "b", "c", "d", "e", "f"},
|
||||
})
|
||||
|
||||
result := record.ReplaceModifers(map[string]any{
|
||||
"text-": "m-",
|
||||
"text+": "m+",
|
||||
"number-": 3,
|
||||
"number+": 5,
|
||||
"rel_one-": "a",
|
||||
"rel_one+": "b",
|
||||
"rel_many-": []string{"a"},
|
||||
"rel_many+": []string{"c", "d", "e"},
|
||||
"select_one-": "a",
|
||||
"select_one+": "c",
|
||||
"select_many-": []string{"b", "c"},
|
||||
"select_many+": []string{"d", "e"},
|
||||
"file_one+": "skip", // should be ignored
|
||||
"file_one-": "a",
|
||||
"file_one_index.0": "",
|
||||
"file_one_name.c": "",
|
||||
"file_many+": []string{"e", "f"}, // should be ignored
|
||||
"file_many-": []string{"c", "d"},
|
||||
"file_many.f": nil,
|
||||
"file_many.0": nil,
|
||||
})
|
||||
|
||||
raw, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `{"file_many":["b","e"],"file_one":"","file_one_index":"","file_one_name":"","number":12,"rel_many":["b","c","d","e"],"rel_one":"b","select_many":["a","d","e"],"select_one":"c","text":"test"}`
|
||||
|
||||
if v := string(raw); v != expected {
|
||||
t.Fatalf("Expected \n%s, \ngot \n%s", expected, v)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Auth helpers:
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
// RequestData defines a HTTP request data struct, usually used
|
||||
// as part of the `@request.*` filter resolver.
|
||||
type RequestData struct {
|
||||
@@ -9,3 +15,18 @@ type RequestData struct {
|
||||
AuthRecord *Record `json:"authRecord"`
|
||||
Admin *Admin `json:"admin"`
|
||||
}
|
||||
|
||||
// HasModifierDataKeys loosely checks if the current struct has any modifier Data keys.
|
||||
func (r *RequestData) HasModifierDataKeys() bool {
|
||||
allModifiers := schema.FieldValueModifiers()
|
||||
|
||||
for key := range r.Data {
|
||||
for _, m := range allModifiers {
|
||||
if strings.HasSuffix(key, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func TestRequestDataHasModifierDataKeys(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
requestData *models.RequestData
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
&models.RequestData{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Data with regular fields",
|
||||
&models.RequestData{
|
||||
Query: map[string]any{"data+": "demo"}, // should be ignored
|
||||
Data: map[string]any{"a": 123, "b": "test", "c.d": false},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Data with +modifier fields",
|
||||
&models.RequestData{
|
||||
Data: map[string]any{"a+": 123, "b": "test", "c.d": false},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Data with -modifier fields",
|
||||
&models.RequestData{
|
||||
Data: map[string]any{"a": 123, "b-": "test", "c.d": false},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Data with mixed modifier fields",
|
||||
&models.RequestData{
|
||||
Data: map[string]any{"a": 123, "b-": "test", "c.d+": false},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.requestData.HasModifierDataKeys()
|
||||
|
||||
if result != s.expected {
|
||||
t.Fatalf("[%s] Expected %v, got %v", s.name, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,22 +15,36 @@ import (
|
||||
|
||||
var schemaFieldNameRegex = regexp.MustCompile(`^\w+$`)
|
||||
|
||||
// field value modifiers
|
||||
const (
|
||||
FieldValueModifierAdd string = "+"
|
||||
FieldValueModifierSubtract string = "-"
|
||||
)
|
||||
|
||||
// FieldValueModifiers returns a list with all available field modifier tokens.
|
||||
func FieldValueModifiers() []string {
|
||||
return []string{
|
||||
FieldValueModifierAdd,
|
||||
FieldValueModifierSubtract,
|
||||
}
|
||||
}
|
||||
|
||||
// commonly used field names
|
||||
const (
|
||||
FieldNameId = "id"
|
||||
FieldNameCreated = "created"
|
||||
FieldNameUpdated = "updated"
|
||||
FieldNameCollectionId = "collectionId"
|
||||
FieldNameCollectionName = "collectionName"
|
||||
FieldNameExpand = "expand"
|
||||
FieldNameUsername = "username"
|
||||
FieldNameEmail = "email"
|
||||
FieldNameEmailVisibility = "emailVisibility"
|
||||
FieldNameVerified = "verified"
|
||||
FieldNameTokenKey = "tokenKey"
|
||||
FieldNamePasswordHash = "passwordHash"
|
||||
FieldNameLastResetSentAt = "lastResetSentAt"
|
||||
FieldNameLastVerificationSentAt = "lastVerificationSentAt"
|
||||
FieldNameId string = "id"
|
||||
FieldNameCreated string = "created"
|
||||
FieldNameUpdated string = "updated"
|
||||
FieldNameCollectionId string = "collectionId"
|
||||
FieldNameCollectionName string = "collectionName"
|
||||
FieldNameExpand string = "expand"
|
||||
FieldNameUsername string = "username"
|
||||
FieldNameEmail string = "email"
|
||||
FieldNameEmailVisibility string = "emailVisibility"
|
||||
FieldNameVerified string = "verified"
|
||||
FieldNameTokenKey string = "tokenKey"
|
||||
FieldNamePasswordHash string = "passwordHash"
|
||||
FieldNameLastResetSentAt string = "lastResetSentAt"
|
||||
FieldNameLastVerificationSentAt string = "lastVerificationSentAt"
|
||||
)
|
||||
|
||||
// BaseModelFieldNames returns the field names that all models have (id, created, updated).
|
||||
@@ -168,8 +182,8 @@ func (f SchemaField) Validate() error {
|
||||
f.InitOptions()
|
||||
|
||||
excludeNames := BaseModelFieldNames()
|
||||
// exclude filter literals
|
||||
excludeNames = append(excludeNames, "null", "true", "false")
|
||||
// exclude special filter literals
|
||||
excludeNames = append(excludeNames, "null", "true", "false", "isset")
|
||||
// exclude system literals
|
||||
excludeNames = append(excludeNames, SystemFieldNames()...)
|
||||
|
||||
@@ -276,7 +290,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
options, _ := f.Options.(*SelectOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(val) > 0 {
|
||||
return val[0]
|
||||
return val[len(val)-1] // the last selected
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -288,7 +302,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
options, _ := f.Options.(*FileOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(val) > 0 {
|
||||
return val[0]
|
||||
return val[len(val)-1] // the last selected
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -300,7 +314,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
options, _ := f.Options.(*RelationOptions)
|
||||
if options.MaxSelect != nil && *options.MaxSelect <= 1 {
|
||||
if len(ids) > 0 {
|
||||
return ids[0]
|
||||
return ids[len(ids)-1] // the last selected
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -311,6 +325,46 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareValueWithModifier returns normalized and properly formatted field value
|
||||
// by "merging" baseValue with the modifierValue based on the specified modifier (+ or -).
|
||||
func (f *SchemaField) PrepareValueWithModifier(baseValue any, modifier string, modifierValue any) any {
|
||||
resolvedValue := baseValue
|
||||
|
||||
switch f.Type {
|
||||
case FieldTypeNumber:
|
||||
switch modifier {
|
||||
case FieldValueModifierAdd:
|
||||
resolvedValue = cast.ToFloat64(baseValue) + cast.ToFloat64(modifierValue)
|
||||
case FieldValueModifierSubtract:
|
||||
resolvedValue = cast.ToFloat64(baseValue) - cast.ToFloat64(modifierValue)
|
||||
}
|
||||
case FieldTypeSelect, FieldTypeRelation:
|
||||
switch modifier {
|
||||
case FieldValueModifierAdd:
|
||||
resolvedValue = append(
|
||||
list.ToUniqueStringSlice(baseValue),
|
||||
list.ToUniqueStringSlice(modifierValue)...,
|
||||
)
|
||||
case FieldValueModifierSubtract:
|
||||
resolvedValue = list.SubtractSlice(
|
||||
list.ToUniqueStringSlice(baseValue),
|
||||
list.ToUniqueStringSlice(modifierValue),
|
||||
)
|
||||
}
|
||||
case FieldTypeFile:
|
||||
// note: file for now supports only the subtract modifier
|
||||
switch modifier {
|
||||
case FieldValueModifierSubtract:
|
||||
resolvedValue = list.SubtractSlice(
|
||||
list.ToUniqueStringSlice(baseValue),
|
||||
list.ToUniqueStringSlice(modifierValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return f.PrepareValue(resolvedValue)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// FieldOptions interfaces that defines common methods that every field options struct has.
|
||||
|
||||
@@ -603,7 +603,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{schema.SchemaField{Type: schema.FieldTypeSelect}, "", `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeSelect}, "test", `"test"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test1"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test2"`},
|
||||
{
|
||||
// no values validation/filtering
|
||||
schema.SchemaField{
|
||||
@@ -680,7 +680,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{schema.SchemaField{Type: schema.FieldTypeFile}, "", `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeFile}, "test", `"test"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test1"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test2"`},
|
||||
// file (multiple)
|
||||
{
|
||||
schema.SchemaField{
|
||||
@@ -785,7 +785,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`,
|
||||
`"2ba88b4f-e9da-42f0-9764-9a55c953e724"`,
|
||||
},
|
||||
// relation (multiple)
|
||||
{
|
||||
@@ -863,6 +863,696 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaFieldPrepareValueWithModifier(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
field schema.SchemaField
|
||||
baseValue any
|
||||
modifier string
|
||||
modifierValue any
|
||||
expectJson string
|
||||
}{
|
||||
// text
|
||||
{
|
||||
"text with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeText},
|
||||
"base",
|
||||
"+",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"text with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeText},
|
||||
"base",
|
||||
"-",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"text with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeText},
|
||||
"base",
|
||||
"?",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"text cast check",
|
||||
schema.SchemaField{Type: schema.FieldTypeText},
|
||||
123,
|
||||
"?",
|
||||
"new",
|
||||
`"123"`,
|
||||
},
|
||||
|
||||
// number
|
||||
{
|
||||
"number with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeNumber},
|
||||
1,
|
||||
"+",
|
||||
4,
|
||||
`5`,
|
||||
},
|
||||
{
|
||||
"number with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeNumber},
|
||||
1,
|
||||
"-",
|
||||
4,
|
||||
`-3`,
|
||||
},
|
||||
{
|
||||
"number with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeNumber},
|
||||
"1",
|
||||
"?",
|
||||
4,
|
||||
`1`,
|
||||
},
|
||||
{
|
||||
"number cast check",
|
||||
schema.SchemaField{Type: schema.FieldTypeNumber},
|
||||
"test",
|
||||
"+",
|
||||
"4",
|
||||
`4`,
|
||||
},
|
||||
|
||||
// bool
|
||||
{
|
||||
"bool with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeBool},
|
||||
true,
|
||||
"+",
|
||||
false,
|
||||
`true`,
|
||||
},
|
||||
{
|
||||
"bool with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeBool},
|
||||
true,
|
||||
"-",
|
||||
false,
|
||||
`true`,
|
||||
},
|
||||
{
|
||||
"bool with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeBool},
|
||||
true,
|
||||
"?",
|
||||
false,
|
||||
`true`,
|
||||
},
|
||||
{
|
||||
"bool cast check",
|
||||
schema.SchemaField{Type: schema.FieldTypeBool},
|
||||
"true",
|
||||
"?",
|
||||
false,
|
||||
`true`,
|
||||
},
|
||||
|
||||
// email
|
||||
{
|
||||
"email with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeEmail},
|
||||
"base",
|
||||
"+",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"email with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeEmail},
|
||||
"base",
|
||||
"-",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"email with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeEmail},
|
||||
"base",
|
||||
"?",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"email cast check",
|
||||
schema.SchemaField{Type: schema.FieldTypeEmail},
|
||||
123,
|
||||
"?",
|
||||
"new",
|
||||
`"123"`,
|
||||
},
|
||||
|
||||
// url
|
||||
{
|
||||
"url with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeUrl},
|
||||
"base",
|
||||
"+",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"url with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeUrl},
|
||||
"base",
|
||||
"-",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"url with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeUrl},
|
||||
"base",
|
||||
"?",
|
||||
"new",
|
||||
`"base"`,
|
||||
},
|
||||
{
|
||||
"url cast check",
|
||||
schema.SchemaField{Type: schema.FieldTypeUrl},
|
||||
123,
|
||||
"-",
|
||||
"new",
|
||||
`"123"`,
|
||||
},
|
||||
|
||||
// date
|
||||
{
|
||||
"date with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeDate},
|
||||
"2023-01-01 00:00:00.123",
|
||||
"+",
|
||||
"2023-02-01 00:00:00.456",
|
||||
`"2023-01-01 00:00:00.123Z"`,
|
||||
},
|
||||
{
|
||||
"date with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeDate},
|
||||
"2023-01-01 00:00:00.123Z",
|
||||
"-",
|
||||
"2023-02-01 00:00:00.456Z",
|
||||
`"2023-01-01 00:00:00.123Z"`,
|
||||
},
|
||||
{
|
||||
"date with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeDate},
|
||||
"2023-01-01 00:00:00.123",
|
||||
"?",
|
||||
"2023-01-01 00:00:00.456",
|
||||
`"2023-01-01 00:00:00.123Z"`,
|
||||
},
|
||||
{
|
||||
"date cast check",
|
||||
schema.SchemaField{Type: schema.FieldTypeDate},
|
||||
1672524000, // 2022-12-31 22:00:00.000Z
|
||||
"+",
|
||||
100,
|
||||
`"2022-12-31 22:00:00.000Z"`,
|
||||
},
|
||||
|
||||
// json
|
||||
{
|
||||
"json with '+' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeJson},
|
||||
10,
|
||||
"+",
|
||||
5,
|
||||
`10`,
|
||||
},
|
||||
{
|
||||
"json with '+' modifier (slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeJson},
|
||||
[]string{"a", "b"},
|
||||
"+",
|
||||
"c",
|
||||
`["a","b"]`,
|
||||
},
|
||||
{
|
||||
"json with '-' modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeJson},
|
||||
10,
|
||||
"-",
|
||||
5,
|
||||
`10`,
|
||||
},
|
||||
{
|
||||
"json with '-' modifier (slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeJson},
|
||||
`["a","b"]`,
|
||||
"-",
|
||||
"c",
|
||||
`["a","b"]`,
|
||||
},
|
||||
{
|
||||
"json with unknown modifier",
|
||||
schema.SchemaField{Type: schema.FieldTypeJson},
|
||||
`"base"`,
|
||||
"?",
|
||||
`"new"`,
|
||||
`"base"`,
|
||||
},
|
||||
|
||||
// single select
|
||||
{
|
||||
"single select with '+' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"",
|
||||
"+",
|
||||
"b",
|
||||
`"b"`,
|
||||
},
|
||||
{
|
||||
"single select with '+' modifier (nonempty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"+",
|
||||
"b",
|
||||
`"b"`,
|
||||
},
|
||||
{
|
||||
"single select with '-' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"",
|
||||
"-",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single select with '-' modifier (nonempty base and empty modifier value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
"",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single select with '-' modifier (nonempty base and different value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
"b",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single select with '-' modifier (nonempty base and matching value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single select with '-' modifier (nonempty base and matching value in a slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
[]string{"b", "a", "c", "123"},
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single select with unknown modifier (nonempty)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
|
||||
"",
|
||||
"?",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
|
||||
// multi select
|
||||
{
|
||||
"multi select with '+' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
nil,
|
||||
"+",
|
||||
"b",
|
||||
`["b"]`,
|
||||
},
|
||||
{
|
||||
"multi select with '+' modifier (nonempty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
[]string{"a"},
|
||||
"+",
|
||||
[]string{"b", "c"},
|
||||
`["a","b","c"]`,
|
||||
},
|
||||
{
|
||||
"multi select with '+' modifier (nonempty base; already existing value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b"},
|
||||
"+",
|
||||
"b",
|
||||
`["a","b"]`,
|
||||
},
|
||||
{
|
||||
"multi select with '-' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
nil,
|
||||
"-",
|
||||
[]string{"a"},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
"multi select with '-' modifier (nonempty base and empty modifier value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
"a",
|
||||
"-",
|
||||
"",
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi select with '-' modifier (nonempty base and different value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
"a",
|
||||
"-",
|
||||
"b",
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi select with '-' modifier (nonempty base and matching value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"-",
|
||||
"c",
|
||||
`["a","b","d"]`,
|
||||
},
|
||||
{
|
||||
"multi select with '-' modifier (nonempty base and matching value in a slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"-",
|
||||
[]string{"b", "a", "123"},
|
||||
`["c","d"]`,
|
||||
},
|
||||
{
|
||||
"multi select with unknown modifier (nonempty)",
|
||||
schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b"},
|
||||
"?",
|
||||
"a",
|
||||
`["a","b"]`,
|
||||
},
|
||||
|
||||
// single relation
|
||||
{
|
||||
"single relation with '+' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"",
|
||||
"+",
|
||||
"b",
|
||||
`"b"`,
|
||||
},
|
||||
{
|
||||
"single relation with '+' modifier (nonempty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"a",
|
||||
"+",
|
||||
"b",
|
||||
`"b"`,
|
||||
},
|
||||
{
|
||||
"single relation with '-' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"",
|
||||
"-",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single relation with '-' modifier (nonempty base and empty modifier value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"a",
|
||||
"-",
|
||||
"",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single relation with '-' modifier (nonempty base and different value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"a",
|
||||
"-",
|
||||
"b",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single relation with '-' modifier (nonempty base and matching value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"a",
|
||||
"-",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single relation with '-' modifier (nonempty base and matching value in a slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"a",
|
||||
"-",
|
||||
[]string{"b", "a", "c", "123"},
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single relation with unknown modifier (nonempty)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
"",
|
||||
"?",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
|
||||
// multi relation
|
||||
{
|
||||
"multi relation with '+' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
nil,
|
||||
"+",
|
||||
"b",
|
||||
`["b"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '+' modifier (nonempty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
[]string{"a"},
|
||||
"+",
|
||||
[]string{"b", "c"},
|
||||
`["a","b","c"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '+' modifier (nonempty base; already existing value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
[]string{"a", "b"},
|
||||
"+",
|
||||
"b",
|
||||
`["a","b"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '-' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
nil,
|
||||
"-",
|
||||
[]string{"a"},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '-' modifier (nonempty base and empty modifier value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
"a",
|
||||
"-",
|
||||
"",
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '-' modifier (nonempty base and different value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
"a",
|
||||
"-",
|
||||
"b",
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '-' modifier (nonempty base and matching value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"-",
|
||||
"c",
|
||||
`["a","b","d"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with '-' modifier (nonempty base and matching value in a slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"-",
|
||||
[]string{"b", "a", "123"},
|
||||
`["c","d"]`,
|
||||
},
|
||||
{
|
||||
"multi relation with unknown modifier (nonempty)",
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
[]string{"a", "b"},
|
||||
"?",
|
||||
"a",
|
||||
`["a","b"]`,
|
||||
},
|
||||
|
||||
// single file
|
||||
{
|
||||
"single file with '+' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"",
|
||||
"+",
|
||||
"b",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single file with '+' modifier (nonempty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"+",
|
||||
"b",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single file with '-' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"",
|
||||
"-",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single file with '-' modifier (nonempty base and empty modifier value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
"",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single file with '-' modifier (nonempty base and different value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
"b",
|
||||
`"a"`,
|
||||
},
|
||||
{
|
||||
"single file with '-' modifier (nonempty base and matching value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single file with '-' modifier (nonempty base and matching value in a slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"a",
|
||||
"-",
|
||||
[]string{"b", "a", "c", "123"},
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
"single file with unknown modifier (nonempty)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
|
||||
"",
|
||||
"?",
|
||||
"a",
|
||||
`""`,
|
||||
},
|
||||
|
||||
// multi file
|
||||
{
|
||||
"multi file with '+' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
nil,
|
||||
"+",
|
||||
"b",
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
"multi file with '+' modifier (nonempty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
[]string{"a"},
|
||||
"+",
|
||||
[]string{"b", "c"},
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi file with '+' modifier (nonempty base; already existing value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b"},
|
||||
"+",
|
||||
"b",
|
||||
`["a","b"]`,
|
||||
},
|
||||
{
|
||||
"multi file with '-' modifier (empty base)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
nil,
|
||||
"-",
|
||||
[]string{"a"},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
"multi file with '-' modifier (nonempty base and empty modifier value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
"a",
|
||||
"-",
|
||||
"",
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi file with '-' modifier (nonempty base and different value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
"a",
|
||||
"-",
|
||||
"b",
|
||||
`["a"]`,
|
||||
},
|
||||
{
|
||||
"multi file with '-' modifier (nonempty base and matching value)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"-",
|
||||
"c",
|
||||
`["a","b","d"]`,
|
||||
},
|
||||
{
|
||||
"multi file with '-' modifier (nonempty base and matching value in a slice)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b", "c", "d"},
|
||||
"-",
|
||||
[]string{"b", "a", "123"},
|
||||
`["c","d"]`,
|
||||
},
|
||||
{
|
||||
"multi file with unknown modifier (nonempty)",
|
||||
schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
|
||||
[]string{"a", "b"},
|
||||
"?",
|
||||
"a",
|
||||
`["a","b"]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.field.PrepareValueWithModifier(s.baseValue, s.modifier, s.modifierValue)
|
||||
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("[%s] %v", s.name, err)
|
||||
}
|
||||
|
||||
if string(encoded) != s.expectJson {
|
||||
t.Fatalf("[%s], Expected %v, got %v", s.name, s.expectJson, string(encoded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type fieldOptionsScenario struct {
|
||||
@@ -1306,7 +1996,7 @@ func TestRelationOptionsValidate(t *testing.T) {
|
||||
[]string{"maxSelect"},
|
||||
},
|
||||
{
|
||||
"MaxSelect > 0 && non-empty CollectionId",
|
||||
"MaxSelect > 0 && nonempty CollectionId",
|
||||
schema.RelationOptions{
|
||||
CollectionId: "abc",
|
||||
MaxSelect: types.Pointer(1),
|
||||
|
||||
@@ -44,6 +44,8 @@ type Settings struct {
|
||||
SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"`
|
||||
KakaoAuth AuthProviderConfig `form:"kakaoAuth" json:"kakaoAuth"`
|
||||
TwitchAuth AuthProviderConfig `form:"twitchAuth" json:"twitchAuth"`
|
||||
StravaAuth AuthProviderConfig `form:"stravaAuth" json:"stravaAuth"`
|
||||
GiteeAuth AuthProviderConfig `form:"giteeAuth" json:"giteeAuth"`
|
||||
}
|
||||
|
||||
// New creates and returns a new default Settings instance.
|
||||
@@ -124,6 +126,12 @@ func New() *Settings {
|
||||
TwitchAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
StravaAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
GiteeAuth: AuthProviderConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +161,8 @@ func (s *Settings) Validate() error {
|
||||
validation.Field(&s.SpotifyAuth),
|
||||
validation.Field(&s.KakaoAuth),
|
||||
validation.Field(&s.TwitchAuth),
|
||||
validation.Field(&s.StravaAuth),
|
||||
validation.Field(&s.GiteeAuth),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -207,6 +217,8 @@ func (s *Settings) RedactClone() (*Settings, error) {
|
||||
&clone.SpotifyAuth.ClientSecret,
|
||||
&clone.KakaoAuth.ClientSecret,
|
||||
&clone.TwitchAuth.ClientSecret,
|
||||
&clone.StravaAuth.ClientSecret,
|
||||
&clone.GiteeAuth.ClientSecret,
|
||||
}
|
||||
|
||||
// mask all sensitive fields
|
||||
@@ -236,6 +248,8 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
|
||||
auth.NameSpotify: s.SpotifyAuth,
|
||||
auth.NameKakao: s.KakaoAuth,
|
||||
auth.NameTwitch: s.TwitchAuth,
|
||||
auth.NameStrava: s.StravaAuth,
|
||||
auth.NameGitee: s.GiteeAuth,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ func TestSettingsValidate(t *testing.T) {
|
||||
s.KakaoAuth.ClientId = ""
|
||||
s.TwitchAuth.Enabled = true
|
||||
s.TwitchAuth.ClientId = ""
|
||||
s.StravaAuth.Enabled = true
|
||||
s.StravaAuth.ClientId = ""
|
||||
s.GiteeAuth.Enabled = true
|
||||
s.GiteeAuth.ClientId = ""
|
||||
|
||||
// check if Validate() is triggering the members validate methods.
|
||||
err := s.Validate()
|
||||
@@ -76,6 +80,8 @@ func TestSettingsValidate(t *testing.T) {
|
||||
`"spotifyAuth":{`,
|
||||
`"kakaoAuth":{`,
|
||||
`"twitchAuth":{`,
|
||||
`"stravaAuth":{`,
|
||||
`"giteeAuth":{`,
|
||||
}
|
||||
|
||||
errBytes, _ := json.Marshal(err)
|
||||
@@ -124,6 +130,10 @@ func TestSettingsMerge(t *testing.T) {
|
||||
s2.KakaoAuth.ClientId = "kakao_test"
|
||||
s2.TwitchAuth.Enabled = true
|
||||
s2.TwitchAuth.ClientId = "twitch_test"
|
||||
s2.StravaAuth.Enabled = true
|
||||
s2.StravaAuth.ClientId = "strava_test"
|
||||
s2.GiteeAuth.Enabled = true
|
||||
s2.GiteeAuth.ClientId = "gitee_test"
|
||||
|
||||
if err := s1.Merge(s2); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -195,6 +205,8 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
s1.SpotifyAuth.ClientSecret = "test123"
|
||||
s1.KakaoAuth.ClientSecret = "test123"
|
||||
s1.TwitchAuth.ClientSecret = "test123"
|
||||
s1.StravaAuth.ClientSecret = "test123"
|
||||
s1.GiteeAuth.ClientSecret = "test123"
|
||||
|
||||
s2, err := s1.RedactClone()
|
||||
if err != nil {
|
||||
@@ -206,7 +218,7 @@ func TestSettingsRedactClone(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"support@example.com","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","authMethod":"","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"},"twitchAuth":{"enabled":false,"clientSecret":"******"}}`
|
||||
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"support@example.com","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","authMethod":"","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"},"twitchAuth":{"enabled":false,"clientSecret":"******"},"stravaAuth":{"enabled":false,"clientSecret":"******"},"giteeAuth":{"enabled":false,"clientSecret":"******"}}`
|
||||
|
||||
if encodedStr := string(encoded); encodedStr != expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr)
|
||||
@@ -227,6 +239,8 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
s.SpotifyAuth.ClientId = "spotify_test"
|
||||
s.KakaoAuth.ClientId = "kakao_test"
|
||||
s.TwitchAuth.ClientId = "twitch_test"
|
||||
s.StravaAuth.ClientId = "strava_test"
|
||||
s.GiteeAuth.ClientId = "gitee_test"
|
||||
|
||||
result := s.NamedAuthProviderConfigs()
|
||||
|
||||
@@ -247,6 +261,8 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
|
||||
`"twitter":{"enabled":false,"clientId":"twitter_test"}`,
|
||||
`"kakao":{"enabled":false,"clientId":"kakao_test"}`,
|
||||
`"twitch":{"enabled":false,"clientId":"twitch_test"}`,
|
||||
`"strava":{"enabled":false,"clientId":"strava_test"}`,
|
||||
`"gitee":{"enabled":false,"clientId":"gitee_test"}`,
|
||||
}
|
||||
for _, p := range expectedParts {
|
||||
if !strings.Contains(encodedStr, p) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package jsvm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja_nodejs/console"
|
||||
"github.com/dop251/goja_nodejs/require"
|
||||
@@ -75,7 +77,7 @@ func RegisterMigrations(app core.App, options *MigrationsOptions) error {
|
||||
|
||||
_, err := vm.RunString(string(content))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to run migration %s: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +99,8 @@ func readDirFiles(dirPath string) (map[string][]byte, error) {
|
||||
result := map[string][]byte{}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
if f.IsDir() || !strings.HasSuffix(f.Name(), ".js") {
|
||||
continue // not a .js file
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
var _ dbx.Expression = (*multiMatchSubquery)(nil)
|
||||
|
||||
// join defines the specification for a single SQL JOIN clause.
|
||||
type join struct {
|
||||
tableName string
|
||||
tableAlias string
|
||||
on dbx.Expression
|
||||
}
|
||||
|
||||
// multiMatchSubquery defines a record multi-match subquery expression.
|
||||
type multiMatchSubquery struct {
|
||||
baseTableAlias string
|
||||
fromTableName string
|
||||
fromTableAlias string
|
||||
valueIdentifier string
|
||||
joins []*join
|
||||
params dbx.Params
|
||||
}
|
||||
|
||||
// Build converts the expression into a SQL fragment.
|
||||
//
|
||||
// Implements [dbx.Expression] interface.
|
||||
func (m *multiMatchSubquery) Build(db *dbx.DB, params dbx.Params) string {
|
||||
if m.baseTableAlias == "" || m.fromTableName == "" || m.fromTableAlias == "" {
|
||||
return "0=1"
|
||||
}
|
||||
|
||||
if params == nil {
|
||||
params = m.params
|
||||
} else {
|
||||
// merge by updating the parent params
|
||||
for k, v := range m.params {
|
||||
params[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
var mergedJoins strings.Builder
|
||||
for i, j := range m.joins {
|
||||
if i > 0 {
|
||||
mergedJoins.WriteString(" ")
|
||||
}
|
||||
mergedJoins.WriteString("LEFT JOIN ")
|
||||
mergedJoins.WriteString(db.QuoteTableName(j.tableName))
|
||||
mergedJoins.WriteString(" ")
|
||||
mergedJoins.WriteString(db.QuoteTableName(j.tableAlias))
|
||||
if j.on != nil {
|
||||
mergedJoins.WriteString(" ON ")
|
||||
mergedJoins.WriteString(j.on.Build(db, params))
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`SELECT %s as [[multiMatchValue]] FROM %s %s %s WHERE %s = %s`,
|
||||
db.QuoteColumnName(m.valueIdentifier),
|
||||
db.QuoteTableName(m.fromTableName),
|
||||
db.QuoteTableName(m.fromTableAlias),
|
||||
mergedJoins.String(),
|
||||
db.QuoteColumnName(m.fromTableAlias+".id"),
|
||||
db.QuoteColumnName(m.baseTableAlias+".id"),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// parseAndRun starts a new one-off RecordFieldResolver.Resolve execution.
|
||||
func parseAndRun(fieldName string, resolver *RecordFieldResolver) (*search.ResolverResult, error) {
|
||||
r := &runner{
|
||||
fieldName: fieldName,
|
||||
resolver: resolver,
|
||||
}
|
||||
|
||||
return r.run()
|
||||
}
|
||||
|
||||
type runner struct {
|
||||
used bool // indicates whether the runner was already executed
|
||||
resolver *RecordFieldResolver // resolver is the shared expression fields resolver
|
||||
fieldName string // the name of the single field expression the runner is responsible for
|
||||
|
||||
// shared processing state
|
||||
// ---------------------------------------------------------------
|
||||
activeProps []string // holds the active props that remains to be processed
|
||||
activeCollectionName string // the last used collection name
|
||||
activeTableAlias string // the last used table alias
|
||||
allowHiddenFields bool // indicates whether hidden fields (eg. email) should be allowed without extra conditions
|
||||
nullifyMisingField bool // indicating whether to return null on missing field or return an error
|
||||
withMultiMatch bool // indicates whether to attach a multiMatchSubquery condition to the ResolverResult
|
||||
multiMatchActiveTableAlias string // the last used multi-match table alias
|
||||
multiMatch *multiMatchSubquery // the multi-match subquery expression generated from the fieldName
|
||||
}
|
||||
|
||||
func (r *runner) run() (*search.ResolverResult, error) {
|
||||
if r.used {
|
||||
return nil, fmt.Errorf("the runner was already used")
|
||||
}
|
||||
|
||||
if len(r.resolver.allowedFields) > 0 && !list.ExistInSliceWithRegex(r.fieldName, r.resolver.allowedFields) {
|
||||
return nil, fmt.Errorf("failed to resolve field %q", r.fieldName)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
r.used = true
|
||||
}()
|
||||
|
||||
r.prepare()
|
||||
|
||||
// check for @collection field (aka. non-relational join)
|
||||
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
|
||||
if r.activeProps[0] == "@collection" {
|
||||
return r.processCollectionField()
|
||||
}
|
||||
|
||||
if r.activeProps[0] == "@request" {
|
||||
if r.resolver.requestData == nil {
|
||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.fieldName, "@request.auth.") {
|
||||
return r.processRequestAuthField()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.fieldName, "@request.data.") && len(r.activeProps) > 2 {
|
||||
name, modifier, err := splitModifier(r.activeProps[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataField := r.resolver.baseCollection.Schema.GetFieldByName(name)
|
||||
if dataField == nil {
|
||||
return r.resolver.resolveStaticRequestField(r.activeProps[1:]...)
|
||||
}
|
||||
|
||||
dataField.InitOptions()
|
||||
|
||||
// check for data relation field
|
||||
if dataField.Type == schema.FieldTypeRelation && len(r.activeProps) > 3 {
|
||||
return r.processRequestDataRelationField(dataField)
|
||||
}
|
||||
|
||||
// check for select:each field
|
||||
if modifier == eachModifier && dataField.Type == schema.FieldTypeSelect && len(r.activeProps) == 3 {
|
||||
return r.processRequestDataSelectEachModifier(dataField)
|
||||
}
|
||||
|
||||
// check for data arrayble fields ":length" modifier
|
||||
if modifier == lengthModifier && list.ExistInSlice(dataField.Type, schema.ArraybleFieldTypes()) && len(r.activeProps) == 3 {
|
||||
return r.processRequestDataLengthModifier(dataField)
|
||||
}
|
||||
}
|
||||
|
||||
// some other @request.* static field
|
||||
return r.resolver.resolveStaticRequestField(r.activeProps[1:]...)
|
||||
}
|
||||
|
||||
// regular field
|
||||
return r.processActiveProps()
|
||||
}
|
||||
|
||||
func (r *runner) prepare() {
|
||||
r.activeProps = strings.Split(r.fieldName, ".")
|
||||
|
||||
r.activeCollectionName = r.resolver.baseCollection.Name
|
||||
r.activeTableAlias = inflector.Columnify(r.activeCollectionName)
|
||||
|
||||
r.allowHiddenFields = r.resolver.allowHiddenFields
|
||||
// always allow hidden fields since the @.* filter is a system one
|
||||
if r.activeProps[0] == "@collection" || r.activeProps[0] == "@request" {
|
||||
r.allowHiddenFields = true
|
||||
}
|
||||
|
||||
// enable the ignore flag for missing @request.* fields for backward
|
||||
// compatibility and consistency with all @request.* filter fields and types
|
||||
r.nullifyMisingField = r.activeProps[0] == "@request"
|
||||
|
||||
// prepare a multi-match subquery
|
||||
r.multiMatch = &multiMatchSubquery{
|
||||
baseTableAlias: r.activeTableAlias,
|
||||
params: dbx.Params{},
|
||||
}
|
||||
r.multiMatch.fromTableName = inflector.Columnify(r.activeCollectionName)
|
||||
r.multiMatch.fromTableAlias = "__mm_" + r.activeTableAlias
|
||||
r.multiMatchActiveTableAlias = r.multiMatch.fromTableAlias
|
||||
r.withMultiMatch = false
|
||||
}
|
||||
|
||||
func (r *runner) processCollectionField() (*search.ResolverResult, error) {
|
||||
if len(r.activeProps) < 3 {
|
||||
return nil, fmt.Errorf("invalid @collection field path in %q", r.fieldName)
|
||||
}
|
||||
|
||||
collection, err := r.resolver.loadCollection(r.activeProps[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load collection %q from field path %q", r.activeProps[1], r.fieldName)
|
||||
}
|
||||
|
||||
r.activeCollectionName = collection.Name
|
||||
r.activeTableAlias = inflector.Columnify("__collection_" + r.activeCollectionName)
|
||||
|
||||
r.withMultiMatch = true
|
||||
|
||||
// join the collection to the main query
|
||||
r.resolver.registerJoin(inflector.Columnify(collection.Name), r.activeTableAlias, nil)
|
||||
|
||||
// join the collection to the multi-match subquery
|
||||
r.multiMatchActiveTableAlias = "__mm" + r.activeTableAlias
|
||||
r.multiMatch.joins = append(r.multiMatch.joins, &join{
|
||||
tableName: inflector.Columnify(collection.Name),
|
||||
tableAlias: r.multiMatchActiveTableAlias,
|
||||
})
|
||||
|
||||
// leave only the collection fields
|
||||
// aka. @collection.someCollection.fieldA.fieldB -> fieldA.fieldB
|
||||
r.activeProps = r.activeProps[2:]
|
||||
|
||||
return r.processActiveProps()
|
||||
}
|
||||
|
||||
func (r *runner) processRequestAuthField() (*search.ResolverResult, error) {
|
||||
// plain auth field
|
||||
// ---
|
||||
if list.ExistInSlice(r.fieldName, plainRequestAuthFields) {
|
||||
return r.resolver.resolveStaticRequestField(r.activeProps[1:]...)
|
||||
}
|
||||
|
||||
// resolve the auth collection field
|
||||
// ---
|
||||
if r.resolver.requestData == nil || r.resolver.requestData.AuthRecord == nil || r.resolver.requestData.AuthRecord.Collection() == nil {
|
||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||
}
|
||||
|
||||
collection := r.resolver.requestData.AuthRecord.Collection()
|
||||
r.resolver.loadedCollections = append(r.resolver.loadedCollections, collection)
|
||||
|
||||
r.activeCollectionName = collection.Name
|
||||
r.activeTableAlias = "__auth_" + inflector.Columnify(r.activeCollectionName)
|
||||
|
||||
// join the auth collection to the main query
|
||||
r.resolver.registerJoin(
|
||||
inflector.Columnify(r.activeCollectionName),
|
||||
r.activeTableAlias,
|
||||
dbx.HashExp{
|
||||
// aka. __auth_users.id = :userId
|
||||
(r.activeTableAlias + ".id"): r.resolver.requestData.AuthRecord.Id,
|
||||
},
|
||||
)
|
||||
|
||||
// join the auth collection to the multi-match subquery
|
||||
r.multiMatchActiveTableAlias = "__mm_" + r.activeTableAlias
|
||||
r.multiMatch.joins = append(
|
||||
r.multiMatch.joins,
|
||||
&join{
|
||||
tableName: inflector.Columnify(r.activeCollectionName),
|
||||
tableAlias: r.multiMatchActiveTableAlias,
|
||||
on: dbx.HashExp{
|
||||
(r.multiMatchActiveTableAlias + ".id"): r.resolver.requestData.AuthRecord.Id,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// leave only the auth relation fields
|
||||
// aka. @request.auth.fieldA.fieldB -> fieldA.fieldB
|
||||
r.activeProps = r.activeProps[2:]
|
||||
|
||||
return r.processActiveProps()
|
||||
}
|
||||
|
||||
func (r *runner) processRequestDataLengthModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) {
|
||||
dataItems := list.ToUniqueStringSlice(r.resolver.requestData.Data[dataField.Name])
|
||||
|
||||
result := &search.ResolverResult{
|
||||
Identifier: fmt.Sprintf("%d", len(dataItems)),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *runner) processRequestDataSelectEachModifier(dataField *schema.SchemaField) (*search.ResolverResult, error) {
|
||||
options, ok := dataField.Options.(*schema.SelectOptions)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to initialize field %q options", dataField.Name)
|
||||
}
|
||||
|
||||
dataItems := list.ToUniqueStringSlice(r.resolver.requestData.Data[dataField.Name])
|
||||
rawJson, err := json.Marshal(dataItems)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshalize the data select item for field %q", r.activeProps[2])
|
||||
}
|
||||
|
||||
placeholder := "dataSelect" + security.PseudorandomString(4)
|
||||
cleanFieldName := inflector.Columnify(dataField.Name)
|
||||
jeTable := fmt.Sprintf("json_each({:%s})", placeholder)
|
||||
jeAlias := "__dataSelect_" + cleanFieldName + "_je"
|
||||
r.resolver.registerJoin(jeTable, jeAlias, nil)
|
||||
|
||||
result := &search.ResolverResult{
|
||||
Identifier: fmt.Sprintf("[[%s.value]]", jeAlias),
|
||||
Params: dbx.Params{placeholder: rawJson},
|
||||
}
|
||||
|
||||
if options.MaxSelect != 1 {
|
||||
r.withMultiMatch = true
|
||||
}
|
||||
|
||||
if r.withMultiMatch {
|
||||
placeholder2 := "mm" + placeholder
|
||||
jeTable2 := fmt.Sprintf("json_each({:%s})", placeholder2)
|
||||
jeAlias2 := "__mm" + jeAlias
|
||||
|
||||
r.multiMatch.joins = append(r.multiMatch.joins, &join{
|
||||
tableName: jeTable2,
|
||||
tableAlias: jeAlias2,
|
||||
})
|
||||
r.multiMatch.params[placeholder2] = rawJson
|
||||
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2)
|
||||
|
||||
result.MultiMatchSubQuery = r.multiMatch
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *runner) processRequestDataRelationField(dataField *schema.SchemaField) (*search.ResolverResult, error) {
|
||||
options, ok := dataField.Options.(*schema.RelationOptions)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to initialize data field %q options", dataField.Name)
|
||||
}
|
||||
|
||||
dataRelCollection, err := r.resolver.loadCollection(options.CollectionId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load collection %q from data field %q", options.CollectionId, dataField.Name)
|
||||
}
|
||||
|
||||
var dataRelIds []string
|
||||
if r.resolver.requestData != nil && len(r.resolver.requestData.Data) != 0 {
|
||||
dataRelIds = list.ToUniqueStringSlice(r.resolver.requestData.Data[dataField.Name])
|
||||
}
|
||||
if len(dataRelIds) == 0 {
|
||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||
}
|
||||
|
||||
r.activeCollectionName = dataRelCollection.Name
|
||||
r.activeTableAlias = inflector.Columnify("__data_" + dataRelCollection.Name)
|
||||
|
||||
// join the data rel collection to the main collection
|
||||
r.resolver.registerJoin(
|
||||
inflector.Columnify(r.activeCollectionName),
|
||||
r.activeTableAlias,
|
||||
dbx.In(
|
||||
fmt.Sprintf("[[%s.id]]", inflector.Columnify(r.activeTableAlias)),
|
||||
list.ToInterfaceSlice(dataRelIds)...,
|
||||
),
|
||||
)
|
||||
|
||||
if options.MaxSelect == nil || *options.MaxSelect != 1 {
|
||||
r.withMultiMatch = true
|
||||
}
|
||||
|
||||
// join the data rel collection to the multi-match subquery
|
||||
r.multiMatchActiveTableAlias = inflector.Columnify("__data_mm_" + dataRelCollection.Name)
|
||||
r.multiMatch.joins = append(
|
||||
r.multiMatch.joins,
|
||||
&join{
|
||||
tableName: inflector.Columnify(r.activeCollectionName),
|
||||
tableAlias: r.multiMatchActiveTableAlias,
|
||||
on: dbx.In(r.multiMatchActiveTableAlias+".id", list.ToInterfaceSlice(dataRelIds)...),
|
||||
},
|
||||
)
|
||||
|
||||
// leave only the data relation fields
|
||||
// aka. @request.data.someRel.fieldA.fieldB -> fieldA.fieldB
|
||||
r.activeProps = r.activeProps[3:]
|
||||
|
||||
return r.processActiveProps()
|
||||
}
|
||||
|
||||
func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
||||
totalProps := len(r.activeProps)
|
||||
|
||||
for i, prop := range r.activeProps {
|
||||
collection, err := r.resolver.loadCollection(r.activeCollectionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve field %q", prop)
|
||||
}
|
||||
|
||||
// last prop
|
||||
if i == totalProps-1 {
|
||||
// system field, aka. internal model prop
|
||||
// (always available but not part of the collection schema)
|
||||
// -------------------------------------------------------
|
||||
if list.ExistInSlice(prop, resolvableSystemFieldNames(collection)) {
|
||||
result := &search.ResolverResult{
|
||||
Identifier: fmt.Sprintf("[[%s.%s]]", r.activeTableAlias, inflector.Columnify(prop)),
|
||||
}
|
||||
|
||||
// allow querying only auth records with emails marked as public
|
||||
if prop == schema.FieldNameEmail && !r.allowHiddenFields {
|
||||
result.AfterBuild = func(expr dbx.Expression) dbx.Expression {
|
||||
return dbx.And(expr, dbx.NewExp(fmt.Sprintf(
|
||||
"[[%s.%s]] = TRUE",
|
||||
r.activeTableAlias,
|
||||
schema.FieldNameEmailVisibility,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
if r.withMultiMatch {
|
||||
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.%s]]", r.multiMatchActiveTableAlias, inflector.Columnify(prop))
|
||||
result.MultiMatchSubQuery = r.multiMatch
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
name, modifier, err := splitModifier(prop)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
field := collection.Schema.GetFieldByName(name)
|
||||
if field == nil {
|
||||
if r.nullifyMisingField {
|
||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown field %q", name)
|
||||
}
|
||||
|
||||
cleanFieldName := inflector.Columnify(field.Name)
|
||||
|
||||
// arrayble fields ":length" modifier
|
||||
// -------------------------------------------------------
|
||||
if modifier == lengthModifier && list.ExistInSlice(field.Type, schema.ArraybleFieldTypes()) {
|
||||
jePair := r.activeTableAlias + "." + cleanFieldName
|
||||
|
||||
result := &search.ResolverResult{
|
||||
Identifier: jsonArrayLength(jePair),
|
||||
}
|
||||
|
||||
if r.withMultiMatch {
|
||||
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
||||
r.multiMatch.valueIdentifier = jsonArrayLength(jePair2)
|
||||
result.MultiMatchSubQuery = r.multiMatch
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// select field with ":each" modifier
|
||||
// -------------------------------------------------------
|
||||
if field.Type == schema.FieldTypeSelect && modifier == eachModifier {
|
||||
jePair := r.activeTableAlias + "." + cleanFieldName
|
||||
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
||||
r.resolver.registerJoin(jsonEach(jePair), jeAlias, nil)
|
||||
|
||||
result := &search.ResolverResult{
|
||||
Identifier: fmt.Sprintf("[[%s.value]]", jeAlias),
|
||||
}
|
||||
|
||||
field.InitOptions()
|
||||
options, ok := field.Options.(*schema.SelectOptions)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to initialize field %q options", prop)
|
||||
}
|
||||
|
||||
if options.MaxSelect != 1 {
|
||||
r.withMultiMatch = true
|
||||
}
|
||||
|
||||
if r.withMultiMatch {
|
||||
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
||||
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
|
||||
|
||||
r.multiMatch.joins = append(r.multiMatch.joins, &join{
|
||||
tableName: jsonEach(jePair2),
|
||||
tableAlias: jeAlias2,
|
||||
})
|
||||
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2)
|
||||
|
||||
result.MultiMatchSubQuery = r.multiMatch
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// default
|
||||
// -------------------------------------------------------
|
||||
result := &search.ResolverResult{
|
||||
Identifier: fmt.Sprintf("[[%s.%s]]", r.activeTableAlias, cleanFieldName),
|
||||
}
|
||||
|
||||
if r.withMultiMatch {
|
||||
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.%s]]", r.multiMatchActiveTableAlias, cleanFieldName)
|
||||
result.MultiMatchSubQuery = r.multiMatch
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
field := collection.Schema.GetFieldByName(prop)
|
||||
if field == nil {
|
||||
if r.nullifyMisingField {
|
||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown field %q", prop)
|
||||
}
|
||||
|
||||
// check if it is a json field
|
||||
if field.Type == schema.FieldTypeJson {
|
||||
var jsonPath strings.Builder
|
||||
jsonPath.WriteString("$")
|
||||
for _, p := range r.activeProps[i+1:] {
|
||||
if _, err := strconv.Atoi(p); err == nil {
|
||||
jsonPath.WriteString("[")
|
||||
jsonPath.WriteString(inflector.Columnify(p))
|
||||
jsonPath.WriteString("]")
|
||||
} else {
|
||||
jsonPath.WriteString(".")
|
||||
jsonPath.WriteString(inflector.Columnify(p))
|
||||
}
|
||||
}
|
||||
|
||||
result := &search.ResolverResult{
|
||||
Identifier: fmt.Sprintf(
|
||||
"JSON_EXTRACT([[%s.%s]], '%s')",
|
||||
r.activeTableAlias,
|
||||
inflector.Columnify(prop),
|
||||
jsonPath.String(),
|
||||
),
|
||||
}
|
||||
|
||||
if r.withMultiMatch {
|
||||
r.multiMatch.valueIdentifier = fmt.Sprintf(
|
||||
"JSON_EXTRACT([[%s.%s]], '%s')",
|
||||
r.multiMatchActiveTableAlias,
|
||||
inflector.Columnify(prop),
|
||||
jsonPath.String(),
|
||||
)
|
||||
result.MultiMatchSubQuery = r.multiMatch
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// check if it is a relation field
|
||||
if field.Type != schema.FieldTypeRelation {
|
||||
return nil, fmt.Errorf("field %q is not a valid relation", prop)
|
||||
}
|
||||
|
||||
// join the relation to the main query
|
||||
// ---
|
||||
field.InitOptions()
|
||||
options, ok := field.Options.(*schema.RelationOptions)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to initialize field %q options", prop)
|
||||
}
|
||||
|
||||
relCollection, relErr := r.resolver.loadCollection(options.CollectionId)
|
||||
if relErr != nil {
|
||||
return nil, fmt.Errorf("failed to find field %q collection", prop)
|
||||
}
|
||||
|
||||
cleanFieldName := inflector.Columnify(field.Name)
|
||||
newCollectionName := relCollection.Name
|
||||
newTableAlias := r.activeTableAlias + "_" + cleanFieldName
|
||||
|
||||
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair := r.activeTableAlias + "." + cleanFieldName
|
||||
r.resolver.registerJoin(jsonEach(jePair), jeAlias, nil)
|
||||
r.resolver.registerJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeAlias)),
|
||||
)
|
||||
r.activeCollectionName = newCollectionName
|
||||
r.activeTableAlias = newTableAlias
|
||||
// ---
|
||||
|
||||
// join the relation to the multi-match subquery
|
||||
// ---
|
||||
if options.MaxSelect == nil || *options.MaxSelect != 1 {
|
||||
r.withMultiMatch = true
|
||||
}
|
||||
|
||||
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName
|
||||
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
|
||||
r.multiMatchActiveTableAlias = newTableAlias2
|
||||
|
||||
r.multiMatch.joins = append(
|
||||
r.multiMatch.joins,
|
||||
&join{
|
||||
tableName: jsonEach(jePair2),
|
||||
tableAlias: jeAlias2,
|
||||
},
|
||||
&join{
|
||||
tableName: inflector.Columnify(newCollectionName),
|
||||
tableAlias: newTableAlias2,
|
||||
on: dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias2, jeAlias2)),
|
||||
},
|
||||
)
|
||||
// ---
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to resolve field %q", r.fieldName)
|
||||
}
|
||||
|
||||
func jsonArrayLength(tableColumnPair string) string {
|
||||
return fmt.Sprintf(
|
||||
// note: the case is used to normalize value access for single and multiple relations.
|
||||
`json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||
tableColumnPair, tableColumnPair, tableColumnPair,
|
||||
)
|
||||
}
|
||||
|
||||
func jsonEach(tableColumnPair string) string {
|
||||
return fmt.Sprintf(
|
||||
// note: the case is used to normalize value access for single and multiple relations.
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||
tableColumnPair, tableColumnPair, tableColumnPair,
|
||||
)
|
||||
}
|
||||
|
||||
func resolvableSystemFieldNames(collection *models.Collection) []string {
|
||||
result := schema.BaseModelFieldNames()
|
||||
|
||||
if collection.IsAuth() {
|
||||
result = append(
|
||||
result,
|
||||
schema.FieldNameUsername,
|
||||
schema.FieldNameVerified,
|
||||
schema.FieldNameEmailVisibility,
|
||||
schema.FieldNameEmail,
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+129
-284
@@ -10,18 +10,20 @@ import (
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ensure that `search.FieldResolver` interface is implemented
|
||||
var _ search.FieldResolver = (*RecordFieldResolver)(nil)
|
||||
// filter modifiers
|
||||
const (
|
||||
eachModifier string = "each"
|
||||
issetModifier string = "isset"
|
||||
lengthModifier string = "length"
|
||||
)
|
||||
|
||||
// list of auth filter fields that don't require join with the auth
|
||||
// collection or any other extra checks to be resolved
|
||||
// collection or any other extra checks to be resolved.
|
||||
var plainRequestAuthFields = []string{
|
||||
"@request.auth." + schema.FieldNameId,
|
||||
"@request.auth." + schema.FieldNameCollectionId,
|
||||
@@ -34,32 +36,28 @@ var plainRequestAuthFields = []string{
|
||||
"@request.auth." + schema.FieldNameUpdated,
|
||||
}
|
||||
|
||||
type join struct {
|
||||
id string
|
||||
table string
|
||||
on dbx.Expression
|
||||
}
|
||||
// ensure that `search.FieldResolver` interface is implemented
|
||||
var _ search.FieldResolver = (*RecordFieldResolver)(nil)
|
||||
|
||||
// RecordFieldResolver defines a custom search resolver struct for
|
||||
// managing Record model search fields.
|
||||
//
|
||||
// Usually used together with `search.Provider`. Example:
|
||||
// resolver := resolvers.NewRecordFieldResolver(
|
||||
// app.Dao(),
|
||||
// myCollection,
|
||||
// &models.RequestData{...},
|
||||
// true,
|
||||
// )
|
||||
// provider := search.NewProvider(resolver)
|
||||
// ...
|
||||
// resolver := resolvers.NewRecordFieldResolver(
|
||||
// app.Dao(),
|
||||
// myCollection,
|
||||
// &models.RequestData{...},
|
||||
// true,
|
||||
// )
|
||||
// provider := search.NewProvider(resolver)
|
||||
// ...
|
||||
type RecordFieldResolver struct {
|
||||
dao *daos.Dao
|
||||
baseCollection *models.Collection
|
||||
allowHiddenFields bool
|
||||
allowedFields []string
|
||||
loadedCollections []*models.Collection
|
||||
joins []join // we cannot use a map because the insertion order is not preserved
|
||||
exprs []dbx.Expression
|
||||
joins []*join // we cannot use a map because the insertion order is not preserved
|
||||
requestData *models.RequestData
|
||||
staticRequestData map[string]any
|
||||
}
|
||||
@@ -76,20 +74,18 @@ func NewRecordFieldResolver(
|
||||
baseCollection: baseCollection,
|
||||
requestData: requestData,
|
||||
allowHiddenFields: allowHiddenFields,
|
||||
joins: []join{},
|
||||
exprs: []dbx.Expression{},
|
||||
joins: []*join{},
|
||||
loadedCollections: []*models.Collection{baseCollection},
|
||||
allowedFields: []string{
|
||||
`^\w+[\w\.]*$`,
|
||||
`^\w+[\w\.\:]*$`,
|
||||
`^\@request\.method$`,
|
||||
`^\@request\.auth\.\w+[\w\.]*$`,
|
||||
`^\@request\.data\.\w+[\w\.]*$`,
|
||||
`^\@request\.query\.\w+[\w\.]*$`,
|
||||
`^\@collection\.\w+\.\w+[\w\.]*$`,
|
||||
`^\@request\.auth\.[\w\.\:]*\w+$`,
|
||||
`^\@request\.data\.[\w\.\:]*\w+$`,
|
||||
`^\@request\.query\.[\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
|
||||
@@ -115,13 +111,10 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
query.Distinct(true)
|
||||
|
||||
for _, join := range r.joins {
|
||||
query.LeftJoin(join.table, join.on)
|
||||
}
|
||||
}
|
||||
|
||||
for _, expr := range r.exprs {
|
||||
if expr != nil {
|
||||
query.AndWhere(expr)
|
||||
query.LeftJoin(
|
||||
(join.tableName + " " + join.tableAlias),
|
||||
join.on,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,225 +123,63 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
|
||||
// Resolve implements `search.FieldResolver` interface.
|
||||
//
|
||||
// Example of resolvable field formats:
|
||||
// id
|
||||
// project.screen.status
|
||||
// @request.status
|
||||
// @request.auth.someRelation.name
|
||||
// @collection.product.name
|
||||
func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
if len(r.allowedFields) > 0 && !list.ExistInSliceWithRegex(fieldName, r.allowedFields) {
|
||||
return "", nil, fmt.Errorf("Failed to resolve field %q", fieldName)
|
||||
}
|
||||
|
||||
props := strings.Split(fieldName, ".")
|
||||
|
||||
currentCollectionName := r.baseCollection.Name
|
||||
currentTableAlias := inflector.Columnify(currentCollectionName)
|
||||
|
||||
// flag indicating whether to return null on missing field or return on an error
|
||||
nullifyMisingField := false
|
||||
|
||||
allowHiddenFields := r.allowHiddenFields
|
||||
|
||||
// check for @collection field (aka. non-relational join)
|
||||
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
|
||||
if props[0] == "@collection" {
|
||||
if len(props) < 3 {
|
||||
return "", nil, fmt.Errorf("Invalid @collection field path in %q.", fieldName)
|
||||
}
|
||||
|
||||
currentCollectionName = props[1]
|
||||
currentTableAlias = inflector.Columnify("__collection_" + currentCollectionName)
|
||||
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
|
||||
}
|
||||
|
||||
// always allow hidden fields since the @collection.* filter is a system one
|
||||
allowHiddenFields = true
|
||||
|
||||
r.registerJoin(inflector.Columnify(collection.Name), currentTableAlias, nil)
|
||||
|
||||
props = props[2:] // leave only the collection fields
|
||||
} else if props[0] == "@request" {
|
||||
if len(props) == 1 {
|
||||
return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName)
|
||||
}
|
||||
|
||||
if r.requestData == nil {
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
// plain @request.* field
|
||||
if !strings.HasPrefix(fieldName, "@request.auth.") || list.ExistInSlice(fieldName, plainRequestAuthFields) {
|
||||
return r.resolveStaticRequestField(props[1:]...)
|
||||
}
|
||||
|
||||
// always allow hidden fields since the @request.* filter is a system one
|
||||
allowHiddenFields = true
|
||||
|
||||
// enable the ignore flag for missing @request.auth.* fields
|
||||
// for consistency with @request.data.* and @request.query.*
|
||||
nullifyMisingField = true
|
||||
|
||||
// resolve the auth collection fields
|
||||
// ---
|
||||
if r.requestData == nil || r.requestData.AuthRecord == nil || r.requestData.AuthRecord.Collection() == nil {
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
collection := r.requestData.AuthRecord.Collection()
|
||||
r.loadedCollections = append(r.loadedCollections, collection)
|
||||
|
||||
currentCollectionName = collection.Name
|
||||
currentTableAlias = "__auth_" + inflector.Columnify(currentCollectionName)
|
||||
|
||||
authIdParamKey := "auth" + security.PseudorandomString(5)
|
||||
authIdParams := dbx.Params{authIdParamKey: r.requestData.AuthRecord.Id}
|
||||
// ---
|
||||
|
||||
// join the auth collection
|
||||
r.registerJoin(
|
||||
inflector.Columnify(collection.Name),
|
||||
currentTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf(
|
||||
// aka. __auth_users.id = :userId
|
||||
"[[%s.id]] = {:%s}",
|
||||
inflector.Columnify(currentTableAlias),
|
||||
authIdParamKey,
|
||||
), authIdParams),
|
||||
)
|
||||
|
||||
props = props[2:] // leave only the auth relation fields
|
||||
}
|
||||
|
||||
totalProps := len(props)
|
||||
|
||||
for i, prop := range props {
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to resolve field %q.", prop)
|
||||
}
|
||||
|
||||
systemFieldNames := schema.BaseModelFieldNames()
|
||||
if collection.IsAuth() {
|
||||
systemFieldNames = append(
|
||||
systemFieldNames,
|
||||
schema.FieldNameUsername,
|
||||
schema.FieldNameVerified,
|
||||
schema.FieldNameEmailVisibility,
|
||||
schema.FieldNameEmail,
|
||||
)
|
||||
}
|
||||
|
||||
// internal model prop (always available but not part of the collection schema)
|
||||
if list.ExistInSlice(prop, systemFieldNames) {
|
||||
// allow querying only auth records with emails marked as public
|
||||
if prop == schema.FieldNameEmail && !allowHiddenFields {
|
||||
r.registerExpr(dbx.NewExp(fmt.Sprintf(
|
||||
"[[%s.%s]] = TRUE",
|
||||
currentTableAlias,
|
||||
inflector.Columnify(schema.FieldNameEmailVisibility),
|
||||
)))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
|
||||
}
|
||||
|
||||
field := collection.Schema.GetFieldByName(prop)
|
||||
if field == nil {
|
||||
if nullifyMisingField {
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("Unrecognized field %q.", prop)
|
||||
}
|
||||
|
||||
// last prop
|
||||
if i == totalProps-1 {
|
||||
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
|
||||
}
|
||||
|
||||
// check if it is a json field
|
||||
if field.Type == schema.FieldTypeJson {
|
||||
var jsonPath strings.Builder
|
||||
jsonPath.WriteString("$")
|
||||
for _, p := range props[i+1:] {
|
||||
if _, err := strconv.Atoi(p); err == nil {
|
||||
jsonPath.WriteString("[")
|
||||
jsonPath.WriteString(inflector.Columnify(p))
|
||||
jsonPath.WriteString("]")
|
||||
} else {
|
||||
jsonPath.WriteString(".")
|
||||
jsonPath.WriteString(inflector.Columnify(p))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"JSON_EXTRACT([[%s.%s]], '%s')",
|
||||
currentTableAlias,
|
||||
inflector.Columnify(prop),
|
||||
jsonPath.String(),
|
||||
), nil, nil
|
||||
}
|
||||
|
||||
// check if it is a relation field
|
||||
if field.Type != schema.FieldTypeRelation {
|
||||
return "", nil, fmt.Errorf("Field %q is not a valid relation.", prop)
|
||||
}
|
||||
|
||||
// auto join the relation
|
||||
// ---
|
||||
field.InitOptions()
|
||||
options, ok := field.Options.(*schema.RelationOptions)
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("Failed to initialize field %q options.", prop)
|
||||
}
|
||||
|
||||
relCollection, relErr := r.loadCollection(options.CollectionId)
|
||||
if relErr != nil {
|
||||
return "", nil, fmt.Errorf("Failed to find field %q collection.", prop)
|
||||
}
|
||||
|
||||
cleanFieldName := inflector.Columnify(field.Name)
|
||||
newCollectionName := relCollection.Name
|
||||
newTableAlias := currentTableAlias + "_" + cleanFieldName
|
||||
|
||||
jeTable := currentTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair := currentTableAlias + "." + cleanFieldName
|
||||
|
||||
r.registerJoin(
|
||||
fmt.Sprintf(
|
||||
// note: the case is used to normalize value access for single and multiple relations.
|
||||
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
|
||||
jePair, jePair, jePair,
|
||||
),
|
||||
jeTable,
|
||||
nil,
|
||||
)
|
||||
r.registerJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeTable)),
|
||||
)
|
||||
|
||||
currentCollectionName = newCollectionName
|
||||
currentTableAlias = newTableAlias
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("Failed to resolve field %q.", fieldName)
|
||||
// Example of some resolvable fieldName formats:
|
||||
//
|
||||
// id
|
||||
// someSelect.each
|
||||
// project.screen.status
|
||||
// @request.status
|
||||
// @request.query.filter
|
||||
// @request.auth.someRelation.name
|
||||
// @request.data.someRelation.name
|
||||
// @request.data.someField
|
||||
// @request.data.someSelect:each
|
||||
// @request.data.someField:isset
|
||||
// @collection.product.name
|
||||
func (r *RecordFieldResolver) Resolve(fieldName string) (*search.ResolverResult, error) {
|
||||
return parseAndRun(fieldName, r)
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
// ignore error because requestData is dynamic and some of the
|
||||
// lookup keys may not be defined for the request
|
||||
resultVal, _ := extractNestedMapVal(r.staticRequestData, path...)
|
||||
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search.ResolverResult, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, fmt.Errorf("at least one path key should be provided")
|
||||
}
|
||||
|
||||
lastProp, modifier, err := splitModifier(path[len(path)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path[len(path)-1] = lastProp
|
||||
|
||||
// extract value
|
||||
resultVal, err := extractNestedMapVal(r.staticRequestData, path...)
|
||||
|
||||
if modifier == issetModifier {
|
||||
if err != nil {
|
||||
return &search.ResolverResult{Identifier: "FALSE"}, nil
|
||||
}
|
||||
return &search.ResolverResult{Identifier: "TRUE"}, nil
|
||||
}
|
||||
|
||||
// note: we are ignoring the error because requestData is dynamic
|
||||
// and some of the lookup keys may not be defined for the request
|
||||
|
||||
switch v := resultVal.(type) {
|
||||
case nil:
|
||||
return "NULL", nil, nil
|
||||
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return &search.ResolverResult{Identifier: "NULL"}, nil
|
||||
case string:
|
||||
// check if it is a number field and explicitly try to cast to
|
||||
// float in case of a numeric string value was used
|
||||
// (this usually the case when the data is from a multipart/form-data request)
|
||||
field := r.baseCollection.Schema.GetFieldByName(path[len(path)-1])
|
||||
if field != nil && field.Type == schema.FieldTypeNumber {
|
||||
if nv, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
resultVal = nv
|
||||
}
|
||||
}
|
||||
// otherwise - no further processing is needed...
|
||||
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
// no further processing is needed...
|
||||
default:
|
||||
// non-plain value
|
||||
@@ -367,33 +198,11 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultN
|
||||
}
|
||||
|
||||
placeholder := "f" + security.PseudorandomString(5)
|
||||
name := fmt.Sprintf("{:%s}", placeholder)
|
||||
params := dbx.Params{placeholder: resultVal}
|
||||
|
||||
return name, params, nil
|
||||
}
|
||||
|
||||
func extractNestedMapVal(m map[string]any, keys ...string) (result any, err error) {
|
||||
var ok bool
|
||||
|
||||
if len(keys) == 0 {
|
||||
return nil, fmt.Errorf("At least one key should be provided.")
|
||||
}
|
||||
|
||||
if result, ok = m[keys[0]]; !ok {
|
||||
return nil, fmt.Errorf("Invalid key path - missing key %q.", keys[0])
|
||||
}
|
||||
|
||||
// end key reached
|
||||
if len(keys) == 1 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if m, ok = result.(map[string]any); !ok {
|
||||
return nil, fmt.Errorf("Expected map structure, got %#v.", result)
|
||||
}
|
||||
|
||||
return extractNestedMapVal(m, keys[1:]...)
|
||||
return &search.ResolverResult{
|
||||
Identifier: "{:" + placeholder + "}",
|
||||
Params: dbx.Params{placeholder: resultVal},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models.Collection, error) {
|
||||
@@ -415,17 +224,15 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) {
|
||||
tableExpr := (tableName + " " + tableAlias)
|
||||
|
||||
join := join{
|
||||
id: tableAlias,
|
||||
table: tableExpr,
|
||||
on: on,
|
||||
join := &join{
|
||||
tableName: tableName,
|
||||
tableAlias: tableAlias,
|
||||
on: on,
|
||||
}
|
||||
|
||||
// replace existing join
|
||||
for i, j := range r.joins {
|
||||
if j.id == join.id {
|
||||
if j.tableAlias == join.tableAlias {
|
||||
r.joins[i] = join
|
||||
return
|
||||
}
|
||||
@@ -435,6 +242,44 @@ func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string,
|
||||
r.joins = append(r.joins, join)
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) registerExpr(expr dbx.Expression) {
|
||||
r.exprs = append(r.exprs, expr)
|
||||
func extractNestedMapVal(m map[string]any, keys ...string) (any, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, fmt.Errorf("at least one key should be provided")
|
||||
}
|
||||
|
||||
var result any
|
||||
var ok bool
|
||||
|
||||
if result, ok = m[keys[0]]; !ok {
|
||||
return nil, fmt.Errorf("invalid key path - missing key %q", keys[0])
|
||||
}
|
||||
|
||||
// end key reached
|
||||
if len(keys) == 1 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if m, ok = result.(map[string]any); !ok {
|
||||
return nil, fmt.Errorf("expected map, got %#v", result)
|
||||
}
|
||||
|
||||
return extractNestedMapVal(m, keys[1:]...)
|
||||
}
|
||||
|
||||
func splitModifier(combined string) (string, string, error) {
|
||||
parts := strings.Split(combined, ":")
|
||||
|
||||
if len(parts) != 2 {
|
||||
return combined, "", nil
|
||||
}
|
||||
|
||||
// validate modifier
|
||||
switch parts[1] {
|
||||
case issetModifier,
|
||||
eachModifier,
|
||||
lengthModifier:
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("unknown modifier in %q", combined)
|
||||
}
|
||||
@@ -3,12 +3,14 @@ package resolvers_test
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/resolvers"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
)
|
||||
|
||||
func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
@@ -21,149 +23,307 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
requestData := &models.RequestData{
|
||||
Query: map[string]any{
|
||||
"a": nil,
|
||||
"b": 123,
|
||||
},
|
||||
Data: map[string]any{
|
||||
"a": nil,
|
||||
"b": 123,
|
||||
"number": 10,
|
||||
"select_many": []string{"optionA", "optionC"},
|
||||
"rel_one": "test",
|
||||
"rel_many": []string{"test1", "test2"},
|
||||
"file_one": "test",
|
||||
"file_many": []string{"test1", "test2", "test3"},
|
||||
"self_rel_one": "test",
|
||||
"self_rel_many": []string{"test1"},
|
||||
"rel_many_cascade": []string{"test1", "test2"},
|
||||
},
|
||||
AuthRecord: authRecord,
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
collectionIdOrName string
|
||||
fields []string
|
||||
rule string
|
||||
allowHiddenFields bool
|
||||
expectQuery string
|
||||
}{
|
||||
{
|
||||
"missing field",
|
||||
"non relation field (with all default operators)",
|
||||
"demo4",
|
||||
[]string{""},
|
||||
"title = true || title != 'test' || title ~ 'test1' || title !~ '%test2' || title > 1 || title >= 2 || title < 3 || title <= 4",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE (COALESCE([[demo4.title]], '') = COALESCE(1, '') OR COALESCE([[demo4.title]], '') != COALESCE({:TEST}, '') OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})",
|
||||
},
|
||||
{
|
||||
"non relation field",
|
||||
"non relation field (with all opt/any operators)",
|
||||
"demo4",
|
||||
[]string{"title"},
|
||||
"title ?= true || title ?!= 'test' || title ?~ 'test1' || title ?!~ '%test2' || title ?> 1 || title ?>= 2 || title ?< 3 || title ?<= 4",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE (COALESCE([[demo4.title]], '') = COALESCE(1, '') OR COALESCE([[demo4.title]], '') != COALESCE({:TEST}, '') OR [[demo4.title]] LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] NOT LIKE {:TEST} ESCAPE '\\' OR [[demo4.title]] > {:TEST} OR [[demo4.title]] >= {:TEST} OR [[demo4.title]] < {:TEST} OR [[demo4.title]] <= {:TEST})",
|
||||
},
|
||||
{
|
||||
"incomplete rel",
|
||||
"demo4",
|
||||
[]string{"self_rel_one"},
|
||||
"self_rel_one > true",
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
"SELECT `demo4`.* FROM `demo4` WHERE [[demo4.self_rel_one]] > 1",
|
||||
},
|
||||
{
|
||||
"single rel (self rel)",
|
||||
"demo4",
|
||||
[]string{"self_rel_one.title"},
|
||||
"self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE [[demo4_self_rel_one.title]] > 1",
|
||||
},
|
||||
{
|
||||
"single rel (other collection)",
|
||||
"demo4",
|
||||
[]string{"rel_one_cascade.title"},
|
||||
"rel_one_cascade.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.rel_one_cascade]]) THEN [[demo4.rel_one_cascade]] ELSE json_array([[demo4.rel_one_cascade]]) END) `demo4_rel_one_cascade_je` LEFT JOIN `demo3` `demo4_rel_one_cascade` ON [[demo4_rel_one_cascade.id]] = [[demo4_rel_one_cascade_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.rel_one_cascade]]) THEN [[demo4.rel_one_cascade]] ELSE json_array([[demo4.rel_one_cascade]]) END) `demo4_rel_one_cascade_je` LEFT JOIN `demo3` `demo4_rel_one_cascade` ON [[demo4_rel_one_cascade.id]] = [[demo4_rel_one_cascade_je.value]] WHERE [[demo4_rel_one_cascade.title]] > 1",
|
||||
},
|
||||
{
|
||||
"non-relation field + single rel",
|
||||
"demo4",
|
||||
[]string{"title", "self_rel_one.title"},
|
||||
"title > true || self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE ([[demo4.title]] > 1 OR [[demo4_self_rel_one.title]] > 1)",
|
||||
},
|
||||
{
|
||||
"nested incomplete rels",
|
||||
"nested incomplete rels (opt/any operator)",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.self_rel_one"},
|
||||
"self_rel_many.self_rel_one ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE [[demo4_self_rel_many.self_rel_one]] > 1",
|
||||
},
|
||||
{
|
||||
"nested complete rels",
|
||||
"nested incomplete rels (multi-match operator)",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.self_rel_one.title"},
|
||||
"self_rel_many.self_rel_one > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] WHERE (([[demo4_self_rel_many.self_rel_one]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.self_rel_one]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE (NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))",
|
||||
},
|
||||
{
|
||||
"repeated nested rels",
|
||||
"nested complete rels (opt/any operator)",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title"},
|
||||
"self_rel_many.self_rel_one.title ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]] WHERE [[demo4_self_rel_many_self_rel_one.title]] > 1",
|
||||
},
|
||||
{
|
||||
"multiple rels",
|
||||
"nested complete rels (multi-match operator)",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.title", "self_rel_one.onefile"},
|
||||
"self_rel_many.self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]]",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]] WHERE (([[demo4_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many.self_rel_one]]) THEN [[__mm_demo4_self_rel_many.self_rel_one]] ELSE json_array([[__mm_demo4_self_rel_many.self_rel_one]]) END) `__mm_demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE (NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))",
|
||||
},
|
||||
{
|
||||
"@collection join",
|
||||
"repeated nested rels (opt/any operator)",
|
||||
"demo4",
|
||||
[]string{"@collection.demo1.text", "@collection.demo2.active", "@collection.demo1.file_one"},
|
||||
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo1` `__collection_demo1` LEFT JOIN `demo2` `__collection_demo2`",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je.value]] WHERE [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1",
|
||||
},
|
||||
{
|
||||
"repeated nested rels (multi-match operator)",
|
||||
"demo4",
|
||||
"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je.value]] WHERE (([[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many.self_rel_one]]) THEN [[__mm_demo4_self_rel_many.self_rel_one]] ELSE json_array([[__mm_demo4_self_rel_many.self_rel_one]]) END) `__mm_demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[__mm_demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[__mm_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE (NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))",
|
||||
},
|
||||
{
|
||||
"multiple rels (opt/any operators)",
|
||||
"demo4",
|
||||
"self_rel_many.title ?= 'test' || self_rel_one.json_object.a ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE (COALESCE([[demo4_self_rel_many.title]], '') = COALESCE({:TEST}, '') OR JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') > 1)",
|
||||
},
|
||||
{
|
||||
"multiple rels (multi-match operators)",
|
||||
"demo4",
|
||||
"self_rel_many.title = 'test' || self_rel_one.json_object.a > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] WHERE ((COALESCE([[demo4_self_rel_many.title]], '') = COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo4_self_rel_many.title]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_many]]) THEN [[__mm_demo4.self_rel_many]] ELSE json_array([[__mm_demo4.self_rel_many]]) END) `__mm_demo4_self_rel_many_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_many` ON [[__mm_demo4_self_rel_many.id]] = [[__mm_demo4_self_rel_many_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__TEST}} WHERE NOT (COALESCE([[__TEST.multiMatchValue]], '') = COALESCE({:TEST}, '')))) OR JSON_EXTRACT([[demo4_self_rel_one.json_object]], '$.a') > 1)",
|
||||
},
|
||||
{
|
||||
"@collection join (opt/any operators)",
|
||||
"demo4",
|
||||
"@collection.demo1.text ?> true || @collection.demo2.active ?> true || @collection.demo1.file_one ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo1` `__collection_demo1` LEFT JOIN `demo2` `__collection_demo2` WHERE ([[__collection_demo1.text]] > 1 OR [[__collection_demo2.active]] > 1 OR [[__collection_demo1.file_one]] > 1)",
|
||||
},
|
||||
{
|
||||
"@collection join (multi-match operators)",
|
||||
"demo4",
|
||||
"@collection.demo1.text > true || @collection.demo2.active > true || @collection.demo1.file_one > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo1` `__collection_demo1` LEFT JOIN `demo2` `__collection_demo2` WHERE (([[__collection_demo1.text]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo1.text]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo1` `__mm__collection_demo1` WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE (NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))) OR ([[__collection_demo2.active]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo2.active]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo2` `__mm__collection_demo2` WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__TEST}} WHERE (NOT ([[__TEST.multiMatchValue]] > 1)) OR ([[__TEST.multiMatchValue]] IS NULL))) OR ([[__collection_demo1.file_one]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo1.file_one]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo1` `__mm__collection_demo1` WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{TEST}} WHERE (NOT ([[TEST.multiMatchValue]] > 1)) OR ([[TEST.multiMatchValue]] IS NULL))))",
|
||||
},
|
||||
{
|
||||
"@request.auth fields",
|
||||
"demo4",
|
||||
[]string{"@request.auth.id", "@request.auth.username", "@request.auth.rel.title", "@request.data.demo"},
|
||||
"@request.auth.id > true || @request.auth.username > true || @request.auth.rel.title > true || @request.data.demo > true",
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON [[__auth_users.id]] =") +
|
||||
" {:.*} " +
|
||||
regexp.QuoteMeta("LEFT JOIN json_each(CASE WHEN json_valid([[__auth_users.rel]]) THEN [[__auth_users.rel]] ELSE json_array([[__auth_users.rel]]) END) `__auth_users_rel_je` LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users_rel_je.value]]") +
|
||||
"$",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON `__auth_users`.`id`={:TEST} LEFT JOIN json_each(CASE WHEN json_valid([[__auth_users.rel]]) THEN [[__auth_users.rel]] ELSE json_array([[__auth_users.rel]]) END) `__auth_users_rel_je` LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users_rel_je.value]] WHERE ({:TEST} > 1 OR {:TEST} > 1 OR [[__auth_users_rel.title]] > 1 OR NULL > 1)",
|
||||
},
|
||||
{
|
||||
"hidden field with system filters (ignore emailVisibility)",
|
||||
"hidden field with system filters (multi-match and ignore emailVisibility)",
|
||||
"demo4",
|
||||
[]string{"@collection.users.email", "@request.auth.email"},
|
||||
"@collection.users.email > true || @request.auth.email > true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__collection_users`",
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__collection_users` WHERE (([[__collection_users.email]] > 1) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_users.email]] as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `users` `__mm__collection_users` WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE (NOT ([[__smTEST.multiMatchValue]] > 1)) OR ([[__smTEST.multiMatchValue]] IS NULL))) OR {:TEST} > 1)",
|
||||
},
|
||||
{
|
||||
"hidden field (add emailVisibility)",
|
||||
"users",
|
||||
[]string{"email"},
|
||||
"email > true",
|
||||
false,
|
||||
"SELECT `users`.* FROM `users` WHERE [[users.emailVisibility]] = TRUE",
|
||||
"SELECT `users`.* FROM `users` WHERE (([[users.email]] > 1) AND ([[users.emailVisibility]] = TRUE))",
|
||||
},
|
||||
{
|
||||
"hidden field (force ignore emailVisibility)",
|
||||
"users",
|
||||
[]string{"email"},
|
||||
"email > true",
|
||||
true,
|
||||
"SELECT `users`.* FROM `users`",
|
||||
"SELECT `users`.* FROM `users` WHERE [[users.email]] > 1",
|
||||
},
|
||||
{
|
||||
"isset key",
|
||||
"demo1",
|
||||
"@request.data.a:isset > true ||" +
|
||||
"@request.data.b:isset > true ||" +
|
||||
"@request.data.c:isset > true ||" +
|
||||
"@request.query.a:isset > true ||" +
|
||||
"@request.query.b:isset > true ||" +
|
||||
"@request.query.c:isset > true",
|
||||
false,
|
||||
"SELECT `demo1`.* FROM `demo1` WHERE (TRUE > 1 OR TRUE > 1 OR FALSE > 1 OR TRUE > 1 OR TRUE > 1 OR FALSE > 1)",
|
||||
},
|
||||
{
|
||||
"@request.data.rel.* fields",
|
||||
"demo1",
|
||||
"@request.data.rel_one > true &&" +
|
||||
"@request.data.rel_one.text > true &&" +
|
||||
"@request.data.rel_many > true &&" +
|
||||
"@request.data.rel_many.email != 'test' &&" +
|
||||
"@request.data.rel_many.url ?= 'test' &&" +
|
||||
"@request.data.rel_many.avatar ~ 'test'",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN `demo1` `__data_demo1` ON [[__data_demo1.id]]={:TEST} LEFT JOIN `users` `__data_users` ON [[__data_users.id]] IN ({:TEST}, {:TEST}) WHERE ({:TEST} > 1 AND [[__data_demo1.text]] > 1 AND {:TEST} > 1 AND (COALESCE([[__data_users.email]], '') != COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_users.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE (NOT (COALESCE([[__smTEST.multiMatchValue]], '') != COALESCE({:TEST}, ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))) AND COALESCE(NULL, '') = COALESCE({:TEST}, '') AND ([[__data_users.avatar]] LIKE {:TEST} ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__data_mm_users.avatar]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `users` `__data_mm_users` ON `__data_mm_users`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE (NOT ([[__smTEST.multiMatchValue]] LIKE {:TEST} ESCAPE '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))))",
|
||||
},
|
||||
{
|
||||
"@request.data.select:each fields",
|
||||
"demo1",
|
||||
"@request.data.select_one = 'test' &&" +
|
||||
"@request.data.select_one:each != 'test' &&" +
|
||||
"@request.data.select_one:each ?= 'test' &&" +
|
||||
"@request.data.select_many ~ 'test' &&" +
|
||||
"@request.data.select_many:each = 'test' &&" +
|
||||
"@request.data.select_many:each ?< true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each({:TEST}) `__dataSelect_select_one_je` LEFT JOIN json_each({:TEST}) `__dataSelect_select_many_je` WHERE (COALESCE(NULL, '') = COALESCE({:TEST}, '') AND COALESCE([[__dataSelect_select_one_je.value]], '') != COALESCE({:TEST}, '') AND COALESCE([[__dataSelect_select_one_je.value]], '') = COALESCE({:TEST}, '') AND {:TEST} LIKE {:TEST} ESCAPE '\\' AND (COALESCE([[__dataSelect_select_many_je.value]], '') = COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__dataSelect_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:TEST}) `__mm__dataSelect_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT (COALESCE([[__smTEST.multiMatchValue]], '') = COALESCE({:TEST}, '')))) AND [[__dataSelect_select_many_je.value]] < 1)",
|
||||
},
|
||||
{
|
||||
"regular select:each fields",
|
||||
"demo1",
|
||||
"select_one = 'test' &&" +
|
||||
"select_one:each != 'test' &&" +
|
||||
"select_one:each ?> true &&" +
|
||||
"select_many ~ 'test' &&" +
|
||||
"select_many:each = 'test' &&" +
|
||||
"select_many:each ?> true",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_one]]) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_many]]) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` WHERE (COALESCE([[demo1.select_one]], '') = COALESCE({:TEST}, '') AND COALESCE([[demo1_select_one_je.value]], '') != COALESCE({:TEST}, '') AND [[demo1_select_one_je.value]] > 1 AND [[demo1.select_many]] LIKE {:TEST} ESCAPE '\\' AND (COALESCE([[demo1_select_many_je.value]], '') = COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE NOT (COALESCE([[__smTEST.multiMatchValue]], '') = COALESCE({:TEST}, '')))) AND [[demo1_select_many_je.value]] > 1)",
|
||||
},
|
||||
{
|
||||
"select:each vs select:each",
|
||||
"demo1",
|
||||
"select_one:each != select_many:each &&" +
|
||||
"select_many:each > select_one:each &&" +
|
||||
"select_many:each ?< select_one:each &&" +
|
||||
"select_many:each = @request.data.select_many:each",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_one]]) THEN [[demo1.select_one]] ELSE json_array([[demo1.select_one]]) END) `demo1_select_one_je` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.select_many]]) THEN [[demo1.select_many]] ELSE json_array([[demo1.select_many]]) END) `demo1_select_many_je` LEFT JOIN json_each({:TEST}) `__dataSelect_select_many_je` WHERE ((COALESCE([[demo1_select_one_je.value]], '') != COALESCE([[demo1_select_many_je.value]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE (NOT (COALESCE([[demo1_select_one_je.value]], '') != COALESCE([[__smTEST.multiMatchValue]], ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))) AND ([[demo1_select_many_je.value]] > [[demo1_select_one_je.value]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE (NOT ([[__smTEST.multiMatchValue]] > [[demo1_select_one_je.value]])) OR ([[__smTEST.multiMatchValue]] IS NULL))) AND [[demo1_select_many_je.value]] < [[demo1_select_one_je.value]] AND (COALESCE([[demo1_select_many_je.value]], '') = COALESCE([[__dataSelect_select_many_je.value]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.select_many]]) THEN [[__mm_demo1.select_many]] ELSE json_array([[__mm_demo1.select_many]]) END) `__mm_demo1_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm__dataSelect_select_many_je.value]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each({:TEST}) `__mm__dataSelect_select_many_je` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], '')))))",
|
||||
},
|
||||
{
|
||||
"mixed multi-match vs multi-match",
|
||||
"demo1",
|
||||
"rel_many.rel.active != rel_many.name &&" +
|
||||
"rel_many.rel.active ?= rel_many.name &&" +
|
||||
"rel_many.rel.title ~ rel_one.email &&" +
|
||||
"@collection.demo2.active = rel_many.rel.active &&" +
|
||||
"@collection.demo2.active ?= rel_many.rel.active &&" +
|
||||
"rel_many.email > @request.data.rel_many.email",
|
||||
false,
|
||||
"SELECT DISTINCT `demo1`.* FROM `demo1` LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) `demo1_rel_many_je` LEFT JOIN `users` `demo1_rel_many` ON [[demo1_rel_many.id]] = [[demo1_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo1_rel_many.rel]]) THEN [[demo1_rel_many.rel]] ELSE json_array([[demo1_rel_many.rel]]) END) `demo1_rel_many_rel_je` LEFT JOIN `demo2` `demo1_rel_many_rel` ON [[demo1_rel_many_rel.id]] = [[demo1_rel_many_rel_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo1.rel_one]]) THEN [[demo1.rel_one]] ELSE json_array([[demo1.rel_one]]) END) `demo1_rel_one_je` LEFT JOIN `demo1` `demo1_rel_one` ON [[demo1_rel_one.id]] = [[demo1_rel_one_je.value]] LEFT JOIN `demo2` `__collection_demo2` LEFT JOIN `users` `__data_users` ON [[__data_users.id]] IN ({:TEST}, {:TEST}) WHERE ((COALESCE([[demo1_rel_many_rel.active]], '') != COALESCE([[demo1_rel_many.name]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1_rel_many.rel]]) THEN [[__mm_demo1_rel_many.rel]] ELSE json_array([[__mm_demo1_rel_many.rel]]) END) `__mm_demo1_rel_many_rel_je` LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many_rel_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many.name]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE (NOT (COALESCE([[__mlTEST.multiMatchValue]], '') != COALESCE([[__mrTEST.multiMatchValue]], ''))) OR ([[__mlTEST.multiMatchValue]] IS NULL) OR ([[__mrTEST.multiMatchValue]] IS NULL))) AND COALESCE([[demo1_rel_many_rel.active]], '') = COALESCE([[demo1_rel_many.name]], '') AND ([[demo1_rel_many_rel.title]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\') AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many_rel.title]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1_rel_many.rel]]) THEN [[__mm_demo1_rel_many.rel]] ELSE json_array([[__mm_demo1_rel_many.rel]]) END) `__mm_demo1_rel_many_rel_je` LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many_rel_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__smTEST}} WHERE (NOT ([[__smTEST.multiMatchValue]] LIKE ('%' || [[demo1_rel_one.email]] || '%') ESCAPE '\\')) OR ([[__smTEST.multiMatchValue]] IS NULL))) AND (COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm__collection_demo2.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN `demo2` `__mm__collection_demo2` WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mlTEST}} LEFT JOIN (SELECT [[__mm_demo1_rel_many_rel.active]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `users` `__mm_demo1_rel_many` ON [[__mm_demo1_rel_many.id]] = [[__mm_demo1_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1_rel_many.rel]]) THEN [[__mm_demo1_rel_many.rel]] ELSE json_array([[__mm_demo1_rel_many.rel]]) END) `__mm_demo1_rel_many_rel_je` LEFT JOIN `demo2` `__mm_demo1_rel_many_rel` ON [[__mm_demo1_rel_many_rel.id]] = [[__mm_demo1_rel_many_rel_je.value]] WHERE `__mm_demo1`.`id` = `demo1`.`id`) {{__mrTEST}} WHERE NOT (COALESCE([[__mlTEST.multiMatchValue]], '') = COALESCE([[__mrTEST.multiMatchValue]], '')))) AND COALESCE([[__collection_demo2.active]], '') = COALESCE([[demo1_rel_many_rel.active]], '') AND (([[demo1_rel_many.email]] > [[__data_users.email]]) AND (NOT EXISTS (SELECT 1 FROM (SELECT [[__mm_demo1_rel_many.email]] as [[multiMatchValue]] FROM `demo1` `__mm_demo1` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo1.rel_many]]) THEN [[__mm_demo1.rel_many]] ELSE json_array([[__mm_demo1.rel_many]]) END) `__mm_demo1_rel_many_je` LEFT JOIN `usersLine truncated
|
||||
},
|
||||
{
|
||||
"@request.data.arrayable:length fields",
|
||||
"demo1",
|
||||
"@request.data.select_one:length > 1 &&" +
|
||||
"@request.data.select_one:length ?> 2 &&" +
|
||||
"@request.data.select_many:length < 3 &&" +
|
||||
"@request.data.select_many:length ?> 4 &&" +
|
||||
"@request.data.rel_one:length = 5 &&" +
|
||||
"@request.data.rel_one:length ?= 6 &&" +
|
||||
"@request.data.rel_many:length != 7 &&" +
|
||||
"@request.data.rel_many:length ?!= 8 &&" +
|
||||
"@request.data.file_one:length = 9 &&" +
|
||||
"@request.data.file_one:length ?= 0 &&" +
|
||||
"@request.data.file_many:length != 1 &&" +
|
||||
"@request.data.file_many:length ?!= 2",
|
||||
false,
|
||||
"SELECT `demo1`.* FROM `demo1` WHERE (0 > {:TEST} AND 0 > {:TEST} AND 2 < {:TEST} AND 2 > {:TEST} AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(2, '') != COALESCE({:TEST}, '') AND COALESCE(2, '') != COALESCE({:TEST}, '') AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(1, '') = COALESCE({:TEST}, '') AND COALESCE(3, '') != COALESCE({:TEST}, '') AND COALESCE(3, '') != COALESCE({:TEST}, ''))",
|
||||
},
|
||||
{
|
||||
"regular arrayable:length fields",
|
||||
"demo4",
|
||||
"@request.data.self_rel_one.self_rel_many:length > 1 &&" +
|
||||
"@request.data.self_rel_one.self_rel_many:length ?> 2 &&" +
|
||||
"@request.data.rel_many_cascade.files:length ?< 3 &&" +
|
||||
"@request.data.rel_many_cascade.files:length < 4 &&" +
|
||||
"self_rel_one.self_rel_many:length = 5 &&" +
|
||||
"self_rel_one.self_rel_many:length ?= 6 &&" +
|
||||
"self_rel_one.rel_many_cascade.files:length != 7 &&" +
|
||||
"self_rel_one.rel_many_cascade.files:length ?!= 8",
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4` ON [[__data_demo4.id]]={:TEST} LEFT JOIN `demo3` `__data_demo3` ON [[__data_demo3.id]] IN ({:TEST}, {:TEST}) LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_one.rel_many_cascade]]) THEN [[demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[demo4_self_rel_one.rel_many_cascade]]) END) `demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `demo4_self_rel_one_rel_many_cascade` ON [[demo4_self_rel_one_rel_many_cascade.id]] = [[demo4_self_rel_one_rel_many_cascade_je.value]] WHERE (json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo4.self_rel_many]]) THEN [[__data_demo4.self_rel_many]] ELSE json_array([[__data_demo4.self_rel_many]]) END) > {:TEST} AND json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST} AND (json_array_length(CASE WHEN json_valid([[__data_demo3.files]]) THEN [[__data_demo3.files]] ELSE json_array([[__data_demo3.files]]) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__data_mm_demo3.files]]) THEN [[__data_mm_demo3.files]] ELSE json_array([[__data_mm_demo3.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__data_mm_demo3` ON `__data_mm_demo3`.`id` IN ({:TEST}, {:TEST}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE (NOT ([[__smTEST.multiMatchValue]] < {:TEST})) OR ([[__smTEST.multiMatchValue]] IS NULL))) AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END), '') = COALESCE({:TEST}, '') AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END), '') = COALESCE({:TEST}, '') AND (COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END), '') != COALESCE({:TEST}, '')) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN json_valid([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) THEN [[__mm_demo4_self_rel_one_rel_many_cascade.files]] ELSE json_array([[__mm_demo4_self_rel_one_rel_many_cascade.files]]) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4.self_rel_one]]) THEN [[__mm_demo4.self_rel_one]] ELSE json_array([[__mm_demo4.self_rel_one]]) END) `__mm_demo4_self_rel_one_je` LEFT JOIN `demo4` `__mm_demo4_self_rel_one` ON [[__mm_demo4_self_rel_one.id]] = [[__mm_demo4_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[__mm_demo4_self_rel_one.rel_many_cascade]]) THEN [[__mm_demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[__mm_demo4_self_rel_one.rel_many_cascade]]) END) `__mm_demo4_self_rel_one_rel_many_cascade_je` LEFT JOIN `demo3` `__mm_demo4_self_rel_one_rel_many_cascade` ON [[__mm_demo4_self_rel_one_rel_many_cascade.id]] = [[__mm_demo4_self_rel_one_rel_many_cascade_je.value]] WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE (NOT (COALESCE([[__smTEST.multiMatchValue]], '') != COALESCE({:TEST}, ''))) OR ([[__smTEST.multiMatchValue]] IS NULL))) AND COALESCE(json_array_length(CASE WHEN json_valid([[demo4_self_rel_one_rel_many_cascade.files]]) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END), '') != COALESCE({:TEST}, ''))",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
collection, err := app.Dao().FindCollectionByNameOrId(s.collectionIdOrName)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to load collection %s: %v", s.name, s.collectionIdOrName, err)
|
||||
t.Fatalf("[%s] Failed to load collection %s: %v", s.name, s.collectionIdOrName, err)
|
||||
}
|
||||
|
||||
query := app.Dao().RecordQuery(collection)
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, s.allowHiddenFields)
|
||||
for _, field := range s.fields {
|
||||
r.Resolve(field)
|
||||
|
||||
expr, err := search.FilterData(s.rule).BuildExpr(r)
|
||||
if err != nil {
|
||||
t.Fatalf("[%s] BuildExpr failed with error %v", s.name, err)
|
||||
}
|
||||
|
||||
if err := r.UpdateQuery(query); err != nil {
|
||||
t.Errorf("[%s] UpdateQuery failed with error %v", s.name, err)
|
||||
continue
|
||||
t.Fatalf("[%s] UpdateQuery failed with error %v", s.name, err)
|
||||
}
|
||||
|
||||
rawQuery := query.Build().SQL()
|
||||
rawQuery := query.AndWhere(expr).Build().SQL()
|
||||
|
||||
if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) {
|
||||
t.Errorf("[%s] Expected query\n %v \ngot:\n %v", s.name, s.expectQuery, rawQuery)
|
||||
// replace TEST placeholder with .+ regex pattern
|
||||
expectQuery := strings.ReplaceAll(
|
||||
"^"+regexp.QuoteMeta(s.expectQuery)+"$",
|
||||
"TEST",
|
||||
`\w+`,
|
||||
)
|
||||
|
||||
if !list.ExistInSliceWithRegex(rawQuery, []string{expectQuery}) {
|
||||
t.Fatalf("[%s] Expected query\n %v \ngot:\n %v", s.name, expectQuery, rawQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +390,7 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
name, params, err := r.Resolve(s.fieldName)
|
||||
r, err := r.Resolve(s.fieldName)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
@@ -238,13 +398,17 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
if name != s.expectName {
|
||||
t.Errorf("(%q) Expected name %q, got %q", s.fieldName, s.expectName, name)
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.Identifier != s.expectName {
|
||||
t.Errorf("(%q) Expected r.Identifier %q, got %q", s.fieldName, s.expectName, r.Identifier)
|
||||
}
|
||||
|
||||
// params should be empty for non @request fields
|
||||
if len(params) != 0 {
|
||||
t.Errorf("(%q) Expected 0 params, got %v", s.fieldName, params)
|
||||
if len(r.Params) != 0 {
|
||||
t.Errorf("(%q) Expected 0 r.Params, got %v", s.fieldName, r.Params)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +417,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo4")
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -269,8 +433,10 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
"a": 123,
|
||||
},
|
||||
Data: map[string]any{
|
||||
"b": 456,
|
||||
"c": map[string]int{"sub": 1},
|
||||
"number": "10",
|
||||
"number_unknown": "20",
|
||||
"b": 456,
|
||||
"c": map[string]int{"sub": 1},
|
||||
},
|
||||
AuthRecord: authRecord,
|
||||
}
|
||||
@@ -292,6 +458,8 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
{"@request.query.a.missing", false, ``},
|
||||
{"@request.data", true, ``},
|
||||
{"@request.data.b", false, `456`},
|
||||
{"@request.data.number", false, `10`}, // number field normalization
|
||||
{"@request.data.number_unknown", false, `"20"`}, // no numeric normalizations for unknown fields
|
||||
{"@request.data.b.missing", false, ``},
|
||||
{"@request.data.c", false, `"{\"sub\":1}"`},
|
||||
{"@request.auth", true, ""},
|
||||
@@ -304,7 +472,7 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
name, params, err := r.Resolve(s.fieldName)
|
||||
r, err := r.Resolve(s.fieldName)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
@@ -318,34 +486,34 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
|
||||
// missing key
|
||||
// ---
|
||||
if len(params) == 0 {
|
||||
if name != "NULL" {
|
||||
t.Errorf("(%d) Expected 0 placeholder parameters for %v, got %v", i, name, params)
|
||||
if len(r.Params) == 0 {
|
||||
if r.Identifier != "NULL" {
|
||||
t.Errorf("(%d) Expected 0 placeholder parameters for %v, got %v", i, r.Identifier, r.Params)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// existing key
|
||||
// ---
|
||||
if len(params) != 1 {
|
||||
t.Errorf("(%d) Expected 1 placeholder parameter for %v, got %v", i, name, params)
|
||||
if len(r.Params) != 1 {
|
||||
t.Errorf("(%d) Expected 1 placeholder parameter for %v, got %v", i, r.Identifier, r.Params)
|
||||
continue
|
||||
}
|
||||
|
||||
var paramName string
|
||||
var paramValue any
|
||||
for k, v := range params {
|
||||
for k, v := range r.Params {
|
||||
paramName = k
|
||||
paramValue = v
|
||||
}
|
||||
|
||||
if name != ("{:" + paramName + "}") {
|
||||
t.Errorf("(%d) Expected parameter name %q, got %q", i, paramName, name)
|
||||
if r.Identifier != ("{:" + paramName + "}") {
|
||||
t.Errorf("(%d) Expected parameter r.Identifier %q, got %q", i, paramName, r.Identifier)
|
||||
}
|
||||
|
||||
encodedParamValue, _ := json.Marshal(paramValue)
|
||||
if string(encodedParamValue) != s.expectParamValue {
|
||||
t.Errorf("(%d) Expected params %v for %v, got %v", i, s.expectParamValue, name, string(encodedParamValue))
|
||||
t.Errorf("(%d) Expected r.Params %v for %v, got %v", i, s.expectParamValue, r.Identifier, string(encodedParamValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
test
|
||||
@@ -0,0 +1 @@
|
||||
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":{"original-filename":"test.txt"},"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="}
|
||||
@@ -0,0 +1 @@
|
||||
test
|
||||
@@ -0,0 +1 @@
|
||||
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":{"original-filename":"test.txt"},"md5":"2Oj8otwPiW/Xy0ywAxuiSQ=="}
|
||||
+12
-7
@@ -9,13 +9,14 @@ import (
|
||||
|
||||
// AuthUser defines a standardized oauth2 user data structure.
|
||||
type AuthUser struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
RawUser map[string]any `json:"rawUser"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
RawUser map[string]any `json:"rawUser"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
// Provider defines a common interface for an OAuth2 client.
|
||||
@@ -105,6 +106,10 @@ func NewProviderByName(name string) (Provider, error) {
|
||||
return NewKakaoProvider(), nil
|
||||
case NameTwitch:
|
||||
return NewTwitchProvider(), nil
|
||||
case NameStrava:
|
||||
return NewStravaProvider(), nil
|
||||
case NameGitee:
|
||||
return NewGiteeProvider(), nil
|
||||
default:
|
||||
return nil, errors.New("Missing provider " + name)
|
||||
}
|
||||
|
||||
@@ -108,4 +108,22 @@ func TestNewProviderByName(t *testing.T) {
|
||||
if _, ok := p.(*auth.Twitch); !ok {
|
||||
t.Error("Expected to be instance of *auth.Twitch")
|
||||
}
|
||||
|
||||
// strava
|
||||
p, err = auth.NewProviderByName(auth.NameStrava)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Strava); !ok {
|
||||
t.Error("Expected to be instance of *auth.Strava")
|
||||
}
|
||||
|
||||
// gitee
|
||||
p, err = auth.NewProviderByName(auth.NameGitee)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Gitee); !ok {
|
||||
t.Error("Expected to be instance of *auth.Gitee")
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,13 @@ func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
username := fmt.Sprintf("%s#%s", extracted.Username, extracted.Discriminator)
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: username,
|
||||
Username: extracted.Username,
|
||||
AvatarUrl: avatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Id,
|
||||
Name: username,
|
||||
Username: extracted.Username,
|
||||
AvatarUrl: avatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
if extracted.Verified {
|
||||
user.Email = extracted.Email
|
||||
|
||||
@@ -54,12 +54,13 @@ func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.Picture.Data.Url,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.Picture.Data.Url,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var _ Provider = (*Gitee)(nil)
|
||||
|
||||
// NameGitee is the unique name of the Gitee provider.
|
||||
const NameGitee string = "gitee"
|
||||
|
||||
// Gitee allows authentication via Gitee OAuth2.
|
||||
type Gitee struct {
|
||||
*baseProvider
|
||||
}
|
||||
|
||||
// NewGiteeProvider creates new Gitee provider instance with some defaults.
|
||||
func NewGiteeProvider() *Gitee {
|
||||
return &Gitee{&baseProvider{
|
||||
scopes: []string{"user_info", "emails"},
|
||||
authUrl: "https://gitee.com/oauth/authorize",
|
||||
tokenUrl: "https://gitee.com/oauth/token",
|
||||
userApiUrl: "https://gitee.com/api/v5/user",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Gitee's user api.
|
||||
//
|
||||
// API reference: https://gitee.com/api/v5/swagger#/getV5User
|
||||
func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserData(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Login string `json:"login"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Login,
|
||||
AvatarUrl: extracted.AvatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
if extracted.Email != "" && is.EmailFormat.Validate(extracted.Email) == nil {
|
||||
// valid public primary email
|
||||
user.Email = extracted.Email
|
||||
} else {
|
||||
// send an additional optional request to retrieve the email
|
||||
email, err := p.fetchPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Email = email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchPrimaryEmail sends an API request to retrieve the verified primary email,
|
||||
// in case the user hasn't set "Public email address" or has unchecked
|
||||
// the "Access your emails data" permission during authentication.
|
||||
//
|
||||
// NB! This method can succeed and still return an empty email.
|
||||
// Error responses that are result of insufficient scopes permissions are ignored.
|
||||
//
|
||||
// API reference: https://gitee.com/api/v5/swagger#/getV5Emails
|
||||
func (p *Gitee) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Get("https://gitee.com/api/v5/emails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// ignore common http errors caused by insufficient scope permissions
|
||||
if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
emails := []struct {
|
||||
Email string
|
||||
State string
|
||||
Scope []string
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &emails); err != nil {
|
||||
// ignore unmarshal error in case "Keep my email address private"
|
||||
// was set because response.Body will be something like:
|
||||
// {"email":"12285415+test@user.noreply.gitee.com"}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// extract the first verified primary email
|
||||
for _, email := range emails {
|
||||
for _, scope := range email.Scope {
|
||||
if email.State == "confirmed" && scope == "primary" && is.EmailFormat.Validate(email.Email) == nil {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
+59
-36
@@ -55,48 +55,71 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Login,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.AvatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Login,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.AvatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
// in case user set "Keep my email address private",
|
||||
// email should be retrieved via extra API request
|
||||
// in case user has set "Keep my email address private", send an
|
||||
// **optional** API request to retrieve the verified primary email
|
||||
if user.Email == "" {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Get(p.userApiUrl + "/emails")
|
||||
email, err := p.fetchPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
emails := []struct {
|
||||
Email string
|
||||
Verified bool
|
||||
Primary bool
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &emails); err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// extract the verified primary email
|
||||
for _, email := range emails {
|
||||
if email.Verified && email.Primary {
|
||||
user.Email = email.Email
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
user.Email = email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchPrimaryEmail sends an API request to retrieve the verified
|
||||
// primary email, in case "Keep my email address private" was set.
|
||||
//
|
||||
// NB! This method can succeed and still return an empty email.
|
||||
// Error responses that are result of insufficient scopes permissions are ignored.
|
||||
//
|
||||
// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28
|
||||
func (p *Github) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Get(p.userApiUrl + "/emails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// ignore common http errors caused by insufficient scope permissions
|
||||
// (the email field is optional, aka. return the auth user without it)
|
||||
if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
emails := []struct {
|
||||
Email string
|
||||
Verified bool
|
||||
Primary bool
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &emails); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// extract the verified primary email
|
||||
for _, email := range emails {
|
||||
if email.Verified && email.Primary {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
@@ -53,13 +53,14 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.AvatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.AvatarUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
@@ -52,12 +52,13 @@ func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.Picture,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
AvatarUrl: extracted.Picture,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
+6
-5
@@ -59,11 +59,12 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Username: extracted.Profile.Nickname,
|
||||
AvatarUrl: extracted.Profile.ImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Username: extracted.Profile.Nickname,
|
||||
AvatarUrl: extracted.Profile.ImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
if extracted.KakaoAccount.IsEmailValid && extracted.KakaoAccount.IsEmailVerified {
|
||||
user.Email = extracted.KakaoAccount.Email
|
||||
|
||||
@@ -53,11 +53,12 @@ func (p *Microsoft) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
@@ -61,10 +61,11 @@ func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
if len(extracted.Images) > 0 {
|
||||
user.AvatarUrl = extracted.Images[0].Url
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var _ Provider = (*Strava)(nil)
|
||||
|
||||
// NameStrava is the unique name of the Strava provider.
|
||||
const NameStrava string = "strava"
|
||||
|
||||
// Strava allows authentication via Strava OAuth2.
|
||||
type Strava struct {
|
||||
*baseProvider
|
||||
}
|
||||
|
||||
// NewStravaProvider creates new Strava provider instance with some defaults.
|
||||
func NewStravaProvider() *Strava {
|
||||
return &Strava{&baseProvider{
|
||||
scopes: []string{
|
||||
"profile:read_all",
|
||||
},
|
||||
authUrl: "https://www.strava.com/oauth/authorize",
|
||||
tokenUrl: "https://www.strava.com/api/v3/oauth/token",
|
||||
userApiUrl: "https://www.strava.com/api/v3/athlete",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Strava's user api.
|
||||
//
|
||||
// API reference: https://developers.strava.com/docs/authentication/
|
||||
func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserData(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id int `json:"id"`
|
||||
FirstName string `json:"firstname"`
|
||||
LastName string `json:"lastname"`
|
||||
Username string `json:"username"`
|
||||
ProfileImageUrl string `json:"profile"`
|
||||
|
||||
// At the time of writing, Strava OAuth2 doesn't support returning the user email address
|
||||
// Email string `json:"email"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.Itoa(extracted.Id),
|
||||
Name: extracted.FirstName + " " + extracted.LastName,
|
||||
Username: extracted.Username,
|
||||
AvatarUrl: extracted.ProfileImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -61,13 +61,14 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data[0].Id,
|
||||
Name: extracted.Data[0].DisplayName,
|
||||
Username: extracted.Data[0].Login,
|
||||
Email: extracted.Data[0].Email,
|
||||
AvatarUrl: extracted.Data[0].ProfileImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Data[0].Id,
|
||||
Name: extracted.Data[0].DisplayName,
|
||||
Username: extracted.Data[0].Login,
|
||||
Email: extracted.Data[0].Email,
|
||||
AvatarUrl: extracted.Data[0].ProfileImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
@@ -63,12 +63,13 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Id,
|
||||
Name: extracted.Data.Name,
|
||||
Username: extracted.Data.Username,
|
||||
AvatarUrl: extracted.Data.ProfileImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
Id: extracted.Data.Id,
|
||||
Name: extracted.Data.Name,
|
||||
Username: extracted.Data.Username,
|
||||
AvatarUrl: extracted.Data.ProfileImageUrl,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
@@ -46,6 +48,23 @@ func NewFileFromPath(path string) (*File, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// NewFileFromBytes creates a new File instance from the provided byte slice.
|
||||
func NewFileFromBytes(b []byte, name string) (*File, error) {
|
||||
size := len(b)
|
||||
if size == 0 {
|
||||
return nil, errors.New("cannot create an empty file")
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
|
||||
f.Reader = &BytesReader{b}
|
||||
f.Size = int64(size)
|
||||
f.OriginalName = name
|
||||
f.Name = normalizeName(f.Reader, f.OriginalName)
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// NewFileFromMultipart creates a new File instace from the provided multipart header.
|
||||
func NewFileFromMultipart(mh *multipart.FileHeader) (*File, error) {
|
||||
f := &File{}
|
||||
@@ -62,7 +81,7 @@ func NewFileFromMultipart(mh *multipart.FileHeader) (*File, error) {
|
||||
|
||||
var _ FileReader = (*MultipartReader)(nil)
|
||||
|
||||
// MultipartReader defines a [multipart.FileHeader] reader.
|
||||
// MultipartReader defines a FileReader from [multipart.FileHeader].
|
||||
type MultipartReader struct {
|
||||
Header *multipart.FileHeader
|
||||
}
|
||||
@@ -76,6 +95,7 @@ func (r *MultipartReader) Open() (io.ReadSeekCloser, error) {
|
||||
|
||||
var _ FileReader = (*PathReader)(nil)
|
||||
|
||||
// PathReader defines a FileReader from a local file path.
|
||||
type PathReader struct {
|
||||
Path string
|
||||
}
|
||||
@@ -87,6 +107,29 @@ func (r *PathReader) Open() (io.ReadSeekCloser, error) {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ FileReader = (*BytesReader)(nil)
|
||||
|
||||
// BytesReader defines a FileReader from bytes content.
|
||||
type BytesReader struct {
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// Open implements the [filesystem.FileReader] interface.
|
||||
func (r *BytesReader) Open() (io.ReadSeekCloser, error) {
|
||||
return &bytesReadSeekCloser{bytes.NewReader(r.Bytes)}, nil
|
||||
}
|
||||
|
||||
type bytesReadSeekCloser struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
// Close implements the [io.ReadSeekCloser] interface.
|
||||
func (r *bytesReadSeekCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var extInvalidCharsRegex = regexp.MustCompile(`[^\w\.\*\-\+\=\#]+`)
|
||||
|
||||
func normalizeName(fr FileReader, name string) string {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
)
|
||||
|
||||
func TestNewFileFromFromPath(t *testing.T) {
|
||||
func TestNewFileFromPath(t *testing.T) {
|
||||
testDir := createTestDir(t)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestNewFileFromFromPath(t *testing.T) {
|
||||
t.Fatalf("Expected nil error, got %v", err)
|
||||
}
|
||||
if f.OriginalName != originalName {
|
||||
t.Fatalf("Expected originalName %q, got %q", originalName, f.OriginalName)
|
||||
t.Fatalf("Expected OriginalName %q, got %q", originalName, f.OriginalName)
|
||||
}
|
||||
if match, _ := regexp.Match(normalizedNamePattern, []byte(f.Name)); !match {
|
||||
t.Fatalf("Expected Name to match %v, got %q (%v)", normalizedNamePattern, f.Name, err)
|
||||
@@ -43,6 +43,34 @@ func TestNewFileFromFromPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileFromBytes(t *testing.T) {
|
||||
// nil bytes
|
||||
if _, err := filesystem.NewFileFromBytes(nil, "photo.jpg"); err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
// zero bytes
|
||||
if _, err := filesystem.NewFileFromBytes([]byte{}, "photo.jpg"); err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
originalName := "image_! noext"
|
||||
normalizedNamePattern := regexp.QuoteMeta("image_noext_") + `\w{10}` + regexp.QuoteMeta(".txt")
|
||||
f, err := filesystem.NewFileFromBytes([]byte("text\n"), originalName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Size != 5 {
|
||||
t.Fatalf("Expected Size %v, got %v", 5, f.Size)
|
||||
}
|
||||
if f.OriginalName != originalName {
|
||||
t.Fatalf("Expected OriginalName %q, got %q", originalName, f.OriginalName)
|
||||
}
|
||||
if match, _ := regexp.Match(normalizedNamePattern, []byte(f.Name)); !match {
|
||||
t.Fatalf("Expected Name to match %v, got %q (%v)", normalizedNamePattern, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileFromMultipart(t *testing.T) {
|
||||
formData, mp, err := tests.MockMultipartData(nil, "test")
|
||||
if err != nil {
|
||||
|
||||
@@ -225,13 +225,11 @@ func (s *System) DeletePrefix(prefix string) []error {
|
||||
})
|
||||
for {
|
||||
obj, err := iter.Next(s.ctx)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
failed = append(failed, err)
|
||||
continue
|
||||
if err != io.EOF {
|
||||
failed = append(failed, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err := s.Delete(obj.Key); err != nil {
|
||||
|
||||
@@ -10,6 +10,20 @@ import (
|
||||
|
||||
var cachedPatterns = map[string]*regexp.Regexp{}
|
||||
|
||||
// SubtractSlice returns a new slice with only the "base" elements
|
||||
// that don't exist in "subtract".
|
||||
func SubtractSlice[T comparable](base []T, subtract []T) []T {
|
||||
var result = make([]T, 0, len(base))
|
||||
|
||||
for _, b := range base {
|
||||
if !ExistInSlice(b, subtract) {
|
||||
result = append(result, b)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ExistInSlice checks whether a comparable element exists in a slice of the same type.
|
||||
func ExistInSlice[T comparable](item T, list []T) bool {
|
||||
if len(list) == 0 {
|
||||
|
||||
@@ -1,12 +1,111 @@
|
||||
package list_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestSubtractSliceString(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
base []string
|
||||
subtract []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
[]string{},
|
||||
[]string{},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
[]string{},
|
||||
[]string{"1", "2", "3", "4"},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
[]string{"1", "2", "3", "4"},
|
||||
[]string{},
|
||||
`["1","2","3","4"]`,
|
||||
},
|
||||
{
|
||||
[]string{"1", "2", "3", "4"},
|
||||
[]string{"1", "2", "3", "4"},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
[]string{"1", "2", "3", "4", "7"},
|
||||
[]string{"2", "4", "5", "6"},
|
||||
`["1","3","7"]`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := list.SubtractSlice(s.base, s.subtract)
|
||||
|
||||
raw, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("(%d) Failed to serialize: %v", i, err)
|
||||
}
|
||||
|
||||
strResult := string(raw)
|
||||
|
||||
if strResult != s.expected {
|
||||
t.Fatalf("(%d) Expected %v, got %v", i, s.expected, strResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtractSliceInt(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
base []int
|
||||
subtract []int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
[]int{},
|
||||
[]int{},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
[]int{},
|
||||
[]int{1, 2, 3, 4},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
[]int{1, 2, 3, 4},
|
||||
[]int{},
|
||||
`[1,2,3,4]`,
|
||||
},
|
||||
{
|
||||
[]int{1, 2, 3, 4},
|
||||
[]int{1, 2, 3, 4},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
[]int{1, 2, 3, 4, 7},
|
||||
[]int{2, 4, 5, 6},
|
||||
`[1,3,7]`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := list.SubtractSlice(s.base, s.subtract)
|
||||
|
||||
raw, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("(%d) Failed to serialize: %v", i, err)
|
||||
}
|
||||
|
||||
strResult := string(raw)
|
||||
|
||||
if strResult != s.expected {
|
||||
t.Fatalf("(%d) Expected %v, got %v", i, s.expected, strResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistInSliceString(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
item string
|
||||
|
||||
+59
-25
@@ -2,6 +2,7 @@ package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
@@ -72,9 +73,19 @@ func (r *Runner) Run(args ...string) error {
|
||||
}
|
||||
}
|
||||
|
||||
names, err := r.lastAppliedMigrations(toRevertCount)
|
||||
if err != nil {
|
||||
color.Red(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Do you really want to revert the last %d applied migration(s)?", toRevertCount),
|
||||
Message: fmt.Sprintf(
|
||||
"\n%v\nDo you really want to revert the last %d applied migration(s)?",
|
||||
strings.Join(names, "\n"),
|
||||
toRevertCount,
|
||||
),
|
||||
}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
@@ -138,38 +149,43 @@ func (r *Runner) Up() ([]string, error) {
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// Down reverts the last `toRevertCount` applied migrations.
|
||||
// Down reverts the last `toRevertCount` applied migrations
|
||||
// (in the order they were applied).
|
||||
//
|
||||
// On success returns list with the reverted migrations file names.
|
||||
func (r *Runner) Down(toRevertCount int) ([]string, error) {
|
||||
reverted := make([]string, 0, toRevertCount)
|
||||
|
||||
names, appliedErr := r.lastAppliedMigrations(toRevertCount)
|
||||
if appliedErr != nil {
|
||||
return nil, appliedErr
|
||||
}
|
||||
|
||||
err := r.db.Transactional(func(tx *dbx.Tx) error {
|
||||
for i := len(r.migrationsList.Items()) - 1; i >= 0; i-- {
|
||||
m := r.migrationsList.Item(i)
|
||||
|
||||
// skip unapplied
|
||||
if !r.isMigrationApplied(tx, m.File) {
|
||||
continue
|
||||
}
|
||||
|
||||
// revert limit reached
|
||||
if toRevertCount-len(reverted) <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// ignore empty Down action
|
||||
if m.Down != nil {
|
||||
if err := m.Down(tx); err != nil {
|
||||
return fmt.Errorf("Failed to revert migration %s: %w", m.File, err)
|
||||
for _, name := range names {
|
||||
for _, m := range r.migrationsList.Items() {
|
||||
if m.File != name {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.saveRevertedMigration(tx, m.File); err != nil {
|
||||
return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err)
|
||||
}
|
||||
// revert limit reached
|
||||
if toRevertCount-len(reverted) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
reverted = append(reverted, m.File)
|
||||
// ignore empty Down action
|
||||
if m.Down != nil {
|
||||
if err := m.Down(tx); err != nil {
|
||||
return fmt.Errorf("Failed to revert migration %s: %w", m.File, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.saveRevertedMigration(tx, m.File); err != nil {
|
||||
return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err)
|
||||
}
|
||||
|
||||
reverted = append(reverted, m.File)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -178,6 +194,7 @@ func (r *Runner) Down(toRevertCount int) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reverted, nil
|
||||
}
|
||||
|
||||
@@ -207,7 +224,7 @@ func (r *Runner) isMigrationApplied(tx dbx.Builder, file string) bool {
|
||||
func (r *Runner) saveAppliedMigration(tx dbx.Builder, file string) error {
|
||||
_, err := tx.Insert(r.tableName, dbx.Params{
|
||||
"file": file,
|
||||
"applied": time.Now().Unix(),
|
||||
"applied": time.Now().UnixMicro(),
|
||||
}).Execute()
|
||||
|
||||
return err
|
||||
@@ -218,3 +235,20 @@ func (r *Runner) saveRevertedMigration(tx dbx.Builder, file string) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Runner) lastAppliedMigrations(limit int) ([]string, error) {
|
||||
var files = make([]string, 0, limit)
|
||||
|
||||
err := r.db.Select("file").
|
||||
From(r.tableName).
|
||||
Where(dbx.Not(dbx.HashExp{"applied": nil})).
|
||||
OrderBy("applied DESC", "file DESC").
|
||||
Limit(int64(limit)).
|
||||
Column(&files)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package migrate
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -52,73 +53,88 @@ func TestRunnerUpAndDown(t *testing.T) {
|
||||
}
|
||||
defer testDB.Close()
|
||||
|
||||
var test1UpCalled bool
|
||||
var test1DownCalled bool
|
||||
var test2UpCalled bool
|
||||
var test2DownCalled bool
|
||||
callsOrder := []string{}
|
||||
|
||||
l := MigrationsList{}
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
test1UpCalled = true
|
||||
callsOrder = append(callsOrder, "up2")
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
test1DownCalled = true
|
||||
return nil
|
||||
}, "1_test")
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
test2UpCalled = true
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
test2DownCalled = true
|
||||
callsOrder = append(callsOrder, "down2")
|
||||
return nil
|
||||
}, "2_test")
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
callsOrder = append(callsOrder, "up3")
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
callsOrder = append(callsOrder, "down3")
|
||||
return nil
|
||||
}, "3_test")
|
||||
l.Register(func(db dbx.Builder) error {
|
||||
callsOrder = append(callsOrder, "up1")
|
||||
return nil
|
||||
}, func(db dbx.Builder) error {
|
||||
callsOrder = append(callsOrder, "down1")
|
||||
return nil
|
||||
}, "1_test")
|
||||
|
||||
r, err := NewRunner(testDB.DB, l)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// simulate partially run migration
|
||||
r.saveAppliedMigration(testDB, r.migrationsList.Item(0).File)
|
||||
// simulate partially out-of-order run migration
|
||||
r.saveAppliedMigration(testDB, "2_test")
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Up()
|
||||
// ---
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
if _, err := r.Up(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if test1UpCalled {
|
||||
t.Fatalf("Didn't expect 1_test to be called")
|
||||
}
|
||||
expectedUpCallsOrder := `["up1","up3"]` // skip up2 since it was applied previously
|
||||
|
||||
if !test2UpCalled {
|
||||
t.Fatalf("Expected 2_test to be called")
|
||||
}
|
||||
|
||||
// simulate unrun migration
|
||||
var test3DownCalled bool
|
||||
r.migrationsList.Register(nil, func(db dbx.Builder) error {
|
||||
test3DownCalled = true
|
||||
return nil
|
||||
}, "3_test")
|
||||
|
||||
// Down()
|
||||
// ---
|
||||
// revert one migration
|
||||
if _, err := r.Down(1); err != nil {
|
||||
upCallsOrder, err := json.Marshal(callsOrder)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if test3DownCalled {
|
||||
t.Fatal("Didn't expect 3_test to be reverted.")
|
||||
if v := string(upCallsOrder); v != expectedUpCallsOrder {
|
||||
t.Fatalf("Expected Up() calls order %s, got %s", expectedUpCallsOrder, upCallsOrder)
|
||||
}
|
||||
|
||||
if !test2DownCalled {
|
||||
t.Fatal("Expected 2_test to be reverted.")
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// reset callsOrder
|
||||
callsOrder = []string{}
|
||||
|
||||
// simulate unrun migration
|
||||
r.migrationsList.Register(nil, func(db dbx.Builder) error {
|
||||
callsOrder = append(callsOrder, "down4")
|
||||
return nil
|
||||
}, "4_test")
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Down()
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
if _, err := r.Down(2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if test1DownCalled {
|
||||
t.Fatal("Didn't expect 1_test to be reverted.")
|
||||
expectedDownCallsOrder := `["down3","down1"]` // revert in the applied order
|
||||
|
||||
downCallsOrder, err := json.Marshal(callsOrder)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v := string(downCallsOrder); v != expectedDownCallsOrder {
|
||||
t.Fatalf("Expected Down() calls order %s, got %s", expectedDownCallsOrder, downCallsOrder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+275
-66
@@ -85,72 +85,119 @@ func (f FilterData) build(data []fexpr.ExprGroup, fieldResolver FieldResolver) (
|
||||
}
|
||||
|
||||
func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldResolver) (dbx.Expression, error) {
|
||||
lName, lParams, lErr := f.resolveToken(expr.Left, fieldResolver)
|
||||
if lName == "" || lErr != nil {
|
||||
return nil, fmt.Errorf("Invalid left operand %q - %v.", expr.Left.Literal, lErr)
|
||||
lResult, lErr := resolveToken(expr.Left, fieldResolver)
|
||||
if lErr != nil || lResult.Identifier == "" {
|
||||
return nil, fmt.Errorf("invalid left operand %q - %v", expr.Left.Literal, lErr)
|
||||
}
|
||||
|
||||
rName, rParams, rErr := f.resolveToken(expr.Right, fieldResolver)
|
||||
if rName == "" || rErr != nil {
|
||||
return nil, fmt.Errorf("Invalid right operand %q - %v.", expr.Right.Literal, rErr)
|
||||
rResult, rErr := resolveToken(expr.Right, fieldResolver)
|
||||
if rErr != nil || rResult.Identifier == "" {
|
||||
return nil, fmt.Errorf("invalid right operand %q - %v", expr.Right.Literal, rErr)
|
||||
}
|
||||
|
||||
switch expr.Op {
|
||||
case fexpr.SignEq:
|
||||
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') = COALESCE(%s, '')", lName, rName), mergeParams(lParams, rParams)), nil
|
||||
case fexpr.SignNeq:
|
||||
return dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", lName, rName), mergeParams(lParams, rParams)), nil
|
||||
case fexpr.SignLike:
|
||||
// the right side is a column and therefor wrap it with "%" for contains like behavior
|
||||
if len(rParams) == 0 {
|
||||
return dbx.NewExp(fmt.Sprintf("%s LIKE ('%%' || %s || '%%') ESCAPE '\\'", lName, rName), lParams), nil
|
||||
}
|
||||
|
||||
return dbx.NewExp(fmt.Sprintf("%s LIKE %s ESCAPE '\\'", lName, rName), mergeParams(lParams, wrapLikeParams(rParams))), nil
|
||||
case fexpr.SignNlike:
|
||||
// the right side is a column and therefor wrap it with "%" for not-contains like behavior
|
||||
if len(rParams) == 0 {
|
||||
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE ('%%' || %s || '%%') ESCAPE '\\'", lName, rName), lParams), nil
|
||||
}
|
||||
|
||||
// normalize operands and switch sides if the left operand is a number/text, but the right one is a column
|
||||
// (usually this shouldn't be needed, but it's kept for backward compatibility)
|
||||
if len(lParams) > 0 && len(rParams) == 0 {
|
||||
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s ESCAPE '\\'", rName, lName), wrapLikeParams(lParams)), nil
|
||||
}
|
||||
|
||||
return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s ESCAPE '\\'", lName, rName), mergeParams(lParams, wrapLikeParams(rParams))), nil
|
||||
case fexpr.SignLt:
|
||||
return dbx.NewExp(fmt.Sprintf("%s < %s", lName, rName), mergeParams(lParams, rParams)), nil
|
||||
case fexpr.SignLte:
|
||||
return dbx.NewExp(fmt.Sprintf("%s <= %s", lName, rName), mergeParams(lParams, rParams)), nil
|
||||
case fexpr.SignGt:
|
||||
return dbx.NewExp(fmt.Sprintf("%s > %s", lName, rName), mergeParams(lParams, rParams)), nil
|
||||
case fexpr.SignGte:
|
||||
return dbx.NewExp(fmt.Sprintf("%s >= %s", lName, rName), mergeParams(lParams, rParams)), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unknown expression operator %q", expr.Op)
|
||||
return buildExpr(lResult, expr.Op, rResult)
|
||||
}
|
||||
|
||||
func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver) (name string, params dbx.Params, err error) {
|
||||
func buildExpr(
|
||||
left *ResolverResult,
|
||||
op fexpr.SignOp,
|
||||
right *ResolverResult,
|
||||
) (dbx.Expression, error) {
|
||||
var expr dbx.Expression
|
||||
|
||||
switch op {
|
||||
case fexpr.SignEq, fexpr.SignAnyEq:
|
||||
expr = dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') = COALESCE(%s, '')", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
case fexpr.SignNeq, fexpr.SignAnyNeq:
|
||||
expr = dbx.NewExp(fmt.Sprintf("COALESCE(%s, '') != COALESCE(%s, '')", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
case fexpr.SignLike, fexpr.SignAnyLike:
|
||||
// the right side is a column and therefor wrap it with "%" for contains like behavior
|
||||
if len(right.Params) == 0 {
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s LIKE ('%%' || %s || '%%') ESCAPE '\\'", left.Identifier, right.Identifier), left.Params)
|
||||
} else {
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s LIKE %s ESCAPE '\\'", left.Identifier, right.Identifier), mergeParams(left.Params, wrapLikeParams(right.Params)))
|
||||
}
|
||||
case fexpr.SignNlike, fexpr.SignAnyNlike:
|
||||
// the right side is a column and therefor wrap it with "%" for not-contains like behavior
|
||||
if len(right.Params) == 0 {
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s NOT LIKE ('%%' || %s || '%%') ESCAPE '\\'", left.Identifier, right.Identifier), left.Params)
|
||||
} else {
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s ESCAPE '\\'", left.Identifier, right.Identifier), mergeParams(left.Params, wrapLikeParams(right.Params)))
|
||||
}
|
||||
case fexpr.SignLt, fexpr.SignAnyLt:
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s < %s", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
case fexpr.SignLte, fexpr.SignAnyLte:
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s <= %s", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
case fexpr.SignGt, fexpr.SignAnyGt:
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s > %s", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
case fexpr.SignGte, fexpr.SignAnyGte:
|
||||
expr = dbx.NewExp(fmt.Sprintf("%s >= %s", left.Identifier, right.Identifier), mergeParams(left.Params, right.Params))
|
||||
}
|
||||
|
||||
if expr == nil {
|
||||
return nil, fmt.Errorf("unknown expression operator %q", op)
|
||||
}
|
||||
|
||||
// multi-match expressions
|
||||
if !isAnyMatchOp(op) {
|
||||
if left.MultiMatchSubQuery != nil && right.MultiMatchSubQuery != nil {
|
||||
mm := &manyVsManyExpr{
|
||||
leftSubQuery: left.MultiMatchSubQuery,
|
||||
rightSubQuery: right.MultiMatchSubQuery,
|
||||
op: op,
|
||||
}
|
||||
|
||||
expr = dbx.And(expr, mm)
|
||||
} else if left.MultiMatchSubQuery != nil {
|
||||
mm := &manyVsOneExpr{
|
||||
subQuery: left.MultiMatchSubQuery,
|
||||
op: op,
|
||||
otherOperand: right,
|
||||
}
|
||||
|
||||
expr = dbx.And(expr, mm)
|
||||
} else if right.MultiMatchSubQuery != nil {
|
||||
mm := &manyVsOneExpr{
|
||||
subQuery: right.MultiMatchSubQuery,
|
||||
op: op,
|
||||
otherOperand: left,
|
||||
inverse: true,
|
||||
}
|
||||
|
||||
expr = dbx.And(expr, mm)
|
||||
}
|
||||
}
|
||||
|
||||
if left.AfterBuild != nil {
|
||||
expr = left.AfterBuild(expr)
|
||||
}
|
||||
|
||||
if right.AfterBuild != nil {
|
||||
expr = right.AfterBuild(expr)
|
||||
}
|
||||
|
||||
return expr, nil
|
||||
}
|
||||
|
||||
func resolveToken(token fexpr.Token, fieldResolver FieldResolver) (*ResolverResult, error) {
|
||||
switch token.Type {
|
||||
case fexpr.TokenIdentifier:
|
||||
// current datetime constant
|
||||
// ---
|
||||
if token.Literal == "@now" {
|
||||
placeholder := "t" + security.PseudorandomString(8)
|
||||
name := fmt.Sprintf("{:%s}", placeholder)
|
||||
params := dbx.Params{placeholder: types.NowDateTime().String()}
|
||||
placeholder := "t" + security.PseudorandomString(5)
|
||||
|
||||
return name, params, nil
|
||||
return &ResolverResult{
|
||||
Identifier: "{:" + placeholder + "}",
|
||||
Params: dbx.Params{placeholder: types.NowDateTime().String()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// custom resolver
|
||||
// ---
|
||||
name, params, err := fieldResolver.Resolve(token.Literal)
|
||||
result, err := fieldResolver.Resolve(token.Literal)
|
||||
|
||||
if name == "" || err != nil {
|
||||
if err != nil || result.Identifier == "" {
|
||||
m := map[string]string{
|
||||
// if `null` field is missing, treat `null` identifier as NULL token
|
||||
"null": "NULL",
|
||||
@@ -160,27 +207,46 @@ func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver)
|
||||
"false": "0",
|
||||
}
|
||||
if v, ok := m[strings.ToLower(token.Literal)]; ok {
|
||||
return v, nil, nil
|
||||
return &ResolverResult{Identifier: v}, nil
|
||||
}
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return name, params, err
|
||||
return result, err
|
||||
case fexpr.TokenText:
|
||||
placeholder := "t" + security.PseudorandomString(8)
|
||||
name := fmt.Sprintf("{:%s}", placeholder)
|
||||
params := dbx.Params{placeholder: token.Literal}
|
||||
placeholder := "t" + security.PseudorandomString(5)
|
||||
|
||||
return name, params, nil
|
||||
return &ResolverResult{
|
||||
Identifier: "{:" + placeholder + "}",
|
||||
Params: dbx.Params{placeholder: token.Literal},
|
||||
}, nil
|
||||
case fexpr.TokenNumber:
|
||||
placeholder := "t" + security.PseudorandomString(8)
|
||||
name := fmt.Sprintf("{:%s}", placeholder)
|
||||
params := dbx.Params{placeholder: cast.ToFloat64(token.Literal)}
|
||||
placeholder := "t" + security.PseudorandomString(5)
|
||||
|
||||
return name, params, nil
|
||||
return &ResolverResult{
|
||||
Identifier: "{:" + placeholder + "}",
|
||||
Params: dbx.Params{placeholder: cast.ToFloat64(token.Literal)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return "", nil, errors.New("Unresolvable token type.")
|
||||
return nil, errors.New("unresolvable token type")
|
||||
}
|
||||
|
||||
func isAnyMatchOp(op fexpr.SignOp) bool {
|
||||
switch op {
|
||||
case
|
||||
fexpr.SignAnyEq,
|
||||
fexpr.SignAnyNeq,
|
||||
fexpr.SignAnyLike,
|
||||
fexpr.SignAnyNlike,
|
||||
fexpr.SignAnyLt,
|
||||
fexpr.SignAnyLte,
|
||||
fexpr.SignAnyGt,
|
||||
fexpr.SignAnyGte:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// mergeParams returns new dbx.Params where each provided params item
|
||||
@@ -218,18 +284,24 @@ func wrapLikeParams(params dbx.Params) dbx.Params {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ dbx.Expression = (*opExpr)(nil)
|
||||
|
||||
// opExpr defines an expression that contains a raw sql operator string.
|
||||
type opExpr struct {
|
||||
op string
|
||||
}
|
||||
|
||||
// Build converts an expression into a SQL fragment.
|
||||
// Build converts the expression into a SQL fragment.
|
||||
//
|
||||
// Implements [dbx.Expression] interface.
|
||||
func (e *opExpr) Build(db *dbx.DB, params dbx.Params) string {
|
||||
return e.op
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ dbx.Expression = (*concatExpr)(nil)
|
||||
|
||||
// concatExpr defines an expression that concatenates multiple
|
||||
// other expressions with a specified separator.
|
||||
type concatExpr struct {
|
||||
@@ -237,7 +309,7 @@ type concatExpr struct {
|
||||
separator string
|
||||
}
|
||||
|
||||
// Build converts an expression into a SQL fragment.
|
||||
// Build converts the expression into a SQL fragment.
|
||||
//
|
||||
// Implements [dbx.Expression] interface.
|
||||
func (e *concatExpr) Build(db *dbx.DB, params dbx.Params) string {
|
||||
@@ -247,12 +319,12 @@ func (e *concatExpr) Build(db *dbx.DB, params dbx.Params) string {
|
||||
|
||||
stringParts := make([]string, 0, len(e.parts))
|
||||
|
||||
for _, a := range e.parts {
|
||||
if a == nil {
|
||||
for _, p := range e.parts {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if sql := a.Build(db, params); sql != "" {
|
||||
if sql := p.Build(db, params); sql != "" {
|
||||
stringParts = append(stringParts, sql)
|
||||
}
|
||||
}
|
||||
@@ -267,3 +339,140 @@ func (e *concatExpr) Build(db *dbx.DB, params dbx.Params) string {
|
||||
|
||||
return "(" + strings.Join(stringParts, e.separator) + ")"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ dbx.Expression = (*manyVsManyExpr)(nil)
|
||||
|
||||
// manyVsManyExpr constructs a multi-match many<->many db where expression.
|
||||
//
|
||||
// Expects leftSubQuery and rightSubQuery to return a subquery with a
|
||||
// single "multiMatchValue" column.
|
||||
type manyVsManyExpr struct {
|
||||
leftSubQuery dbx.Expression
|
||||
rightSubQuery dbx.Expression
|
||||
op fexpr.SignOp
|
||||
}
|
||||
|
||||
// Build converts the expression into a SQL fragment.
|
||||
//
|
||||
// Implements [dbx.Expression] interface.
|
||||
func (e *manyVsManyExpr) Build(db *dbx.DB, params dbx.Params) string {
|
||||
if e.leftSubQuery == nil || e.rightSubQuery == nil {
|
||||
return "0=1"
|
||||
}
|
||||
|
||||
lAlias := "__ml" + security.PseudorandomString(5)
|
||||
rAlias := "__mr" + security.PseudorandomString(5)
|
||||
|
||||
whereExpr, buildErr := buildExpr(
|
||||
&ResolverResult{
|
||||
Identifier: "[[" + lAlias + ".multiMatchValue]]",
|
||||
},
|
||||
e.op,
|
||||
&ResolverResult{
|
||||
Identifier: "[[" + rAlias + ".multiMatchValue]]",
|
||||
// note: the AfterBuild needs to be handled only once and it
|
||||
// doesn't matter whether it is applied on the left or right subquery operand
|
||||
AfterBuild: multiMatchAfterBuildFunc(e.op, lAlias, rAlias),
|
||||
},
|
||||
)
|
||||
|
||||
if buildErr != nil {
|
||||
return "0=1"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"NOT EXISTS (SELECT 1 FROM (%s) {{%s}} LEFT JOIN (%s) {{%s}} WHERE %s)",
|
||||
e.leftSubQuery.Build(db, params),
|
||||
lAlias,
|
||||
e.rightSubQuery.Build(db, params),
|
||||
rAlias,
|
||||
whereExpr.Build(db, params),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var _ dbx.Expression = (*manyVsOneExpr)(nil)
|
||||
|
||||
// manyVsManyExpr constructs a multi-match many<->one db where expression.
|
||||
//
|
||||
// Expects subQuery to return a subquery with a single "multiMatchValue" column.
|
||||
//
|
||||
// You can set inverse=false to reverse the condition sides (aka. one<->many).
|
||||
type manyVsOneExpr struct {
|
||||
subQuery dbx.Expression
|
||||
op fexpr.SignOp
|
||||
otherOperand *ResolverResult
|
||||
inverse bool
|
||||
}
|
||||
|
||||
// Build converts the expression into a SQL fragment.
|
||||
//
|
||||
// Implements [dbx.Expression] interface.
|
||||
func (e *manyVsOneExpr) Build(db *dbx.DB, params dbx.Params) string {
|
||||
if e.subQuery == nil {
|
||||
return "0=1"
|
||||
}
|
||||
|
||||
alias := "__sm" + security.PseudorandomString(5)
|
||||
|
||||
r1 := &ResolverResult{
|
||||
Identifier: "[[" + alias + ".multiMatchValue]]",
|
||||
AfterBuild: multiMatchAfterBuildFunc(e.op, alias),
|
||||
}
|
||||
|
||||
r2 := &ResolverResult{
|
||||
Identifier: e.otherOperand.Identifier,
|
||||
Params: e.otherOperand.Params,
|
||||
}
|
||||
|
||||
var whereExpr dbx.Expression
|
||||
var buildErr error
|
||||
|
||||
if e.inverse {
|
||||
whereExpr, buildErr = buildExpr(r2, e.op, r1)
|
||||
} else {
|
||||
whereExpr, buildErr = buildExpr(r1, e.op, r2)
|
||||
}
|
||||
|
||||
if buildErr != nil {
|
||||
return "0=1"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"NOT EXISTS (SELECT 1 FROM (%s) {{%s}} WHERE %s)",
|
||||
e.subQuery.Build(db, params),
|
||||
alias,
|
||||
whereExpr.Build(db, params),
|
||||
)
|
||||
}
|
||||
|
||||
func multiMatchAfterBuildFunc(op fexpr.SignOp, multiMatchAliases ...string) func(dbx.Expression) dbx.Expression {
|
||||
return func(expr dbx.Expression) dbx.Expression {
|
||||
expr = dbx.Not(expr) // inverse for the not-exist expression
|
||||
|
||||
if op == fexpr.SignEq {
|
||||
return expr
|
||||
}
|
||||
|
||||
orExprs := make([]dbx.Expression, len(multiMatchAliases)+1)
|
||||
orExprs[0] = expr
|
||||
|
||||
// Add an optional "IS NULL" condition(s) to handle the empty rows result.
|
||||
//
|
||||
// For example, let's assume that some "rel" field is [nonemptyRel1, nonemptyRel2, emptyRel3],
|
||||
// The filter "rel.total > 0" will ensures that the above will return true only if all relations
|
||||
// are existing and match the condition.
|
||||
//
|
||||
// The "=" operator is excluded because it will never equal directly with NULL anyway
|
||||
// and also because we want in case "rel.id = ''" is specified to allow
|
||||
// matching the empty relations (they will match due to the applied COALESCE).
|
||||
for i, mAlias := range multiMatchAliases {
|
||||
orExprs[i+1] = dbx.NewExp("[[" + mAlias + ".multiMatchValue]] IS NULL")
|
||||
}
|
||||
|
||||
return dbx.Or(orExprs...)
|
||||
}
|
||||
}
|
||||
@@ -495,12 +495,12 @@ func (t *testFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testFieldResolver) Resolve(field string) (name string, placeholderParams dbx.Params, err error) {
|
||||
func (t *testFieldResolver) Resolve(field string) (*ResolverResult, error) {
|
||||
t.ResolveCalls++
|
||||
|
||||
if field == "unknown" {
|
||||
return "", nil, errors.New("test error")
|
||||
return nil, errors.New("test error")
|
||||
}
|
||||
|
||||
return field, nil, nil
|
||||
return &ResolverResult{Identifier: field}, nil
|
||||
}
|
||||
@@ -8,6 +8,25 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
)
|
||||
|
||||
// ResolverResult defines a single FieldResolver.Resolve() successfully parsed result.
|
||||
type ResolverResult struct {
|
||||
// Identifier is the plain SQL identifier/column that will be used
|
||||
// in the final db expression as left or right operand.
|
||||
Identifier string
|
||||
|
||||
// Params is a map with db placeholder->value pairs that will be added
|
||||
// to the query when building both resolved operands/sides in a single expression.
|
||||
Params dbx.Params
|
||||
|
||||
// MultiMatchSubQuery is an optional sub query expression that will be added
|
||||
// in addition to the combined ResolverResult expression during build.
|
||||
MultiMatchSubQuery dbx.Expression
|
||||
|
||||
// AfterBuild is an optional function that will be called after building
|
||||
// and combining the result of both resolved operands/sides in a single expression.
|
||||
AfterBuild func(expr dbx.Expression) dbx.Expression
|
||||
}
|
||||
|
||||
// FieldResolver defines an interface for managing search fields.
|
||||
type FieldResolver interface {
|
||||
// UpdateQuery allows to updated the provided db query based on the
|
||||
@@ -18,7 +37,7 @@ type FieldResolver interface {
|
||||
|
||||
// Resolve parses the provided field and returns a properly
|
||||
// formatted db identifier (eg. NULL, quoted column, placeholder parameter, etc.).
|
||||
Resolve(field string) (name string, placeholderParams dbx.Params, err error)
|
||||
Resolve(field string) (*ResolverResult, error)
|
||||
}
|
||||
|
||||
// NewSimpleFieldResolver creates a new `SimpleFieldResolver` with the
|
||||
@@ -49,10 +68,12 @@ func (r *SimpleFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
// Resolve implements `search.Resolve` interface.
|
||||
//
|
||||
// Returns error if `field` is not in `r.allowedFields`.
|
||||
func (r *SimpleFieldResolver) Resolve(field string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
func (r *SimpleFieldResolver) Resolve(field string) (*ResolverResult, error) {
|
||||
if !list.ExistInSliceWithRegex(field, r.allowedFields) {
|
||||
return "", nil, fmt.Errorf("Failed to resolve field %q.", field)
|
||||
return nil, fmt.Errorf("Failed to resolve field %q.", field)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[[%s]]", inflector.Columnify(field)), nil, nil
|
||||
return &ResolverResult{
|
||||
Identifier: "[[" + inflector.Columnify(field) + "]]",
|
||||
}, nil
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TestSimpleFieldResolverResolve(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
name, params, err := r.Resolve(s.fieldName)
|
||||
r, err := r.Resolve(s.fieldName)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
@@ -69,13 +69,17 @@ func TestSimpleFieldResolverResolve(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
if name != s.expectName {
|
||||
t.Errorf("(%d) Expected name %q, got %q", i, s.expectName, name)
|
||||
if hasErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.Identifier != s.expectName {
|
||||
t.Errorf("(%d) Expected r.Identifier %q, got %q", i, s.expectName, r.Identifier)
|
||||
}
|
||||
|
||||
// params should be empty
|
||||
if len(params) != 0 {
|
||||
t.Errorf("(%d) Expected 0 params, got %v", i, params)
|
||||
if len(r.Params) != 0 {
|
||||
t.Errorf("(%d) Expected 0 r.Params, got %v", i, r.Params)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-6
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const randomSortKey string = "@random"
|
||||
|
||||
// sort field directions
|
||||
const (
|
||||
SortAsc string = "ASC"
|
||||
@@ -19,14 +21,19 @@ type SortField struct {
|
||||
|
||||
// BuildExpr resolves the sort field into a valid db sort expression.
|
||||
func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) {
|
||||
name, params, err := fieldResolver.Resolve(s.Name)
|
||||
|
||||
// invalidate empty fields and non-column identifiers
|
||||
if err != nil || len(params) > 0 || name == "" || strings.ToLower(name) == "null" {
|
||||
return "", fmt.Errorf("Invalid sort field %q.", s.Name)
|
||||
// special case for random sort
|
||||
if s.Name == randomSortKey {
|
||||
return "RANDOM()", nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", name, s.Direction), nil
|
||||
result, err := fieldResolver.Resolve(s.Name)
|
||||
|
||||
// invalidate empty fields and non-column identifiers
|
||||
if err != nil || len(result.Params) > 0 || result.Identifier == "" || strings.ToLower(result.Identifier) == "null" {
|
||||
return "", fmt.Errorf("invalid sort field %q", s.Name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", result.Identifier, s.Direction), nil
|
||||
}
|
||||
|
||||
// ParseSortFromString parses the provided string expression
|
||||
|
||||
@@ -27,6 +27,8 @@ func TestSortFieldBuildExpr(t *testing.T) {
|
||||
{search.SortField{"test1", search.SortAsc}, false, "[[test1]] ASC"},
|
||||
// allowed field - desc
|
||||
{search.SortField{"test1", search.SortDesc}, false, "[[test1]] DESC"},
|
||||
// special @random field (ignore direction)
|
||||
{search.SortField{"@random", search.SortDesc}, false, "RANDOM()"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -54,6 +56,7 @@ func TestParseSortFromString(t *testing.T) {
|
||||
{"+test", `[{"name":"test","direction":"ASC"}]`},
|
||||
{"-test", `[{"name":"test","direction":"DESC"}]`},
|
||||
{"test1,-test2,+test3", `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"DESC"},{"name":"test3","direction":"ASC"}]`},
|
||||
{"@random,-test", `[{"name":"@random","direction":"ASC"},{"name":"test","direction":"DESC"}]`},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
|
||||
@@ -4,7 +4,8 @@ PB_INSTALLER_PARAM = "installer"
|
||||
PB_OAUTH2_EXAMPLE = "https://pocketbase.io/docs/authentication/#web-oauth2-integration"
|
||||
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/api-rules-and-filters/"
|
||||
PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling/"
|
||||
PB_DOCS_URL = "https://pocketbase.io/docs/"
|
||||
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
|
||||
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
|
||||
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
|
||||
PB_VERSION = "v0.10.4"
|
||||
PB_VERSION = "v0.11.2"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as _e,f as k,g as h,h as n,m as me,x as G,O as re,P as we,k as ve,Q as Ce,n as Pe,t as L,a as Y,o as _,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index.5dab115f.js";import{S as Be}from"./SdkTabs.25abcc83.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",m,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),m=b(s),f=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,m),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(m,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&_(o),i=!1,u()}}}function he(a,l){let o,s,m,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),_e(s.$$.fragment),m=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),me(s,o,null),n(o,m),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&H(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&_(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",m,f,i,u,d,v,C,F=a[0].name+"",U,R,q,P,D,j,W,M,K,X,Q,A,Z,V,y=a[0].name+"",I,x,E,B,J,S,O,w=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
|
||||
import{S as ke,i as be,s as ge,e as r,w as b,b as g,c as _e,f as k,g as h,h as n,m as me,x as G,O as re,P as we,k as ve,Q as Ce,n as Pe,t as L,a as Y,o as _,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index.536fddcb.js";import{S as Be}from"./SdkTabs.3571c26b.js";function ue(a,l,o){const s=a.slice();return s[5]=l[o],s}function de(a,l,o){const s=a.slice();return s[5]=l[o],s}function fe(a,l){let o,s=l[5].code+"",m,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),m=b(s),f=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(v,C){h(v,o,C),n(o,m),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(m,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&_(o),i=!1,u()}}}function he(a,l){let o,s,m,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),_e(s.$$.fragment),m=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),me(s,o,null),n(o,m),f=!0},p(i,u){l=i;const d={};u&4&&(d.content=l[5].body),s.$set(d),(!f||u&6)&&H(o,"active",l[1]===l[5].code)},i(i){f||(L(s.$$.fragment,i),f=!0)},o(i){Y(s.$$.fragment,i),f=!1},d(i){i&&_(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",m,f,i,u,d,v,C,F=a[0].name+"",U,R,q,P,D,j,W,M,K,X,Q,A,Z,V,y=a[0].name+"",I,x,E,B,J,S,O,w=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[3]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as c,h as o,m as ne,x as re,O as qe,P as xe,k as Ie,Q as Je,n as Ke,t as U,a as j,o as d,d as ie,R as Qe,C as He,p as We,r as x,u as Ge}from"./index.5dab115f.js";import{S as Xe}from"./SdkTabs.25abcc83.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){c(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&d(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){c(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(s),ie(n)}}}function Ye(r){var Be,Me;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,B,I,S,F,ce,L,M,de,J,N=r[0].name+"",K,ue,pe,V,Q,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,$=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
|
||||
import{S as ze,i as Ue,s as je,N as Ve,e as a,w as k,b as p,c as ae,f as b,g as c,h as o,m as ne,x as re,O as qe,P as xe,k as Ie,Q as Je,n as Ke,t as U,a as j,o as d,d as ie,R as Qe,C as He,p as We,r as x,u as Ge}from"./index.536fddcb.js";import{S as Xe}from"./SdkTabs.3571c26b.js";function Ee(r,l,s){const n=r.slice();return n[5]=l[s],n}function Fe(r,l,s){const n=r.slice();return n[5]=l[s],n}function Le(r,l){let s,n=l[5].code+"",m,_,i,f;function v(){return l[4](l[5])}return{key:r,first:null,c(){s=a("button"),m=k(n),_=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(g,w){c(g,s,w),o(s,m),o(s,_),i||(f=Ge(s,"click",v),i=!0)},p(g,w){l=g,w&4&&n!==(n=l[5].code+"")&&re(m,n),w&6&&x(s,"active",l[1]===l[5].code)},d(g){g&&d(s),i=!1,f()}}}function Ne(r,l){let s,n,m,_;return n=new Ve({props:{content:l[5].body}}),{key:r,first:null,c(){s=a("div"),ae(n.$$.fragment),m=p(),b(s,"class","tab-item"),x(s,"active",l[1]===l[5].code),this.first=s},m(i,f){c(i,s,f),ne(n,s,null),o(s,m),_=!0},p(i,f){l=i;const v={};f&4&&(v.content=l[5].body),n.$set(v),(!_||f&6)&&x(s,"active",l[1]===l[5].code)},i(i){_||(U(n.$$.fragment,i),_=!0)},o(i){j(n.$$.fragment,i),_=!1},d(i){i&&d(s),ie(n)}}}function Ye(r){var Be,Me;let l,s,n=r[0].name+"",m,_,i,f,v,g,w,B,I,S,F,ce,L,M,de,J,N=r[0].name+"",K,ue,pe,V,Q,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,$=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as je,i as He,s as Je,N as We,e as s,w as v,b as p,c as re,f as h,g as r,h as a,m as ce,x as de,O as Ve,P as Ne,k as Qe,Q as ze,n as Ke,t as j,a as H,o as c,d as ue,R as Ye,C as Be,p as Ge,r as J,u as Xe}from"./index.5dab115f.js";import{S as Ze}from"./SdkTabs.25abcc83.js";function Fe(i,l,o){const n=i.slice();return n[5]=l[o],n}function Le(i,l,o){const n=i.slice();return n[5]=l[o],n}function xe(i,l){let o,n=l[5].code+"",m,_,d,b;function g(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=v(n),_=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(k,R){r(k,o,R),a(o,m),a(o,_),d||(b=Xe(o,"click",g),d=!0)},p(k,R){l=k,R&4&&n!==(n=l[5].code+"")&&de(m,n),R&6&&J(o,"active",l[1]===l[5].code)},d(k){k&&c(o),d=!1,b()}}}function Me(i,l){let o,n,m,_;return n=new We({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),re(n.$$.fragment),m=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(d,b){r(d,o,b),ce(n,o,null),a(o,m),_=!0},p(d,b){l=d;const g={};b&4&&(g.content=l[5].body),n.$set(g),(!_||b&6)&&J(o,"active",l[1]===l[5].code)},i(d){_||(j(n.$$.fragment,d),_=!0)},o(d){H(n.$$.fragment,d),_=!1},d(d){d&&c(o),ue(n)}}}function et(i){var qe,Ie;let l,o,n=i[0].name+"",m,_,d,b,g,k,R,C,N,y,L,pe,x,D,he,Q,M=i[0].name+"",z,be,K,q,Y,I,G,P,X,O,Z,fe,ee,$,te,me,ae,_e,f,ve,E,ge,ke,we,le,Se,oe,Re,ye,Oe,se,$e,ne,U,ie,A,V,S=[],Ae=new Map,Ee,B,w=[],Te=new Map,T;k=new Ze({props:{js:`
|
||||
import{S as je,i as He,s as Je,N as We,e as s,w as v,b as p,c as re,f as h,g as r,h as a,m as ce,x as de,O as Ve,P as Ne,k as Qe,Q as ze,n as Ke,t as j,a as H,o as c,d as ue,R as Ye,C as Be,p as Ge,r as J,u as Xe}from"./index.536fddcb.js";import{S as Ze}from"./SdkTabs.3571c26b.js";function Fe(i,l,o){const n=i.slice();return n[5]=l[o],n}function Le(i,l,o){const n=i.slice();return n[5]=l[o],n}function xe(i,l){let o,n=l[5].code+"",m,_,d,b;function g(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=v(n),_=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(k,R){r(k,o,R),a(o,m),a(o,_),d||(b=Xe(o,"click",g),d=!0)},p(k,R){l=k,R&4&&n!==(n=l[5].code+"")&&de(m,n),R&6&&J(o,"active",l[1]===l[5].code)},d(k){k&&c(o),d=!1,b()}}}function Me(i,l){let o,n,m,_;return n=new We({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),re(n.$$.fragment),m=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(d,b){r(d,o,b),ce(n,o,null),a(o,m),_=!0},p(d,b){l=d;const g={};b&4&&(g.content=l[5].body),n.$set(g),(!_||b&6)&&J(o,"active",l[1]===l[5].code)},i(d){_||(j(n.$$.fragment,d),_=!0)},o(d){H(n.$$.fragment,d),_=!1},d(d){d&&c(o),ue(n)}}}function et(i){var qe,Ie;let l,o,n=i[0].name+"",m,_,d,b,g,k,R,C,N,y,L,pe,x,D,he,Q,M=i[0].name+"",z,be,K,q,Y,I,G,P,X,O,Z,fe,ee,$,te,me,ae,_e,f,ve,E,ge,ke,we,le,Se,oe,Re,ye,Oe,se,$e,ne,U,ie,A,V,S=[],Ae=new Map,Ee,B,w=[],Te=new Map,T;k=new Ze({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i[3]}');
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as r,h as o,m as At,x as Tt,O as ce,P as ye,k as ge,Q as Pe,n as Re,t as tt,a as et,o as c,d as Ut,R as $e,C as de,p as Ce,r as lt,u as Oe}from"./index.5dab115f.js";import{S as Ae}from"./SdkTabs.25abcc83.js";function ue(n,e,l){const i=n.slice();return i[8]=e[l],i}function fe(n,e,l){const i=n.slice();return i[8]=e[l],i}function Te(n){let e;return{c(){e=f("email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Ue(n){let e;return{c(){e=f("username")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Me(n){let e;return{c(){e=f("username/email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function pe(n){let e;return{c(){e=s("strong"),e.textContent="username"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function be(n){let e;return{c(){e=f("or")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function me(n){let e;return{c(){e=s("strong"),e.textContent="email"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function he(n,e){let l,i=e[8].code+"",S,m,p,d;function _(){return e[7](e[8])}return{key:n,first:null,c(){l=s("button"),S=f(i),m=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m($,C){r($,l,C),o(l,S),o(l,m),p||(d=Oe(l,"click",_),p=!0)},p($,C){e=$,C&16&&i!==(i=e[8].code+"")&&Tt(S,i),C&24&<(l,"active",e[3]===e[8].code)},d($){$&&c(l),p=!1,d()}}}function _e(n,e){let l,i,S,m;return i=new ke({props:{content:e[8].body}}),{key:n,first:null,c(){l=s("div"),Ot(i.$$.fragment),S=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m(p,d){r(p,l,d),At(i,l,null),o(l,S),m=!0},p(p,d){e=p;const _={};d&16&&(_.content=e[8].body),i.$set(_),(!m||d&24)&<(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),Ut(i)}}}function De(n){var se,ne;let e,l,i=n[0].name+"",S,m,p,d,_,$,C,O,B,Mt,ot,T,at,F,st,U,G,Dt,X,I,Et,nt,Z=n[0].name+"",it,Wt,rt,N,ct,M,dt,Lt,V,D,ut,Bt,ft,Ht,g,Yt,pt,bt,mt,qt,ht,_t,j,kt,E,St,Ft,vt,W,wt,It,yt,Nt,k,Vt,H,jt,Jt,Qt,gt,Kt,Pt,zt,Gt,Xt,Rt,Zt,$t,J,Ct,L,Q,A=[],xt=new Map,te,K,P=[],ee=new Map,Y;function le(t,a){if(t[1]&&t[2])return Me;if(t[1])return Ue;if(t[2])return Te}let q=le(n),R=q&&q(n);T=new Ae({props:{js:`
|
||||
import{S as Se,i as ve,s as we,N as ke,e as s,w as f,b as u,c as Ot,f as h,g as r,h as o,m as At,x as Tt,O as ce,P as ye,k as ge,Q as Pe,n as Re,t as tt,a as et,o as c,d as Ut,R as $e,C as de,p as Ce,r as lt,u as Oe}from"./index.536fddcb.js";import{S as Ae}from"./SdkTabs.3571c26b.js";function ue(n,e,l){const i=n.slice();return i[8]=e[l],i}function fe(n,e,l){const i=n.slice();return i[8]=e[l],i}function Te(n){let e;return{c(){e=f("email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Ue(n){let e;return{c(){e=f("username")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function Me(n){let e;return{c(){e=f("username/email")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function pe(n){let e;return{c(){e=s("strong"),e.textContent="username"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function be(n){let e;return{c(){e=f("or")},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function me(n){let e;return{c(){e=s("strong"),e.textContent="email"},m(l,i){r(l,e,i)},d(l){l&&c(e)}}}function he(n,e){let l,i=e[8].code+"",S,m,p,d;function _(){return e[7](e[8])}return{key:n,first:null,c(){l=s("button"),S=f(i),m=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m($,C){r($,l,C),o(l,S),o(l,m),p||(d=Oe(l,"click",_),p=!0)},p($,C){e=$,C&16&&i!==(i=e[8].code+"")&&Tt(S,i),C&24&<(l,"active",e[3]===e[8].code)},d($){$&&c(l),p=!1,d()}}}function _e(n,e){let l,i,S,m;return i=new ke({props:{content:e[8].body}}),{key:n,first:null,c(){l=s("div"),Ot(i.$$.fragment),S=u(),h(l,"class","tab-item"),lt(l,"active",e[3]===e[8].code),this.first=l},m(p,d){r(p,l,d),At(i,l,null),o(l,S),m=!0},p(p,d){e=p;const _={};d&16&&(_.content=e[8].body),i.$set(_),(!m||d&24)&<(l,"active",e[3]===e[8].code)},i(p){m||(tt(i.$$.fragment,p),m=!0)},o(p){et(i.$$.fragment,p),m=!1},d(p){p&&c(l),Ut(i)}}}function De(n){var se,ne;let e,l,i=n[0].name+"",S,m,p,d,_,$,C,O,B,Mt,ot,T,at,F,st,U,G,Dt,X,I,Et,nt,Z=n[0].name+"",it,Wt,rt,N,ct,M,dt,Lt,V,D,ut,Bt,ft,Ht,g,Yt,pt,bt,mt,qt,ht,_t,j,kt,E,St,Ft,vt,W,wt,It,yt,Nt,k,Vt,H,jt,Jt,Qt,gt,Kt,Pt,zt,Gt,Xt,Rt,Zt,$t,J,Ct,L,Q,A=[],xt=new Map,te,K,P=[],ee=new Map,Y;function le(t,a){if(t[1]&&t[2])return Me;if(t[1])return Ue;if(t[2])return Te}let q=le(n),R=q&&q(n);T=new Ae({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[6]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as _e,i as xe,s as Ue,e as Ve,f as ke,T as bO,g as Re,y as TO,o as we,K as je,L as ve,M as We}from"./index.5dab115f.js";import{P as Ge,N as Ce,u as Ye,D as ze,v as QO,T as Y,I as HO,w as cO,x as n,y as Ae,L as hO,z as uO,A as z,B as dO,F as Oe,G as pO,H as v,J as Ee,K as De,E as y,M as j,O as Ie,Q as Ne,R as m,U as Be,a as k,h as Je,b as Le,c as Me,d as Fe,e as Ke,s as He,f as Ot,g as et,i as tt,r as at,j as it,k as rt,l as st,m as lt,n as nt,o as ot,p as Qt,q as ct,t as XO,C as W}from"./index.e8a8986f.js";class N{constructor(O,t,a,i,r,s,l,Q,c,h=0,o){this.p=O,this.stack=t,this.state=a,this.reducePos=i,this.pos=r,this.score=s,this.buffer=l,this.bufferBase=Q,this.curContext=c,this.lookAhead=h,this.parent=o}toString(){return`[${this.stack.filter((O,t)=>t%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,t,a=0){let i=O.parser.context;return new N(O,[],t,a,a,0,[],0,i?new ZO(i,i.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,t){this.stack.push(this.state,t,this.bufferBase+this.buffer.length),this.state=O}reduce(O){let t=O>>19,a=O&65535,{parser:i}=this.p,r=i.dynamicPrecedence(a);if(r&&(this.score+=r),t==0){this.pushState(i.getGoto(this.state,a,!0),this.reducePos),a<i.minRepeatTerm&&this.storeNode(a,this.reducePos,this.reducePos,4,!0),this.reduceContext(a,this.reducePos);return}let s=this.stack.length-(t-1)*3-(O&262144?6:0),l=this.stack[s-2],Q=this.stack[s-1],c=this.bufferBase+this.buffer.length-Q;if(a<i.minRepeatTerm||O&131072){let h=i.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(a,l,h,c+4,!0)}if(O&262144)this.state=this.stack[s];else{let h=this.stack[s-3];this.state=i.getGoto(h,a,!0)}for(;this.stack.length>s;)this.stack.pop();this.reduceContext(a,l)}storeNode(O,t,a,i=4,r=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let s=this,l=this.buffer.length;if(l==0&&s.parent&&(l=s.bufferBase-s.parent.bufferBase,s=s.parent),l>0&&s.buffer[l-4]==0&&s.buffer[l-1]>-1){if(t==a)return;if(s.buffer[l-2]>=t){s.buffer[l-2]=a;return}}}if(!r||this.pos==a)this.buffer.push(O,t,a,i);else{let s=this.buffer.length;if(s>0&&this.buffer[s-4]!=0)for(;s>0&&this.buffer[s-2]>a;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,i>4&&(i-=4);this.buffer[s]=O,this.buffer[s+1]=t,this.buffer[s+2]=a,this.buffer[s+3]=i}}shift(O,t,a){let i=this.pos;if(O&131072)this.pushState(O&65535,this.pos);else if((O&262144)==0){let r=O,{parser:s}=this.p;(a>this.pos||t<=s.maxNode)&&(this.pos=a,s.stateFlag(r,1)||(this.reducePos=a)),this.pushState(r,i),this.shiftContext(t,i),t<=s.maxNode&&this.buffer.push(t,i,a,4)}else this.pos=a,this.shiftContext(t,i),t<=this.p.parser.maxNode&&this.buffer.push(t,i,a,4)}apply(O,t,a){O&65536?this.reduce(O):this.shift(O,t,a)}useNode(O,t){let a=this.p.reused.length-1;(a<0||this.p.reused[a]!=O)&&(this.p.reused.push(O),a++);let i=this.pos;this.reducePos=this.pos=i+O.length,this.pushState(t,i),this.buffer.push(a,i,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,t=O.buffer.length;for(;t>0&&O.buffer[t-2]>O.reducePos;)t-=4;let a=O.buffer.slice(t),i=O.bufferBase+t;for(;O&&i==O.bufferBase;)O=O.parent;return new N(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,a,i,this.curContext,this.lookAhead,O)}recoverByDelete(O,t){let a=O<=this.p.parser.maxNode;a&&this.storeNode(O,this.pos,t,4),this.storeNode(0,this.pos,t,a?8:4),this.pos=this.reducePos=t,this.score-=190}canShift(O){for(let t=new ht(this);;){let a=this.p.parser.stateSlot(t.state,4)||this.p.parser.hasAction(t.state,O);if(a==0)return!1;if((a&65536)==0)return!0;t.reduce(a)}}recoverByInsert(O){if(this.stack.length>=300)return[];let t=this.p.parser.nextStates(this.state);if(t.length>4<<1||this.stack.length>=120){let i=[];for(let r=0,s;r<t.length;r+=2)(s=t[r+1])!=this.state&&this.p.parser.hasAction(s,O)&&i.push(t[r],s);if(this.stack.length<120)for(let r=0;i.length<4<<1&&r<t.length;r+=2){let s=t[r+1];i.some((l,Q)=>Q&1&&l==s)||i.push(t[r],s)}t=i}let a=[];for(let i=0;i<t.length&&a.length<4;i+=2){let r=t[i+1];if(r==this.state)continue;let s=this.split();s.pushState(r,this.pos),s.storeNode(0,s.pos,s.pos,4,!0),s.shiftContext(t[i],this.pos),s.score-=200,a.push(s)}return a}forceReduce(){let O=this.p.parser.stateSlot(this.state,5);if((O&65536)==0)return!1;let{parser:t}=this.p;if(!t.validAction(this.state,O)){let a=O>>19,i=O&65535,r=this.stack.length-a*3;if(r<0||t.getGoto(this.stack[r],i,!1)<0)return!1;this.storeNode(0,this.reducePos,this.reducePos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(O),!0}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(this.stack.length!=3)return!1;let{paLine truncated
|
||||
import{S as _e,i as xe,s as Ue,e as Ve,f as ke,T as bO,g as Re,y as TO,o as we,K as je,L as ve,M as We}from"./index.536fddcb.js";import{P as Ge,N as Ce,u as Ye,D as ze,v as QO,T as Y,I as HO,w as cO,x as n,y as Ae,L as hO,z as uO,A as z,B as dO,F as Oe,G as pO,H as v,J as Ee,K as De,E as y,M as j,O as Ie,Q as Ne,R as m,U as Be,a as k,h as Je,b as Le,c as Me,d as Fe,e as Ke,s as He,f as Ot,g as et,i as tt,r as at,j as it,k as rt,l as st,m as lt,n as nt,o as ot,p as Qt,q as ct,t as XO,C as W}from"./index.e8a8986f.js";class N{constructor(O,t,a,i,r,s,l,Q,c,h=0,o){this.p=O,this.stack=t,this.state=a,this.reducePos=i,this.pos=r,this.score=s,this.buffer=l,this.bufferBase=Q,this.curContext=c,this.lookAhead=h,this.parent=o}toString(){return`[${this.stack.filter((O,t)=>t%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,t,a=0){let i=O.parser.context;return new N(O,[],t,a,a,0,[],0,i?new ZO(i,i.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,t){this.stack.push(this.state,t,this.bufferBase+this.buffer.length),this.state=O}reduce(O){let t=O>>19,a=O&65535,{parser:i}=this.p,r=i.dynamicPrecedence(a);if(r&&(this.score+=r),t==0){this.pushState(i.getGoto(this.state,a,!0),this.reducePos),a<i.minRepeatTerm&&this.storeNode(a,this.reducePos,this.reducePos,4,!0),this.reduceContext(a,this.reducePos);return}let s=this.stack.length-(t-1)*3-(O&262144?6:0),l=this.stack[s-2],Q=this.stack[s-1],c=this.bufferBase+this.buffer.length-Q;if(a<i.minRepeatTerm||O&131072){let h=i.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(a,l,h,c+4,!0)}if(O&262144)this.state=this.stack[s];else{let h=this.stack[s-3];this.state=i.getGoto(h,a,!0)}for(;this.stack.length>s;)this.stack.pop();this.reduceContext(a,l)}storeNode(O,t,a,i=4,r=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let s=this,l=this.buffer.length;if(l==0&&s.parent&&(l=s.bufferBase-s.parent.bufferBase,s=s.parent),l>0&&s.buffer[l-4]==0&&s.buffer[l-1]>-1){if(t==a)return;if(s.buffer[l-2]>=t){s.buffer[l-2]=a;return}}}if(!r||this.pos==a)this.buffer.push(O,t,a,i);else{let s=this.buffer.length;if(s>0&&this.buffer[s-4]!=0)for(;s>0&&this.buffer[s-2]>a;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,i>4&&(i-=4);this.buffer[s]=O,this.buffer[s+1]=t,this.buffer[s+2]=a,this.buffer[s+3]=i}}shift(O,t,a){let i=this.pos;if(O&131072)this.pushState(O&65535,this.pos);else if((O&262144)==0){let r=O,{parser:s}=this.p;(a>this.pos||t<=s.maxNode)&&(this.pos=a,s.stateFlag(r,1)||(this.reducePos=a)),this.pushState(r,i),this.shiftContext(t,i),t<=s.maxNode&&this.buffer.push(t,i,a,4)}else this.pos=a,this.shiftContext(t,i),t<=this.p.parser.maxNode&&this.buffer.push(t,i,a,4)}apply(O,t,a){O&65536?this.reduce(O):this.shift(O,t,a)}useNode(O,t){let a=this.p.reused.length-1;(a<0||this.p.reused[a]!=O)&&(this.p.reused.push(O),a++);let i=this.pos;this.reducePos=this.pos=i+O.length,this.pushState(t,i),this.buffer.push(a,i,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,t=O.buffer.length;for(;t>0&&O.buffer[t-2]>O.reducePos;)t-=4;let a=O.buffer.slice(t),i=O.bufferBase+t;for(;O&&i==O.bufferBase;)O=O.parent;return new N(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,a,i,this.curContext,this.lookAhead,O)}recoverByDelete(O,t){let a=O<=this.p.parser.maxNode;a&&this.storeNode(O,this.pos,t,4),this.storeNode(0,this.pos,t,a?8:4),this.pos=this.reducePos=t,this.score-=190}canShift(O){for(let t=new ht(this);;){let a=this.p.parser.stateSlot(t.state,4)||this.p.parser.hasAction(t.state,O);if(a==0)return!1;if((a&65536)==0)return!0;t.reduce(a)}}recoverByInsert(O){if(this.stack.length>=300)return[];let t=this.p.parser.nextStates(this.state);if(t.length>4<<1||this.stack.length>=120){let i=[];for(let r=0,s;r<t.length;r+=2)(s=t[r+1])!=this.state&&this.p.parser.hasAction(s,O)&&i.push(t[r],s);if(this.stack.length<120)for(let r=0;i.length<4<<1&&r<t.length;r+=2){let s=t[r+1];i.some((l,Q)=>Q&1&&l==s)||i.push(t[r],s)}t=i}let a=[];for(let i=0;i<t.length&&a.length<4;i+=2){let r=t[i+1];if(r==this.state)continue;let s=this.split();s.pushState(r,this.pos),s.storeNode(0,s.pos,s.pos,4,!0),s.shiftContext(t[i],this.pos),s.score-=200,a.push(s)}return a}forceReduce(){let O=this.p.parser.stateSlot(this.state,5);if((O&65536)==0)return!1;let{parser:t}=this.p;if(!t.validAction(this.state,O)){let a=O>>19,i=O&65535,r=this.stack.length-a*3;if(r<0||t.getGoto(this.stack[r],i,!1)<0)return!1;this.storeNode(0,this.reducePos,this.reducePos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(O),!0}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(this.stack.length!=3)return!1;let{paLine truncated
|
||||
\${}
|
||||
} catch (\${error}) {
|
||||
\${}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Ce,i as $e,s as we,e as c,w as v,b as h,c as he,f as b,g as r,h as n,m as ve,x as Y,O as pe,P as Pe,k as Se,Q as Oe,n as Re,t as Z,a as x,o as f,d as ge,R as Te,C as Ee,p as ye,r as j,u as Be,N as qe}from"./index.5dab115f.js";import{S as Ae}from"./SdkTabs.25abcc83.js";function ue(o,l,s){const a=o.slice();return a[5]=l[s],a}function be(o,l,s){const a=o.slice();return a[5]=l[s],a}function _e(o,l){let s,a=l[5].code+"",_,u,i,d;function p(){return l[4](l[5])}return{key:o,first:null,c(){s=c("button"),_=v(a),u=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(C,$){r(C,s,$),n(s,_),n(s,u),i||(d=Be(s,"click",p),i=!0)},p(C,$){l=C,$&4&&a!==(a=l[5].code+"")&&Y(_,a),$&6&&j(s,"active",l[1]===l[5].code)},d(C){C&&f(s),i=!1,d()}}}function ke(o,l){let s,a,_,u;return a=new qe({props:{content:l[5].body}}),{key:o,first:null,c(){s=c("div"),he(a.$$.fragment),_=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(i,d){r(i,s,d),ve(a,s,null),n(s,_),u=!0},p(i,d){l=i;const p={};d&4&&(p.content=l[5].body),a.$set(p),(!u||d&6)&&j(s,"active",l[1]===l[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&f(s),ge(a)}}}function Ue(o){var re,fe;let l,s,a=o[0].name+"",_,u,i,d,p,C,$,D=o[0].name+"",H,ee,I,w,F,R,L,P,N,te,K,T,le,Q,M=o[0].name+"",z,se,G,E,J,y,V,B,X,S,q,g=[],ae=new Map,oe,A,k=[],ne=new Map,O;w=new Ae({props:{js:`
|
||||
import{S as Ce,i as $e,s as we,e as c,w as v,b as h,c as he,f as b,g as r,h as n,m as ve,x as Y,O as pe,P as Pe,k as Se,Q as Oe,n as Re,t as Z,a as x,o as f,d as ge,R as Te,C as Ee,p as ye,r as j,u as Be,N as qe}from"./index.536fddcb.js";import{S as Ae}from"./SdkTabs.3571c26b.js";function ue(o,l,s){const a=o.slice();return a[5]=l[s],a}function be(o,l,s){const a=o.slice();return a[5]=l[s],a}function _e(o,l){let s,a=l[5].code+"",_,u,i,d;function p(){return l[4](l[5])}return{key:o,first:null,c(){s=c("button"),_=v(a),u=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(C,$){r(C,s,$),n(s,_),n(s,u),i||(d=Be(s,"click",p),i=!0)},p(C,$){l=C,$&4&&a!==(a=l[5].code+"")&&Y(_,a),$&6&&j(s,"active",l[1]===l[5].code)},d(C){C&&f(s),i=!1,d()}}}function ke(o,l){let s,a,_,u;return a=new qe({props:{content:l[5].body}}),{key:o,first:null,c(){s=c("div"),he(a.$$.fragment),_=h(),b(s,"class","tab-item"),j(s,"active",l[1]===l[5].code),this.first=s},m(i,d){r(i,s,d),ve(a,s,null),n(s,_),u=!0},p(i,d){l=i;const p={};d&4&&(p.content=l[5].body),a.$set(p),(!u||d&6)&&j(s,"active",l[1]===l[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&f(s),ge(a)}}}function Ue(o){var re,fe;let l,s,a=o[0].name+"",_,u,i,d,p,C,$,D=o[0].name+"",H,ee,I,w,F,R,L,P,N,te,K,T,le,Q,M=o[0].name+"",z,se,G,E,J,y,V,B,X,S,q,g=[],ae=new Map,oe,A,k=[],ne=new Map,O;w=new Ae({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Se,i as he,s as Re,e as c,w,b as v,c as ve,f as b,g as r,h as n,m as we,x as K,O as me,P as Oe,k as Ne,Q as Ce,n as We,t as Z,a as x,o as d,d as Pe,R as $e,C as Ee,p as Te,r as U,u as ge,N as Ae}from"./index.5dab115f.js";import{S as De}from"./SdkTabs.25abcc83.js";function ue(o,s,l){const a=o.slice();return a[5]=s[l],a}function be(o,s,l){const a=o.slice();return a[5]=s[l],a}function _e(o,s){let l,a=s[5].code+"",_,u,i,p;function m(){return s[4](s[5])}return{key:o,first:null,c(){l=c("button"),_=w(a),u=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(S,h){r(S,l,h),n(l,_),n(l,u),i||(p=ge(l,"click",m),i=!0)},p(S,h){s=S,h&4&&a!==(a=s[5].code+"")&&K(_,a),h&6&&U(l,"active",s[1]===s[5].code)},d(S){S&&d(l),i=!1,p()}}}function ke(o,s){let l,a,_,u;return a=new Ae({props:{content:s[5].body}}),{key:o,first:null,c(){l=c("div"),ve(a.$$.fragment),_=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(i,p){r(i,l,p),we(a,l,null),n(l,_),u=!0},p(i,p){s=i;const m={};p&4&&(m.content=s[5].body),a.$set(m),(!u||p&6)&&U(l,"active",s[1]===s[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&d(l),Pe(a)}}}function ye(o){var re,de;let s,l,a=o[0].name+"",_,u,i,p,m,S,h,q=o[0].name+"",j,ee,H,R,L,W,Q,O,B,te,M,$,se,z,I=o[0].name+"",G,le,J,E,V,T,X,g,Y,N,A,P=[],ae=new Map,oe,D,k=[],ne=new Map,C;R=new De({props:{js:`
|
||||
import{S as Se,i as he,s as Re,e as c,w,b as v,c as ve,f as b,g as r,h as n,m as we,x as K,O as me,P as Oe,k as Ne,Q as Ce,n as We,t as Z,a as x,o as d,d as Pe,R as $e,C as Ee,p as Te,r as U,u as ge,N as Ae}from"./index.536fddcb.js";import{S as De}from"./SdkTabs.3571c26b.js";function ue(o,s,l){const a=o.slice();return a[5]=s[l],a}function be(o,s,l){const a=o.slice();return a[5]=s[l],a}function _e(o,s){let l,a=s[5].code+"",_,u,i,p;function m(){return s[4](s[5])}return{key:o,first:null,c(){l=c("button"),_=w(a),u=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(S,h){r(S,l,h),n(l,_),n(l,u),i||(p=ge(l,"click",m),i=!0)},p(S,h){s=S,h&4&&a!==(a=s[5].code+"")&&K(_,a),h&6&&U(l,"active",s[1]===s[5].code)},d(S){S&&d(l),i=!1,p()}}}function ke(o,s){let l,a,_,u;return a=new Ae({props:{content:s[5].body}}),{key:o,first:null,c(){l=c("div"),ve(a.$$.fragment),_=v(),b(l,"class","tab-item"),U(l,"active",s[1]===s[5].code),this.first=l},m(i,p){r(i,l,p),we(a,l,null),n(l,_),u=!0},p(i,p){s=i;const m={};p&4&&(m.content=s[5].body),a.$set(m),(!u||p&6)&&U(l,"active",s[1]===s[5].code)},i(i){u||(Z(a.$$.fragment,i),u=!0)},o(i){x(a.$$.fragment,i),u=!1},d(i){i&&d(l),Pe(a)}}}function ye(o){var re,de;let s,l,a=o[0].name+"",_,u,i,p,m,S,h,q=o[0].name+"",j,ee,H,R,L,W,Q,O,B,te,M,$,se,z,I=o[0].name+"",G,le,J,E,V,T,X,g,Y,N,A,P=[],ae=new Map,oe,D,k=[],ne=new Map,C;R=new De({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as we,i as Ce,s as Pe,e as c,w as h,b as v,c as ve,f as b,g as r,h as n,m as he,x as D,O as de,P as Te,k as ge,Q as ye,n as Be,t as Z,a as x,o as f,d as $e,R as qe,C as Oe,p as Se,r as H,u as Ee,N as Ne}from"./index.5dab115f.js";import{S as Ve}from"./SdkTabs.25abcc83.js";function ue(i,l,s){const o=i.slice();return o[5]=l[s],o}function be(i,l,s){const o=i.slice();return o[5]=l[s],o}function _e(i,l){let s,o=l[5].code+"",_,u,a,p;function d(){return l[4](l[5])}return{key:i,first:null,c(){s=c("button"),_=h(o),u=v(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(w,C){r(w,s,C),n(s,_),n(s,u),a||(p=Ee(s,"click",d),a=!0)},p(w,C){l=w,C&4&&o!==(o=l[5].code+"")&&D(_,o),C&6&&H(s,"active",l[1]===l[5].code)},d(w){w&&f(s),a=!1,p()}}}function ke(i,l){let s,o,_,u;return o=new Ne({props:{content:l[5].body}}),{key:i,first:null,c(){s=c("div"),ve(o.$$.fragment),_=v(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(a,p){r(a,s,p),he(o,s,null),n(s,_),u=!0},p(a,p){l=a;const d={};p&4&&(d.content=l[5].body),o.$set(d),(!u||p&6)&&H(s,"active",l[1]===l[5].code)},i(a){u||(Z(o.$$.fragment,a),u=!0)},o(a){x(o.$$.fragment,a),u=!1},d(a){a&&f(s),$e(o)}}}function Ke(i){var re,fe;let l,s,o=i[0].name+"",_,u,a,p,d,w,C,M=i[0].name+"",I,ee,F,P,L,B,Q,T,A,te,R,q,le,z,U=i[0].name+"",G,se,J,O,W,S,X,E,Y,g,N,$=[],oe=new Map,ie,V,k=[],ne=new Map,y;P=new Ve({props:{js:`
|
||||
import{S as we,i as Ce,s as Pe,e as c,w as h,b as v,c as ve,f as b,g as r,h as n,m as he,x as D,O as de,P as Te,k as ge,Q as ye,n as Be,t as Z,a as x,o as f,d as $e,R as qe,C as Oe,p as Se,r as H,u as Ee,N as Ne}from"./index.536fddcb.js";import{S as Ve}from"./SdkTabs.3571c26b.js";function ue(i,l,s){const o=i.slice();return o[5]=l[s],o}function be(i,l,s){const o=i.slice();return o[5]=l[s],o}function _e(i,l){let s,o=l[5].code+"",_,u,a,p;function d(){return l[4](l[5])}return{key:i,first:null,c(){s=c("button"),_=h(o),u=v(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(w,C){r(w,s,C),n(s,_),n(s,u),a||(p=Ee(s,"click",d),a=!0)},p(w,C){l=w,C&4&&o!==(o=l[5].code+"")&&D(_,o),C&6&&H(s,"active",l[1]===l[5].code)},d(w){w&&f(s),a=!1,p()}}}function ke(i,l){let s,o,_,u;return o=new Ne({props:{content:l[5].body}}),{key:i,first:null,c(){s=c("div"),ve(o.$$.fragment),_=v(),b(s,"class","tab-item"),H(s,"active",l[1]===l[5].code),this.first=s},m(a,p){r(a,s,p),he(o,s,null),n(s,_),u=!0},p(a,p){l=a;const d={};p&4&&(d.content=l[5].body),o.$set(d),(!u||p&6)&&H(s,"active",l[1]===l[5].code)},i(a){u||(Z(o.$$.fragment,a),u=!0)},o(a){x(o.$$.fragment,a),u=!1},d(a){a&&f(s),$e(o)}}}function Ke(i){var re,fe;let l,s,o=i[0].name+"",_,u,a,p,d,w,C,M=i[0].name+"",I,ee,F,P,L,B,Q,T,A,te,R,q,le,z,U=i[0].name+"",G,se,J,O,W,S,X,E,Y,g,N,$=[],oe=new Map,ie,V,k=[],ne=new Map,y;P=new Ve({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i[3]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Ht,i as Lt,s as Pt,C as Q,N as At,e as a,w as k,b as m,c as Pe,f as h,g as r,h as n,m as Re,x,O as Le,P as ht,k as Rt,Q as Bt,n as Ft,t as fe,a as pe,o as d,d as Be,R as gt,p as jt,r as ue,u as Dt,y as le}from"./index.5dab115f.js";import{S as Nt}from"./SdkTabs.25abcc83.js";function wt(o,e,l){const s=o.slice();return s[7]=e[l],s}function Ct(o,e,l){const s=o.slice();return s[7]=e[l],s}function St(o,e,l){const s=o.slice();return s[12]=e[l],s}function $t(o){let e;return{c(){e=a("p"),e.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",h(e,"class","txt-hint txt-sm txt-right")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Tt(o){let e,l,s,b,p,c,f,y,T,w,O,g,D,V,L,I,j,B,S,N,q,C,_;function M(u,$){var ee,K;return(K=(ee=u[0])==null?void 0:ee.options)!=null&&K.requireEmail?It:Vt}let z=M(o),P=z(o);return{c(){e=a("tr"),e.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=a("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
import{S as Ht,i as Lt,s as Pt,C as Q,N as At,e as a,w as k,b as m,c as Pe,f as h,g as r,h as n,m as Re,x,O as Le,P as ht,k as Rt,Q as Bt,n as Ft,t as fe,a as pe,o as d,d as Be,R as gt,p as jt,r as ue,u as Dt,y as le}from"./index.536fddcb.js";import{S as Nt}from"./SdkTabs.3571c26b.js";function wt(o,e,l){const s=o.slice();return s[7]=e[l],s}function Ct(o,e,l){const s=o.slice();return s[7]=e[l],s}function St(o,e,l){const s=o.slice();return s[12]=e[l],s}function $t(o){let e;return{c(){e=a("p"),e.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",h(e,"class","txt-hint txt-sm txt-right")},m(l,s){r(l,e,s)},d(l){l&&d(e)}}}function Tt(o){let e,l,s,b,p,c,f,y,T,w,O,g,D,V,L,I,j,B,S,N,q,C,_;function M(u,$){var ee,K;return(K=(ee=u[0])==null?void 0:ee.options)!=null&&K.requireEmail?It:Vt}let z=M(o),P=z(o);return{c(){e=a("tr"),e.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=a("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
|
||||
<span>username</span></div></td>
|
||||
<td><span class="label">String</span></td>
|
||||
<td>The username of the auth record.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Ce,i as Re,s as Pe,e as c,w as D,b as k,c as $e,f as m,g as d,h as n,m as we,x,O as _e,P as Ee,k as Oe,Q as Te,n as Be,t as ee,a as te,o as f,d as ge,R as Ie,C as Ae,p as Me,r as N,u as Se,N as qe}from"./index.5dab115f.js";import{S as He}from"./SdkTabs.25abcc83.js";function ke(o,l,s){const a=o.slice();return a[6]=l[s],a}function he(o,l,s){const a=o.slice();return a[6]=l[s],a}function ve(o){let l;return{c(){l=c("p"),l.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",m(l,"class","txt-hint txt-sm txt-right")},m(s,a){d(s,l,a)},d(s){s&&f(l)}}}function ye(o,l){let s,a=l[6].code+"",h,i,r,u;function $(){return l[5](l[6])}return{key:o,first:null,c(){s=c("button"),h=D(a),i=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(b,g){d(b,s,g),n(s,h),n(s,i),r||(u=Se(s,"click",$),r=!0)},p(b,g){l=b,g&20&&N(s,"active",l[2]===l[6].code)},d(b){b&&f(s),r=!1,u()}}}function De(o,l){let s,a,h,i;return a=new qe({props:{content:l[6].body}}),{key:o,first:null,c(){s=c("div"),$e(a.$$.fragment),h=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(r,u){d(r,s,u),we(a,s,null),n(s,h),i=!0},p(r,u){l=r,(!i||u&20)&&N(s,"active",l[2]===l[6].code)},i(r){i||(ee(a.$$.fragment,r),i=!0)},o(r){te(a.$$.fragment,r),i=!1},d(r){r&&f(s),ge(a)}}}function Le(o){var ue,pe;let l,s,a=o[0].name+"",h,i,r,u,$,b,g,q=o[0].name+"",z,le,F,C,K,O,Q,y,H,se,L,E,oe,G,U=o[0].name+"",J,ae,V,ne,W,T,X,B,Y,I,Z,R,A,w=[],ie=new Map,re,M,v=[],ce=new Map,P;C=new He({props:{js:`
|
||||
import{S as Ce,i as Re,s as Pe,e as c,w as D,b as k,c as $e,f as m,g as d,h as n,m as we,x,O as _e,P as Ee,k as Oe,Q as Te,n as Be,t as ee,a as te,o as f,d as ge,R as Ie,C as Ae,p as Me,r as N,u as Se,N as qe}from"./index.536fddcb.js";import{S as He}from"./SdkTabs.3571c26b.js";function ke(o,l,s){const a=o.slice();return a[6]=l[s],a}function he(o,l,s){const a=o.slice();return a[6]=l[s],a}function ve(o){let l;return{c(){l=c("p"),l.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",m(l,"class","txt-hint txt-sm txt-right")},m(s,a){d(s,l,a)},d(s){s&&f(l)}}}function ye(o,l){let s,a=l[6].code+"",h,i,r,u;function $(){return l[5](l[6])}return{key:o,first:null,c(){s=c("button"),h=D(a),i=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(b,g){d(b,s,g),n(s,h),n(s,i),r||(u=Se(s,"click",$),r=!0)},p(b,g){l=b,g&20&&N(s,"active",l[2]===l[6].code)},d(b){b&&f(s),r=!1,u()}}}function De(o,l){let s,a,h,i;return a=new qe({props:{content:l[6].body}}),{key:o,first:null,c(){s=c("div"),$e(a.$$.fragment),h=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(r,u){d(r,s,u),we(a,s,null),n(s,h),i=!0},p(r,u){l=r,(!i||u&20)&&N(s,"active",l[2]===l[6].code)},i(r){i||(ee(a.$$.fragment,r),i=!0)},o(r){te(a.$$.fragment,r),i=!1},d(r){r&&f(s),ge(a)}}}function Le(o){var ue,pe;let l,s,a=o[0].name+"",h,i,r,u,$,b,g,q=o[0].name+"",z,le,F,C,K,O,Q,y,H,se,L,E,oe,G,U=o[0].name+"",J,ae,V,ne,W,T,X,B,Y,I,Z,R,A,w=[],ie=new Map,re,M,v=[],ce=new Map,P;C=new He({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
@@ -1 +0,0 @@
|
||||
import{S as ie,i as re,s as se,e as oe,f as ae,g as ue,y as _,o as le,I as ce,J as fe,K as de,L as he,C as L,M as ge}from"./index.5dab115f.js";import{C as I,E as x,a as C,h as pe,b as ye,c as me,d as be,e as ke,s as Ke,f as xe,g as Ce,i as qe,r as we,j as Se,k as Le,l as Ie,m as Ee,n as Re,o as Ae,p as ve,q as Be,t as z,S as _e}from"./index.e8a8986f.js";function Me(e){Q(e,"start");var i={},t=e.languageData||{},h=!1;for(var g in e)if(g!=t&&e.hasOwnProperty(g))for(var d=i[g]=[],o=e[g],s=0;s<o.length;s++){var u=o[s];d.push(new He(u,e)),(u.indent||u.dedent)&&(h=!0)}return{name:t.name,startState:function(){return{state:"start",pending:null,indent:h?[]:null}},copyState:function(a){var y={state:a.state,pending:a.pending,indent:a.indent&&a.indent.slice(0)};return a.stack&&(y.stack=a.stack.slice(0)),y},token:Fe(i),indent:Te(i,t),languageData:t}}function Q(e,i){if(!e.hasOwnProperty(i))throw new Error("Undefined state "+i+" in simple mode")}function Oe(e,i){if(!e)return/(?:)/;var t="";return e instanceof RegExp?(e.ignoreCase&&(t="i"),e=e.source):e=String(e),new RegExp((i===!1?"":"^")+"(?:"+e+")",t)}function De(e){if(!e)return null;if(e.apply)return e;if(typeof e=="string")return e.replace(/\./g," ");for(var i=[],t=0;t<e.length;t++)i.push(e[t]&&e[t].replace(/\./g," "));return i}function He(e,i){(e.next||e.push)&&Q(i,e.next||e.push),this.regex=Oe(e.regex),this.token=De(e.token),this.data=e}function Fe(e){return function(i,t){if(t.pending){var h=t.pending.shift();return t.pending.length==0&&(t.pending=null),i.pos+=h.text.length,h.token}for(var g=e[t.state],d=0;d<g.length;d++){var o=g[d],s=(!o.data.sol||i.sol())&&i.match(o.regex);if(s){o.data.next?t.state=o.data.next:o.data.push?((t.stack||(t.stack=[])).push(t.state),t.state=o.data.push):o.data.pop&&t.stack&&t.stack.length&&(t.state=t.stack.pop()),o.data.indent&&t.indent.push(i.indentation()+i.indentUnit),o.data.dedent&&t.indent.pop();var u=o.token;if(u&&u.apply&&(u=u(s)),s.length>2&&o.token&&typeof o.token!="string"){t.pending=[];for(var a=2;a<s.length;a++)s[a]&&t.pending.push({text:s[a],token:o.token[a-1]});return i.backUp(s[0].length-(s[1]?s[1].length:0)),u[0]}else return u&&u.join?u[0]:u}}return i.next(),null}}function Te(e,i){return function(t,h){if(t.indent==null||i.dontIndentStates&&i.doneIndentState.indexOf(t.state)>-1)return null;var g=t.indent.length-1,d=e[t.state];e:for(;;){for(var o=0;o<d.length;o++){var s=d[o];if(s.data.dedent&&s.data.dedentIfLineStart!==!1){var u=s.regex.exec(h);if(u&&u[0]){g--,(s.next||s.push)&&(d=e[s.next||s.push]),h=h.slice(u[0].length);continue e}}}break}return g<0?0:t.indent[g]}}function Ue(e){let i;return{c(){i=oe("div"),ae(i,"class","code-editor")},m(t,h){ue(t,i,h),e[15](i)},p:_,i:_,o:_,d(t){t&&le(i),e[15](null)}}}function We(e){return JSON.stringify([e==null?void 0:e.name,e==null?void 0:e.type,e==null?void 0:e.schema])}function Ne(e,i,t){let h;ce(e,fe,n=>t(21,h=n));const g=de();let{id:d=""}=i,{value:o=""}=i,{disabled:s=!1}=i,{placeholder:u=""}=i,{baseCollection:a=null}=i,{singleLine:y=!1}=i,{extraAutocompleteKeys:E=[]}=i,{disableRequestKeys:k=!1}=i,{disableIndirectCollectionsKeys:K=!1}=i,f,m,R=s,M=new I,O=new I,D=new I,H=new I,q=[],F=[],T=[],U=[],w="",A="";function v(){f==null||f.focus()}let B=null;function X(){clearTimeout(B),B=setTimeout(()=>{q=Y(h),U=Z(),F=k?[]:j(),T=K?[]:$()},300)}function Y(n){let r=n.slice();return a&&L.pushOrReplaceByKey(r,a,"id"),r}function W(){m==null||m.dispatchEvent(new CustomEvent("change",{detail:{value:o},bubbles:!0}))}function N(){if(!d)return;const n=document.querySelectorAll('[for="'+d+'"]');for(let r of n)r.removeEventListener("click",v)}function V(){if(!d)return;N();const n=document.querySelectorAll('[for="'+d+'"]');for(let r of n)r.addEventListener("click",v)}function S(n,r="",l=0){let p=q.find(b=>b.name==n||b.id==n);if(!p||l>=4)return[];let c=[r+"id",r+"created",r+"updated"];p.isAuth&&(c.push(r+"username"),c.push(r+"email"),c.push(r+"emailVisibility"),c.push(r+"verified"));for(const b of p.schema){const P=r+b.name;if(c.push(P),b.type==="relation"&&b.options.collectionId){const G=S(b.options.collectionId,P+".",l+1);G.length&&(c=c.concat(G))}}return c}function Z(){return S(a==null?void 0:a.name)}function j(){const n=[];n.push("@request.method"),n.push("@request.query."),n.push("@request.data."),n.push("@request.auth."),n.push("@request.auth.id"),n.push("@request.auth.collectionId"),n.push("@request.auth.collectionName"),n.push("@request.auth.verified"),n.push("@request.auth.username"),n.push("@request.auth.email"),n.push("@request.auth.emailVisibility"),n.push("@request.auth.created"),n.push("@request.auth.updated");const r=q.filter(l=>l.isAuth);for(const l of r){const p=S(l.id,"@request.auth.");for(const c of p)L.pushUnique(n,c)}return n}function $(){const n=[];for(const r of q){const l="@collection."+r.name+".",p=S(r.name,l);for(const c of p)n.push(c)}return n}function ee(n=!0,r=!0){let l=[].concat(E);return l=l.concat(U||[]),n&&(l=l.concat(F||[])),r&&(l=l.concat(T||[])),l.sort(function(p,c){retLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
import{S as oe,i as ae,s as ue,e as le,f as ce,g as fe,y as M,o as de,I as he,J as ge,K as pe,L as ye,C as I,M as me}from"./index.536fddcb.js";import{C as R,E as S,a as q,h as be,b as ke,c as xe,d as Ke,e as Ce,s as Se,f as qe,g as we,i as Le,r as Ee,j as Ie,k as Re,l as Ae,m as ve,n as Be,o as Oe,p as _e,q as Me,t as Y,S as De}from"./index.e8a8986f.js";function He(e){Z(e,"start");var i={},n=e.languageData||{},g=!1;for(var p in e)if(p!=n&&e.hasOwnProperty(p))for(var d=i[p]=[],o=e[p],r=0;r<o.length;r++){var l=o[r];d.push(new Ue(l,e)),(l.indent||l.dedent)&&(g=!0)}return{name:n.name,startState:function(){return{state:"start",pending:null,indent:g?[]:null}},copyState:function(a){var b={state:a.state,pending:a.pending,indent:a.indent&&a.indent.slice(0)};return a.stack&&(b.stack=a.stack.slice(0)),b},token:We(i),indent:Ne(i,n),languageData:n}}function Z(e,i){if(!e.hasOwnProperty(i))throw new Error("Undefined state "+i+" in simple mode")}function Fe(e,i){if(!e)return/(?:)/;var n="";return e instanceof RegExp?(e.ignoreCase&&(n="i"),e=e.source):e=String(e),new RegExp((i===!1?"":"^")+"(?:"+e+")",n)}function Te(e){if(!e)return null;if(e.apply)return e;if(typeof e=="string")return e.replace(/\./g," ");for(var i=[],n=0;n<e.length;n++)i.push(e[n]&&e[n].replace(/\./g," "));return i}function Ue(e,i){(e.next||e.push)&&Z(i,e.next||e.push),this.regex=Fe(e.regex),this.token=Te(e.token),this.data=e}function We(e){return function(i,n){if(n.pending){var g=n.pending.shift();return n.pending.length==0&&(n.pending=null),i.pos+=g.text.length,g.token}for(var p=e[n.state],d=0;d<p.length;d++){var o=p[d],r=(!o.data.sol||i.sol())&&i.match(o.regex);if(r){o.data.next?n.state=o.data.next:o.data.push?((n.stack||(n.stack=[])).push(n.state),n.state=o.data.push):o.data.pop&&n.stack&&n.stack.length&&(n.state=n.stack.pop()),o.data.indent&&n.indent.push(i.indentation()+i.indentUnit),o.data.dedent&&n.indent.pop();var l=o.token;if(l&&l.apply&&(l=l(r)),r.length>2&&o.token&&typeof o.token!="string"){n.pending=[];for(var a=2;a<r.length;a++)r[a]&&n.pending.push({text:r[a],token:o.token[a-1]});return i.backUp(r[0].length-(r[1]?r[1].length:0)),l[0]}else return l&&l.join?l[0]:l}}return i.next(),null}}function Ne(e,i){return function(n,g){if(n.indent==null||i.dontIndentStates&&i.doneIndentState.indexOf(n.state)>-1)return null;var p=n.indent.length-1,d=e[n.state];e:for(;;){for(var o=0;o<d.length;o++){var r=d[o];if(r.data.dedent&&r.data.dedentIfLineStart!==!1){var l=r.regex.exec(g);if(l&&l[0]){p--,(r.next||r.push)&&(d=e[r.next||r.push]),g=g.slice(l[0].length);continue e}}}break}return p<0?0:n.indent[p]}}function Ve(e){let i;return{c(){i=le("div"),ce(i,"class","code-editor")},m(n,g){fe(n,i,g),e[15](i)},p:M,i:M,o:M,d(n){n&&de(i),e[15](null)}}}function Je(e){return JSON.stringify([e==null?void 0:e.name,e==null?void 0:e.type,e==null?void 0:e.schema])}function Pe(e,i,n){let g;he(e,ge,t=>n(21,g=t));const p=pe();let{id:d=""}=i,{value:o=""}=i,{disabled:r=!1}=i,{placeholder:l=""}=i,{baseCollection:a=null}=i,{singleLine:b=!1}=i,{extraAutocompleteKeys:A=[]}=i,{disableRequestKeys:x=!1}=i,{disableIndirectCollectionsKeys:K=!1}=i,f,k,v=r,D=new R,H=new R,F=new R,T=new R,w=[],U=[],W=[],N=[],L="",B="";function O(){f==null||f.focus()}let _=null;function j(){clearTimeout(_),_=setTimeout(()=>{w=$(g),N=ee(),U=x?[]:te(),W=K?[]:ne()},300)}function $(t){let s=t.slice();return a&&I.pushOrReplaceByKey(s,a,"id"),s}function V(){k==null||k.dispatchEvent(new CustomEvent("change",{detail:{value:o},bubbles:!0}))}function J(){if(!d)return;const t=document.querySelectorAll('[for="'+d+'"]');for(let s of t)s.removeEventListener("click",O)}function P(){if(!d)return;J();const t=document.querySelectorAll('[for="'+d+'"]');for(let s of t)s.addEventListener("click",O)}function C(t,s="",c=0){var m,z,Q;let h=w.find(y=>y.name==t||y.id==t);if(!h||c>=4)return[];let u=[s+"id",s+"created",s+"updated"];h.isAuth&&(u.push(s+"username"),u.push(s+"email"),u.push(s+"emailVisibility"),u.push(s+"verified"));for(const y of h.schema){const E=s+y.name;if(u.push(E),y.type==="relation"&&((m=y.options)==null?void 0:m.collectionId)){const X=C(y.options.collectionId,E+".",c+1);X.length&&(u=u.concat(X))}y.type==="select"&&((z=y.options)==null?void 0:z.maxSelect)!=1&&u.push(E+":each"),((Q=y.options)==null?void 0:Q.maxSelect)!=1&&["select","file","relation"].includes(y.type)&&u.push(E+":length")}return u}function ee(){return C(a==null?void 0:a.name)}function te(){const t=[];t.push("@request.method"),t.push("@request.query."),t.push("@request.data."),t.push("@request.auth.id"),t.push("@request.auth.collectionId"),t.push("@request.auth.collectionName"),t.push("@request.auth.verified"),t.push("@request.auth.username"),t.push("@request.auth.email"),t.push("@request.auth.emailVisibility"),t.push("@request.auth.created"),t.push("@request.auth.updated");const s=w.filter(h=>h.isAuth);for(const h of s){const u=C(h.id,"@request.auth.");for(const m of u)I.pushUnique(t,m)}const c=["created","updated"];if(a!=null&&a.id){const h=C(a.name,"@rLine truncated
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
import{S as Et,i as Nt,s as Ht,e as l,b as a,E as qt,f as d,g as p,u as Mt,y as xt,o as u,w as k,h as e,N as Ae,c as ge,m as ye,x as Ue,O as Lt,P as Dt,k as It,Q as Bt,n as zt,t as ce,a as de,d as ve,R as Gt,C as je,p as Ut,r as Ee}from"./index.5dab115f.js";import{S as jt}from"./SdkTabs.25abcc83.js";function Qt(r){let s,n,i;return{c(){s=l("span"),s.textContent="Show details",n=a(),i=l("i"),d(s,"class","txt"),d(i,"class","ri-arrow-down-s-line")},m(c,f){p(c,s,f),p(c,n,f),p(c,i,f)},d(c){c&&u(s),c&&u(n),c&&u(i)}}}function Jt(r){let s,n,i;return{c(){s=l("span"),s.textContent="Hide details",n=a(),i=l("i"),d(s,"class","txt"),d(i,"class","ri-arrow-up-s-line")},m(c,f){p(c,s,f),p(c,n,f),p(c,i,f)},d(c){c&&u(s),c&&u(n),c&&u(i)}}}function Tt(r){let s,n,i,c,f,m,_,w,b,$,h,H,W,fe,T,pe,O,G,C,M,Fe,A,E,Ce,U,X,q,Y,xe,j,Q,D,P,ue,Z,v,I,ee,me,te,N,B,le,be,se,x,J,ne,Le,K,he,V;return{c(){s=l("p"),s.innerHTML=`The syntax basically follows the format
|
||||
<code><span class="txt-success">OPERAND</span>
|
||||
<span class="txt-danger">OPERATOR</span>
|
||||
<span class="txt-success">OPERAND</span></code>, where:`,n=a(),i=l("ul"),c=l("li"),c.innerHTML=`<code class="txt-success">OPERAND</code> - could be any of the above field literal, string (single
|
||||
or double quoted), number, null, true, false`,f=a(),m=l("li"),_=l("code"),_.textContent="OPERATOR",w=k(` - is one of:
|
||||
`),b=l("br"),$=a(),h=l("ul"),H=l("li"),W=l("code"),W.textContent="=",fe=a(),T=l("span"),T.textContent="Equal",pe=a(),O=l("li"),G=l("code"),G.textContent="!=",C=a(),M=l("span"),M.textContent="NOT equal",Fe=a(),A=l("li"),E=l("code"),E.textContent=">",Ce=a(),U=l("span"),U.textContent="Greater than",X=a(),q=l("li"),Y=l("code"),Y.textContent=">=",xe=a(),j=l("span"),j.textContent="Greater than or equal",Q=a(),D=l("li"),P=l("code"),P.textContent="<",ue=a(),Z=l("span"),Z.textContent="Less than or equal",v=a(),I=l("li"),ee=l("code"),ee.textContent="<=",me=a(),te=l("span"),te.textContent="Less than or equal",N=a(),B=l("li"),le=l("code"),le.textContent="~",be=a(),se=l("span"),se.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)`,x=a(),J=l("li"),ne=l("code"),ne.textContent="!~",Le=a(),K=l("span"),K.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)`,he=a(),V=l("p"),V.innerHTML=`To group and combine several expressions you could use brackets
|
||||
<code>(...)</code>, <code>&&</code> (AND) and <code>||</code> (OR) tokens.`,d(_,"class","txt-danger"),d(W,"class","filter-op svelte-1w7s5nw"),d(T,"class","txt-hint"),d(G,"class","filter-op svelte-1w7s5nw"),d(M,"class","txt-hint"),d(E,"class","filter-op svelte-1w7s5nw"),d(U,"class","txt-hint"),d(Y,"class","filter-op svelte-1w7s5nw"),d(j,"class","txt-hint"),d(P,"class","filter-op svelte-1w7s5nw"),d(Z,"class","txt-hint"),d(ee,"class","filter-op svelte-1w7s5nw"),d(te,"class","txt-hint"),d(le,"class","filter-op svelte-1w7s5nw"),d(se,"class","txt-hint"),d(ne,"class","filter-op svelte-1w7s5nw"),d(K,"class","txt-hint")},m(F,R){p(F,s,R),p(F,n,R),p(F,i,R),e(i,c),e(i,f),e(i,m),e(m,_),e(m,w),e(m,b),e(m,$),e(m,h),e(h,H),e(H,W),e(H,fe),e(H,T),e(h,pe),e(h,O),e(O,G),e(O,C),e(O,M),e(h,Fe),e(h,A),e(A,E),e(A,Ce),e(A,U),e(h,X),e(h,q),e(q,Y),e(q,xe),e(q,j),e(h,Q),e(h,D),e(D,P),e(D,ue),e(D,Z),e(h,v),e(h,I),e(I,ee),e(I,me),e(I,te),e(h,N),e(h,B),e(B,le),e(B,be),e(B,se),e(h,x),e(h,J),e(J,ne),e(J,Le),e(J,K),p(F,he,R),p(F,V,R)},d(F){F&&u(s),F&&u(n),F&&u(i),F&&u(he),F&&u(V)}}}function Kt(r){let s,n,i,c,f;function m($,h){return $[0]?Jt:Qt}let _=m(r),w=_(r),b=r[0]&&Tt();return{c(){s=l("button"),w.c(),n=a(),b&&b.c(),i=qt(),d(s,"class","btn btn-sm btn-secondary m-t-5")},m($,h){p($,s,h),w.m(s,null),p($,n,h),b&&b.m($,h),p($,i,h),c||(f=Mt(s,"click",r[1]),c=!0)},p($,[h]){_!==(_=m($))&&(w.d(1),w=_($),w&&(w.c(),w.m(s,null))),$[0]?b||(b=Tt(),b.c(),b.m(i.parentNode,i)):b&&(b.d(1),b=null)},i:xt,o:xt,d($){$&&u(s),w.d(),$&&u(n),b&&b.d($),$&&u(i),c=!1,f()}}}function Vt(r,s,n){let i=!1;function c(){n(0,i=!i)}return[i,c]}class Wt extends Et{constructor(s){super(),Nt(this,s,Vt,Kt,Ht,{})}}function Pt(r,s,n){const i=r.slice();return i[6]=s[n],i}function Rt(r,s,n){const i=r.slice();return i[6]=s[n],i}function St(r){let s;return{c(){s=l("p"),s.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",d(s,"class","txt-hint txt-sm txt-right")},m(n,i){p(n,s,i)},d(n){n&&u(s)}}}function Ot(r,s){let n,i=s[6].code+"",c,f,m,_;function w(){return s[5](s[6])}return{key:r,first:null,c(){n=l("div"),c=k(i),f=a(),d(n,"class","tab-item"),Ee(n,"active",s[2]===s[6].code),this.first=n},m(b,$){p(b,n,$),e(n,c),e(n,f),m||(_=Mt(n,"click",w),m=!0)},p(b,$){s=b,$&20&&Ee(n,"active",s[2]===s[6].code)},d(b){b&&u(n),m=!1,_()}}}function At(r,s){let n,i,c,f;return i=new Ae({props:{content:s[6].body}}),{key:r,first:null,c(){n=l("div"),ge(i.$$.fragment),c=a(),d(n,"class","tab-item"),Ee(n,"active",s[2]===s[6].code),this.first=n},m(m,_){p(m,n,_),ye(i,n,null),e(n,c),f=!0},p(m,_){s=m,(!f||_&20)&&Ee(n,"active",s[2]===s[6].code)},i(m){f||(ce(i.$$.fragment,m),f=!0)},o(m){de(i.$$.fragment,m),f=!1},d(m){m&&u(n),ve(i)}}}function Xt(r){var mt,bt,ht,_t,$t,kt;let s,n,i=r[0].name+"",c,f,m,_,w,b,$,h=r[0].name+"",H,W,fe,T,pe,O,G,C,M,Fe,A,E,Ce,U,X=r[0].name+"",q,Y,xe,j,Q,D,P,ue,Z,v,I,ee,me,te,N,B,le,be,se,x,J,ne,Le,K,he,V,F,R,Qe,ie,Ne,Je,He,Ke,_e,Ve,$e,We,ke,Xe,oe,Me,Ye,qe,Ze,y,et,we,tt,lt,st,De,nt,Ie,it,ot,at,Be,rt,ze,Te,Ge,ae,Pe,z=[],ct=new Map,dt,Re,S=[],ft=new Map,re;T=new jt({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await pb.collection('${(mt=r[0])==null?void 0:mt.name}').getList(1, 50, {
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2',
|
||||
});
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
const records = await pb.collection('${(bt=r[0])==null?void 0:bt.name}').getFullList(200 /* batch size */, {
|
||||
sort: '-created',
|
||||
});
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
const record = await pb.collection('${(ht=r[0])==null?void 0:ht.name}').getFirstListItem('someField="test"', {
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
});
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
final resultList = await pb.collection('${(_t=r[0])==null?void 0:_t.name}').getList(
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2',
|
||||
);
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
final records = await pb.collection('${($t=r[0])==null?void 0:$t.name}').getFullList(
|
||||
batch: 200,
|
||||
sort: '-created',
|
||||
);
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
final record = await pb.collection('${(kt=r[0])==null?void 0:kt.name}').getFirstListItem(
|
||||
'someField="test"',
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
);
|
||||
`}});let L=r[1]&&St();R=new Ae({props:{content:`
|
||||
// DESC by created and ASC by id
|
||||
?sort=-created,id
|
||||
`}}),$e=new Ae({props:{content:`
|
||||
?filter=(id='abc' && created>'2022-01-01')
|
||||
`}}),ke=new Wt({}),we=new Ae({props:{content:"?expand=relField1,relField2.subRelField"}});let Oe=r[4];const pt=t=>t[6].code;for(let t=0;t<Oe.length;t+=1){let o=Rt(r,Oe,t),g=pt(o);ct.set(g,z[t]=Ot(g,o))}let Se=r[4];const ut=t=>t[6].code;for(let t=0;t<Se.length;t+=1){let o=Pt(r,Se,t),g=ut(o);ft.set(g,S[t]=At(g,o))}return{c(){s=l("h3"),n=k("List/Search ("),c=k(i),f=k(")"),m=a(),_=l("div"),w=l("p"),b=k("Fetch a paginated "),$=l("strong"),H=k(h),W=k(" records list, supporting sorting and filtering."),fe=a(),ge(T.$$.fragment),pe=a(),O=l("h6"),O.textContent="API details",G=a(),C=l("div"),M=l("strong"),M.textContent="GET",Fe=a(),A=l("div"),E=l("p"),Ce=k("/api/collections/"),U=l("strong"),q=k(X),Y=k("/records"),xe=a(),L&&L.c(),j=a(),Q=l("div"),Q.textContent="Query parameters",D=a(),P=l("table"),ue=l("thead"),ue.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,Z=a(),v=l("tbody"),I=l("tr"),I.innerHTML=`<td>page</td>
|
||||
<td><span class="label">Number</span></td>
|
||||
<td>The page (aka. offset) of the paginated list (default to 1).</td>`,ee=a(),me=l("tr"),me.innerHTML=`<td>perPage</td>
|
||||
<td><span class="label">Number</span></td>
|
||||
<td>Specify the max returned records per page (default to 30).</td>`,te=a(),N=l("tr"),B=l("td"),B.textContent="sort",le=a(),be=l("td"),be.innerHTML='<span class="label">String</span>',se=a(),x=l("td"),J=k("Specify the records order attribute(s). "),ne=l("br"),Le=k(`
|
||||
Add `),K=l("code"),K.textContent="-",he=k(" / "),V=l("code"),V.textContent="+",F=k(` (default) in front of the attribute for DESC / ASC order.
|
||||
Ex.:
|
||||
`),ge(R.$$.fragment),Qe=a(),ie=l("tr"),Ne=l("td"),Ne.textContent="filter",Je=a(),He=l("td"),He.innerHTML='<span class="label">String</span>',Ke=a(),_e=l("td"),Ve=k(`Filter the returned records. Ex.:
|
||||
`),ge($e.$$.fragment),We=a(),ge(ke.$$.fragment),Xe=a(),oe=l("tr"),Me=l("td"),Me.textContent="expand",Ye=a(),qe=l("td"),qe.innerHTML='<span class="label">String</span>',Ze=a(),y=l("td"),et=k(`Auto expand record relations. Ex.:
|
||||
`),ge(we.$$.fragment),tt=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),lt=l("br"),st=k(`
|
||||
The expanded relations will be appended to each individual record under the
|
||||
`),De=l("code"),De.textContent="expand",nt=k(" property (eg. "),Ie=l("code"),Ie.textContent='"expand": {"relField1": {...}, ...}',it=k(`).
|
||||
`),ot=l("br"),at=k(`
|
||||
Only the relations to which the request user has permissions to `),Be=l("strong"),Be.textContent="view",rt=k(" will be expanded."),ze=a(),Te=l("div"),Te.textContent="Responses",Ge=a(),ae=l("div"),Pe=l("div");for(let t=0;t<z.length;t+=1)z[t].c();dt=a(),Re=l("div");for(let t=0;t<S.length;t+=1)S[t].c();d(s,"class","m-b-sm"),d(_,"class","content txt-lg m-b-sm"),d(O,"class","m-b-xs"),d(M,"class","label label-primary"),d(A,"class","content"),d(C,"class","alert alert-info"),d(Q,"class","section-title"),d(P,"class","table-compact table-border m-b-base"),d(Te,"class","section-title"),d(Pe,"class","tabs-header compact left"),d(Re,"class","tabs-content"),d(ae,"class","tabs")},m(t,o){p(t,s,o),e(s,n),e(s,c),e(s,f),p(t,m,o),p(t,_,o),e(_,w),e(w,b),e(w,$),e($,H),e(w,W),p(t,fe,o),ye(T,t,o),p(t,pe,o),p(t,O,o),p(t,G,o),p(t,C,o),e(C,M),e(C,Fe),e(C,A),e(A,E),e(E,Ce),e(E,U),e(U,q),e(E,Y),e(C,xe),L&&L.m(C,null),p(t,j,o),p(t,Q,o),p(t,D,o),p(t,P,o),e(P,ue),e(P,Z),e(P,v),e(v,I),e(v,ee),e(v,me),e(v,te),e(v,N),e(N,B),e(N,le),e(N,be),e(N,se),e(N,x),e(x,J),e(x,ne),e(x,Le),e(x,K),e(x,he),e(x,V),e(x,F),ye(R,x,null),e(v,Qe),e(v,ie),e(ie,Ne),e(ie,Je),e(ie,He),e(ie,Ke),e(ie,_e),e(_e,Ve),ye($e,_e,null),e(_e,We),ye(ke,_e,null),e(v,Xe),e(v,oe),e(oe,Me),e(oe,Ye),e(oe,qe),e(oe,Ze),e(oe,y),e(y,et),ye(we,y,null),e(y,tt),e(y,lt),e(y,st),e(y,De),e(y,nt),e(y,Ie),e(y,it),e(y,ot),e(y,at),e(y,Be),e(y,rt),p(t,ze,o),p(t,Te,o),p(t,Ge,o),p(t,ae,o),e(ae,Pe);for(let g=0;g<z.length;g+=1)z[g].m(Pe,null);e(ae,dt),e(ae,Re);for(let g=0;g<S.length;g+=1)S[g].m(Re,null);re=!0},p(t,[o]){var wt,gt,yt,vt,Ft,Ct;(!re||o&1)&&i!==(i=t[0].name+"")&&Ue(c,i),(!re||o&1)&&h!==(h=t[0].name+"")&&Ue(H,h);const g={};o&9&&(g.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${t[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await pb.collection('${(wt=t[0])==null?void 0:wt.name}').getList(1, 50, {
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2',
|
||||
});
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
const records = await pb.collection('${(gt=t[0])==null?void 0:gt.name}').getFullList(200 /* batch size */, {
|
||||
sort: '-created',
|
||||
});
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
const record = await pb.collection('${(yt=t[0])==null?void 0:yt.name}').getFirstListItem('someField="test"', {
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
});
|
||||
`),o&9&&(g.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${t[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
final resultList = await pb.collection('${(vt=t[0])==null?void 0:vt.name}').getList(
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2',
|
||||
);
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
final records = await pb.collection('${(Ft=t[0])==null?void 0:Ft.name}').getFullList(
|
||||
batch: 200,
|
||||
sort: '-created',
|
||||
);
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
final record = await pb.collection('${(Ct=t[0])==null?void 0:Ct.name}').getFirstListItem(
|
||||
'someField="test"',
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
);
|
||||
`),T.$set(g),(!re||o&1)&&X!==(X=t[0].name+"")&&Ue(q,X),t[1]?L||(L=St(),L.c(),L.m(C,null)):L&&(L.d(1),L=null),o&20&&(Oe=t[4],z=Lt(z,o,pt,1,t,Oe,ct,Pe,Dt,Ot,null,Rt)),o&20&&(Se=t[4],It(),S=Lt(S,o,ut,1,t,Se,ft,Re,Bt,At,null,Pt),zt())},i(t){if(!re){ce(T.$$.fragment,t),ce(R.$$.fragment,t),ce($e.$$.fragment,t),ce(ke.$$.fragment,t),ce(we.$$.fragment,t);for(let o=0;o<Se.length;o+=1)ce(S[o]);re=!0}},o(t){de(T.$$.fragment,t),de(R.$$.fragment,t),de($e.$$.fragment,t),de(ke.$$.fragment,t),de(we.$$.fragment,t);for(let o=0;o<S.length;o+=1)de(S[o]);re=!1},d(t){t&&u(s),t&&u(m),t&&u(_),t&&u(fe),ve(T,t),t&&u(pe),t&&u(O),t&&u(G),t&&u(C),L&&L.d(),t&&u(j),t&&u(Q),t&&u(D),t&&u(P),ve(R),ve($e),ve(ke),ve(we),t&&u(ze),t&&u(Te),t&&u(Ge),t&&u(ae);for(let o=0;o<z.length;o+=1)z[o].d();for(let o=0;o<S.length;o+=1)S[o].d()}}}function Yt(r,s,n){let i,c,{collection:f=new Gt}=s,m=200,_=[];const w=b=>n(2,m=b.code);return r.$$set=b=>{"collection"in b&&n(0,f=b.collection)},r.$$.update=()=>{r.$$.dirty&1&&n(1,i=(f==null?void 0:f.listRule)===null),r.$$.dirty&3&&f!=null&&f.id&&(_.push({code:200,body:JSON.stringify({page:1,perPage:30,totalPages:1,totalItems:2,items:[je.dummyCollectionRecord(f),je.dummyCollectionRecord(f)]},null,2)}),_.push({code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Something went wrong while processing your request. Invalid filter.",
|
||||
"data": {}
|
||||
}
|
||||
`}),i&&_.push({code:403,body:`
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Only admins can access this action.",
|
||||
"data": {}
|
||||
}
|
||||
`}))},n(3,c=je.getApiExampleUrl(Ut.baseUrl)),[f,i,m,c,_,w]}class tl extends Et{constructor(s){super(),Nt(this,s,Yt,Xt,Ht,{collection:0})}}export{tl as default};
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o as m,w as _,h as t,N as he,c as Yt,m as Zt,x as we,O as Le,P as Me,k as Ie,Q as Be,n as Ge,t as It,a as Bt,d as te,R as ze,C as _e,p as Ue,r as xe}from"./index.536fddcb.js";import{S as je}from"./SdkTabs.3571c26b.js";function Qe(d){let n,a,r;return{c(){n=e("span"),n.textContent="Show details",a=s(),r=e("i"),o(n,"class","txt"),o(r,"class","ri-arrow-down-s-line")},m(f,p){u(f,n,p),u(f,a,p),u(f,r,p)},d(f){f&&m(n),f&&m(a),f&&m(r)}}}function Je(d){let n,a,r;return{c(){n=e("span"),n.textContent="Hide details",a=s(),r=e("i"),o(n,"class","txt"),o(r,"class","ri-arrow-up-s-line")},m(f,p){u(f,n,p),u(f,a,p),u(f,r,p)},d(f){f&&m(n),f&&m(a),f&&m(r)}}}function Ae(d){let n,a,r,f,p,b,x,$,h,w,c,V,bt,Gt,R,zt,q,it,F,W,ee,B,G,le,at,ht,X,xt,se,rt,ct,Y,O,Ut,wt,v,Z,_t,jt,$t,z,tt,Ct,Qt,kt,L,dt,gt,ne,ft,oe,D,yt,et,vt,U,pt,ie,H,Ft,lt,Lt,st,At,nt,j,E,Jt,Tt,Kt,Pt,C,Q,M,ae,Rt,re,ut,ce,I,Ot,de,Et,Vt,St,Wt,A,mt,J,K,S,Nt,fe,T,k,pe,N,y,ot,ue,P,qt,me,Dt,be,Ht,Xt,Mt;return{c(){n=e("p"),n.innerHTML=`The syntax basically follows the format
|
||||
<code><span class="txt-success">OPERAND</span>
|
||||
<span class="txt-danger">OPERATOR</span>
|
||||
<span class="txt-success">OPERAND</span></code>, where:`,a=s(),r=e("ul"),f=e("li"),f.innerHTML=`<code class="txt-success">OPERAND</code> - could be any of the above field literal, string (single
|
||||
or double quoted), number, null, true, false`,p=s(),b=e("li"),x=e("code"),x.textContent="OPERATOR",$=_(` - is one of:
|
||||
`),h=e("br"),w=s(),c=e("ul"),V=e("li"),bt=e("code"),bt.textContent="=",Gt=s(),R=e("span"),R.textContent="Equal",zt=s(),q=e("li"),it=e("code"),it.textContent="!=",F=s(),W=e("span"),W.textContent="NOT equal",ee=s(),B=e("li"),G=e("code"),G.textContent=">",le=s(),at=e("span"),at.textContent="Greater than",ht=s(),X=e("li"),xt=e("code"),xt.textContent=">=",se=s(),rt=e("span"),rt.textContent="Greater than or equal",ct=s(),Y=e("li"),O=e("code"),O.textContent="<",Ut=s(),wt=e("span"),wt.textContent="Less than or equal",v=s(),Z=e("li"),_t=e("code"),_t.textContent="<=",jt=s(),$t=e("span"),$t.textContent="Less than or equal",z=s(),tt=e("li"),Ct=e("code"),Ct.textContent="~",Qt=s(),kt=e("span"),kt.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)`,L=s(),dt=e("li"),gt=e("code"),gt.textContent="!~",ne=s(),ft=e("span"),ft.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)`,oe=s(),D=e("li"),yt=e("code"),yt.textContent="?=",et=s(),vt=e("em"),vt.textContent="Any/At least one of",U=s(),pt=e("span"),pt.textContent="Equal",ie=s(),H=e("li"),Ft=e("code"),Ft.textContent="?!=",lt=s(),Lt=e("em"),Lt.textContent="Any/At least one of",st=s(),At=e("span"),At.textContent="NOT equal",nt=s(),j=e("li"),E=e("code"),E.textContent="?>",Jt=s(),Tt=e("em"),Tt.textContent="Any/At least one of",Kt=s(),Pt=e("span"),Pt.textContent="Greater than",C=s(),Q=e("li"),M=e("code"),M.textContent="?>=",ae=s(),Rt=e("em"),Rt.textContent="Any/At least one of",re=s(),ut=e("span"),ut.textContent="Greater than or equal",ce=s(),I=e("li"),Ot=e("code"),Ot.textContent="?<",de=s(),Et=e("em"),Et.textContent="Any/At least one of",Vt=s(),St=e("span"),St.textContent="Less than or equal",Wt=s(),A=e("li"),mt=e("code"),mt.textContent="?<=",J=s(),K=e("em"),K.textContent="Any/At least one of",S=s(),Nt=e("span"),Nt.textContent="Less than or equal",fe=s(),T=e("li"),k=e("code"),k.textContent="?~",pe=s(),N=e("em"),N.textContent="Any/At least one of",y=s(),ot=e("span"),ot.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)`,ue=s(),P=e("li"),qt=e("code"),qt.textContent="?!~",me=s(),Dt=e("em"),Dt.textContent="Any/At least one of",be=s(),Ht=e("span"),Ht.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)`,Xt=s(),Mt=e("p"),Mt.innerHTML=`To group and combine several expressions you could use brackets
|
||||
<code>(...)</code>, <code>&&</code> (AND) and <code>||</code> (OR) tokens.`,o(x,"class","txt-danger"),o(bt,"class","filter-op svelte-1w7s5nw"),o(R,"class","txt"),o(it,"class","filter-op svelte-1w7s5nw"),o(W,"class","txt"),o(G,"class","filter-op svelte-1w7s5nw"),o(at,"class","txt"),o(xt,"class","filter-op svelte-1w7s5nw"),o(rt,"class","txt"),o(O,"class","filter-op svelte-1w7s5nw"),o(wt,"class","txt"),o(_t,"class","filter-op svelte-1w7s5nw"),o($t,"class","txt"),o(Ct,"class","filter-op svelte-1w7s5nw"),o(kt,"class","txt"),o(gt,"class","filter-op svelte-1w7s5nw"),o(ft,"class","txt"),o(yt,"class","filter-op svelte-1w7s5nw"),o(vt,"class","txt-hint"),o(pt,"class","txt"),o(Ft,"class","filter-op svelte-1w7s5nw"),o(Lt,"class","txt-hint"),o(At,"class","txt"),o(E,"class","filter-op svelte-1w7s5nw"),o(Tt,"class","txt-hint"),o(Pt,"class","txt"),o(M,"class","filter-op svelte-1w7s5nw"),o(Rt,"class","txt-hint"),o(ut,"class","txt"),o(Ot,"class","filter-op svelte-1w7s5nw"),o(Et,"class","txt-hint"),o(St,"class","txt"),o(mt,"class","filter-op svelte-1w7s5nw"),o(K,"class","txt-hint"),o(Nt,"class","txt"),o(k,"class","filter-op svelte-1w7s5nw"),o(N,"class","txt-hint"),o(ot,"class","txt"),o(qt,"class","filter-op svelte-1w7s5nw"),o(Dt,"class","txt-hint"),o(Ht,"class","txt")},m(l,i){u(l,n,i),u(l,a,i),u(l,r,i),t(r,f),t(r,p),t(r,b),t(b,x),t(b,$),t(b,h),t(b,w),t(b,c),t(c,V),t(V,bt),t(V,Gt),t(V,R),t(c,zt),t(c,q),t(q,it),t(q,F),t(q,W),t(c,ee),t(c,B),t(B,G),t(B,le),t(B,at),t(c,ht),t(c,X),t(X,xt),t(X,se),t(X,rt),t(c,ct),t(c,Y),t(Y,O),t(Y,Ut),t(Y,wt),t(c,v),t(c,Z),t(Z,_t),t(Z,jt),t(Z,$t),t(c,z),t(c,tt),t(tt,Ct),t(tt,Qt),t(tt,kt),t(c,L),t(c,dt),t(dt,gt),t(dt,ne),t(dt,ft),t(c,oe),t(c,D),t(D,yt),t(D,et),t(D,vt),t(D,U),t(D,pt),t(c,ie),t(c,H),t(H,Ft),t(H,lt),t(H,Lt),t(H,st),t(H,At),t(c,nt),t(c,j),t(j,E),t(j,Jt),t(j,Tt),t(j,Kt),t(j,Pt),t(c,C),t(c,Q),t(Q,M),t(Q,ae),t(Q,Rt),t(Q,re),t(Q,ut),t(c,ce),t(c,I),t(I,Ot),t(I,de),t(I,Et),t(I,Vt),t(I,St),t(c,Wt),t(c,A),t(A,mt),t(A,J),t(A,K),t(A,S),t(A,Nt),t(c,fe),t(c,T),t(T,k),t(T,pe),t(T,N),t(T,y),t(T,ot),t(c,ue),t(c,P),t(P,qt),t(P,me),t(P,Dt),t(P,be),t(P,Ht),u(l,Xt,i),u(l,Mt,i)},d(l){l&&m(n),l&&m(a),l&&m(r),l&&m(Xt),l&&m(Mt)}}}function Ke(d){let n,a,r,f,p;function b(w,c){return w[0]?Je:Qe}let x=b(d),$=x(d),h=d[0]&&Ae();return{c(){n=e("button"),$.c(),a=s(),h&&h.c(),r=He(),o(n,"class","btn btn-sm btn-secondary m-t-5")},m(w,c){u(w,n,c),$.m(n,null),u(w,a,c),h&&h.m(w,c),u(w,r,c),f||(p=De(n,"click",d[1]),f=!0)},p(w,[c]){x!==(x=b(w))&&($.d(1),$=x(w),$&&($.c(),$.m(n,null))),w[0]?h||(h=Ae(),h.c(),h.m(r.parentNode,r)):h&&(h.d(1),h=null)},i:Fe,o:Fe,d(w){w&&m(n),$.d(),w&&m(a),h&&h.d(w),w&&m(r),f=!1,p()}}}function Ve(d,n,a){let r=!1;function f(){a(0,r=!r)}return[r,f]}class We extends Se{constructor(n){super(),Ne(this,n,Ve,Ke,qe,{})}}function Te(d,n,a){const r=d.slice();return r[6]=n[a],r}function Pe(d,n,a){const r=d.slice();return r[6]=n[a],r}function Re(d){let n;return{c(){n=e("p"),n.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",o(n,"class","txt-hint txt-sm txt-right")},m(a,r){u(a,n,r)},d(a){a&&m(n)}}}function Oe(d,n){let a,r=n[6].code+"",f,p,b,x;function $(){return n[5](n[6])}return{key:d,first:null,c(){a=e("div"),f=_(r),p=s(),o(a,"class","tab-item"),xe(a,"active",n[2]===n[6].code),this.first=a},m(h,w){u(h,a,w),t(a,f),t(a,p),b||(x=De(a,"click",$),b=!0)},p(h,w){n=h,w&20&&xe(a,"active",n[2]===n[6].code)},d(h){h&&m(a),b=!1,x()}}}function Ee(d,n){let a,r,f,p;return r=new he({props:{content:n[6].body}}),{key:d,first:null,c(){a=e("div"),Yt(r.$$.fragment),f=s(),o(a,"class","tab-item"),xe(a,"active",n[2]===n[6].code),this.first=a},m(b,x){u(b,a,x),Zt(r,a,null),t(a,f),p=!0},p(b,x){n=b,(!p||x&20)&&xe(a,"active",n[2]===n[6].code)},i(b){p||(It(r.$$.fragment,b),p=!0)},o(b){Bt(r.$$.fragment,b),p=!1},d(b){b&&m(a),te(r)}}}function Xe(d){var me,Dt,be,Ht,Xt,Mt;let n,a,r=d[0].name+"",f,p,b,x,$,h,w,c=d[0].name+"",V,bt,Gt,R,zt,q,it,F,W,ee,B,G,le,at,ht=d[0].name+"",X,xt,se,rt,ct,Y,O,Ut,wt,v,Z,_t,jt,$t,z,tt,Ct,Qt,kt,L,dt,gt,ne,ft,oe,D,yt,et,vt,U,pt,ie,H,Ft,lt,Lt,st,At,nt,j,E,Jt,Tt,Kt,Pt,C,Q,M,ae,Rt,re,ut,ce,I,Ot,de,Et,Vt,St,Wt,A,mt,J,K,S=[],Nt=new Map,fe,T,k=[],pe=new Map,N;R=new je({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${d[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await pb.collection('${(me=d[0])==null?void 0:me.name}').getList(1, 50, {
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
|
||||
});
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
const records = await pb.collection('${(Dt=d[0])==null?void 0:Dt.name}').getFullList(200 /* batch size */, {
|
||||
sort: '-created',
|
||||
});
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
const record = await pb.collection('${(be=d[0])==null?void 0:be.name}').getFirstListItem('someField="test"', {
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
});
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${d[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
final resultList = await pb.collection('${(Ht=d[0])==null?void 0:Ht.name}').getList(
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
|
||||
);
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
final records = await pb.collection('${(Xt=d[0])==null?void 0:Xt.name}').getFullList(
|
||||
batch: 200,
|
||||
sort: '-created',
|
||||
);
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
final record = await pb.collection('${(Mt=d[0])==null?void 0:Mt.name}').getFirstListItem(
|
||||
'someField="test"',
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
);
|
||||
`}});let y=d[1]&&Re();et=new he({props:{content:`
|
||||
// DESC by created and ASC by id
|
||||
?sort=-created,id
|
||||
`}}),st=new he({props:{content:`
|
||||
?filter=(id='abc' && created>'2022-01-01')
|
||||
`}}),nt=new We({}),M=new he({props:{content:"?expand=relField1,relField2.subRelField"}});let ot=d[4];const ue=l=>l[6].code;for(let l=0;l<ot.length;l+=1){let i=Pe(d,ot,l),g=ue(i);Nt.set(g,S[l]=Oe(g,i))}let P=d[4];const qt=l=>l[6].code;for(let l=0;l<P.length;l+=1){let i=Te(d,P,l),g=qt(i);pe.set(g,k[l]=Ee(g,i))}return{c(){n=e("h3"),a=_("List/Search ("),f=_(r),p=_(")"),b=s(),x=e("div"),$=e("p"),h=_("Fetch a paginated "),w=e("strong"),V=_(c),bt=_(" records list, supporting sorting and filtering."),Gt=s(),Yt(R.$$.fragment),zt=s(),q=e("h6"),q.textContent="API details",it=s(),F=e("div"),W=e("strong"),W.textContent="GET",ee=s(),B=e("div"),G=e("p"),le=_("/api/collections/"),at=e("strong"),X=_(ht),xt=_("/records"),se=s(),y&&y.c(),rt=s(),ct=e("div"),ct.textContent="Query parameters",Y=s(),O=e("table"),Ut=e("thead"),Ut.innerHTML=`<tr><th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th></tr>`,wt=s(),v=e("tbody"),Z=e("tr"),Z.innerHTML=`<td>page</td>
|
||||
<td><span class="label">Number</span></td>
|
||||
<td>The page (aka. offset) of the paginated list (default to 1).</td>`,_t=s(),jt=e("tr"),jt.innerHTML=`<td>perPage</td>
|
||||
<td><span class="label">Number</span></td>
|
||||
<td>Specify the max returned records per page (default to 30).</td>`,$t=s(),z=e("tr"),tt=e("td"),tt.textContent="sort",Ct=s(),Qt=e("td"),Qt.innerHTML='<span class="label">String</span>',kt=s(),L=e("td"),dt=_("Specify the records order attribute(s). "),gt=e("br"),ne=_(`
|
||||
Add `),ft=e("code"),ft.textContent="-",oe=_(" / "),D=e("code"),D.textContent="+",yt=_(` (default) in front of the attribute for DESC / ASC order.
|
||||
Ex.:
|
||||
`),Yt(et.$$.fragment),vt=s(),U=e("tr"),pt=e("td"),pt.textContent="filter",ie=s(),H=e("td"),H.innerHTML='<span class="label">String</span>',Ft=s(),lt=e("td"),Lt=_(`Filter the returned records. Ex.:
|
||||
`),Yt(st.$$.fragment),At=s(),Yt(nt.$$.fragment),j=s(),E=e("tr"),Jt=e("td"),Jt.textContent="expand",Tt=s(),Kt=e("td"),Kt.innerHTML='<span class="label">String</span>',Pt=s(),C=e("td"),Q=_(`Auto expand record relations. Ex.:
|
||||
`),Yt(M.$$.fragment),ae=_(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),Rt=e("br"),re=_(`
|
||||
The expanded relations will be appended to each individual record under the
|
||||
`),ut=e("code"),ut.textContent="expand",ce=_(" property (eg. "),I=e("code"),I.textContent='"expand": {"relField1": {...}, ...}',Ot=_(`).
|
||||
`),de=e("br"),Et=_(`
|
||||
Only the relations to which the request user has permissions to `),Vt=e("strong"),Vt.textContent="view",St=_(" will be expanded."),Wt=s(),A=e("div"),A.textContent="Responses",mt=s(),J=e("div"),K=e("div");for(let l=0;l<S.length;l+=1)S[l].c();fe=s(),T=e("div");for(let l=0;l<k.length;l+=1)k[l].c();o(n,"class","m-b-sm"),o(x,"class","content txt-lg m-b-sm"),o(q,"class","m-b-xs"),o(W,"class","label label-primary"),o(B,"class","content"),o(F,"class","alert alert-info"),o(ct,"class","section-title"),o(O,"class","table-compact table-border m-b-base"),o(A,"class","section-title"),o(K,"class","tabs-header compact left"),o(T,"class","tabs-content"),o(J,"class","tabs")},m(l,i){u(l,n,i),t(n,a),t(n,f),t(n,p),u(l,b,i),u(l,x,i),t(x,$),t($,h),t($,w),t(w,V),t($,bt),u(l,Gt,i),Zt(R,l,i),u(l,zt,i),u(l,q,i),u(l,it,i),u(l,F,i),t(F,W),t(F,ee),t(F,B),t(B,G),t(G,le),t(G,at),t(at,X),t(G,xt),t(F,se),y&&y.m(F,null),u(l,rt,i),u(l,ct,i),u(l,Y,i),u(l,O,i),t(O,Ut),t(O,wt),t(O,v),t(v,Z),t(v,_t),t(v,jt),t(v,$t),t(v,z),t(z,tt),t(z,Ct),t(z,Qt),t(z,kt),t(z,L),t(L,dt),t(L,gt),t(L,ne),t(L,ft),t(L,oe),t(L,D),t(L,yt),Zt(et,L,null),t(v,vt),t(v,U),t(U,pt),t(U,ie),t(U,H),t(U,Ft),t(U,lt),t(lt,Lt),Zt(st,lt,null),t(lt,At),Zt(nt,lt,null),t(v,j),t(v,E),t(E,Jt),t(E,Tt),t(E,Kt),t(E,Pt),t(E,C),t(C,Q),Zt(M,C,null),t(C,ae),t(C,Rt),t(C,re),t(C,ut),t(C,ce),t(C,I),t(C,Ot),t(C,de),t(C,Et),t(C,Vt),t(C,St),u(l,Wt,i),u(l,A,i),u(l,mt,i),u(l,J,i),t(J,K);for(let g=0;g<S.length;g+=1)S[g].m(K,null);t(J,fe),t(J,T);for(let g=0;g<k.length;g+=1)k[g].m(T,null);N=!0},p(l,[i]){var $e,Ce,ke,ge,ye,ve;(!N||i&1)&&r!==(r=l[0].name+"")&&we(f,r),(!N||i&1)&&c!==(c=l[0].name+"")&&we(V,c);const g={};i&9&&(g.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${l[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await pb.collection('${($e=l[0])==null?void 0:$e.name}').getList(1, 50, {
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
|
||||
});
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
const records = await pb.collection('${(Ce=l[0])==null?void 0:Ce.name}').getFullList(200 /* batch size */, {
|
||||
sort: '-created',
|
||||
});
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
const record = await pb.collection('${(ke=l[0])==null?void 0:ke.name}').getFirstListItem('someField="test"', {
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
});
|
||||
`),i&9&&(g.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${l[3]}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
final resultList = await pb.collection('${(ge=l[0])==null?void 0:ge.name}').getList(
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
|
||||
);
|
||||
|
||||
// you can also fetch all records at once via getFullList
|
||||
final records = await pb.collection('${(ye=l[0])==null?void 0:ye.name}').getFullList(
|
||||
batch: 200,
|
||||
sort: '-created',
|
||||
);
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
final record = await pb.collection('${(ve=l[0])==null?void 0:ve.name}').getFirstListItem(
|
||||
'someField="test"',
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
);
|
||||
`),R.$set(g),(!N||i&1)&&ht!==(ht=l[0].name+"")&&we(X,ht),l[1]?y||(y=Re(),y.c(),y.m(F,null)):y&&(y.d(1),y=null),i&20&&(ot=l[4],S=Le(S,i,ue,1,l,ot,Nt,K,Me,Oe,null,Pe)),i&20&&(P=l[4],Ie(),k=Le(k,i,qt,1,l,P,pe,T,Be,Ee,null,Te),Ge())},i(l){if(!N){It(R.$$.fragment,l),It(et.$$.fragment,l),It(st.$$.fragment,l),It(nt.$$.fragment,l),It(M.$$.fragment,l);for(let i=0;i<P.length;i+=1)It(k[i]);N=!0}},o(l){Bt(R.$$.fragment,l),Bt(et.$$.fragment,l),Bt(st.$$.fragment,l),Bt(nt.$$.fragment,l),Bt(M.$$.fragment,l);for(let i=0;i<k.length;i+=1)Bt(k[i]);N=!1},d(l){l&&m(n),l&&m(b),l&&m(x),l&&m(Gt),te(R,l),l&&m(zt),l&&m(q),l&&m(it),l&&m(F),y&&y.d(),l&&m(rt),l&&m(ct),l&&m(Y),l&&m(O),te(et),te(st),te(nt),te(M),l&&m(Wt),l&&m(A),l&&m(mt),l&&m(J);for(let i=0;i<S.length;i+=1)S[i].d();for(let i=0;i<k.length;i+=1)k[i].d()}}}function Ye(d,n,a){let r,f,{collection:p=new ze}=n,b=200,x=[];const $=h=>a(2,b=h.code);return d.$$set=h=>{"collection"in h&&a(0,p=h.collection)},d.$$.update=()=>{d.$$.dirty&1&&a(1,r=(p==null?void 0:p.listRule)===null),d.$$.dirty&3&&p!=null&&p.id&&(x.push({code:200,body:JSON.stringify({page:1,perPage:30,totalPages:1,totalItems:2,items:[_e.dummyCollectionRecord(p),_e.dummyCollectionRecord(p)]},null,2)}),x.push({code:400,body:`
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Something went wrong while processing your request. Invalid filter.",
|
||||
"data": {}
|
||||
}
|
||||
`}),r&&x.push({code:403,body:`
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Only admins can access this action.",
|
||||
"data": {}
|
||||
}
|
||||
`}))},a(3,f=_e.getApiExampleUrl(Ue.baseUrl)),[p,r,b,f,x,$]}class el extends Se{constructor(n){super(),Ne(this,n,Ye,Xe,qe,{collection:0})}}export{el as default};
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Be,i as qe,s as Oe,e as i,w as v,b as _,c as Ie,f as b,g as r,h as s,m as Se,x as U,O as Pe,P as Le,k as Me,Q as Re,n as We,t as te,a as le,o as d,d as Ee,R as ze,C as De,p as He,r as j,u as Ue,N as je}from"./index.5dab115f.js";import{S as Ne}from"./SdkTabs.25abcc83.js";function ye(a,l,o){const n=a.slice();return n[5]=l[o],n}function Ae(a,l,o){const n=a.slice();return n[5]=l[o],n}function Ce(a,l){let o,n=l[5].code+"",f,h,c,u;function m(){return l[4](l[5])}return{key:a,first:null,c(){o=i("button"),f=v(n),h=_(),b(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m(g,P){r(g,o,P),s(o,f),s(o,h),c||(u=Ue(o,"click",m),c=!0)},p(g,P){l=g,P&4&&n!==(n=l[5].code+"")&&U(f,n),P&6&&j(o,"active",l[1]===l[5].code)},d(g){g&&d(o),c=!1,u()}}}function Te(a,l){let o,n,f,h;return n=new je({props:{content:l[5].body}}),{key:a,first:null,c(){o=i("div"),Ie(n.$$.fragment),f=_(),b(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m(c,u){r(c,o,u),Se(n,o,null),s(o,f),h=!0},p(c,u){l=c;const m={};u&4&&(m.content=l[5].body),n.$set(m),(!h||u&6)&&j(o,"active",l[1]===l[5].code)},i(c){h||(te(n.$$.fragment,c),h=!0)},o(c){le(n.$$.fragment,c),h=!1},d(c){c&&d(o),Ee(n)}}}function Ge(a){var be,he,_e,ke;let l,o,n=a[0].name+"",f,h,c,u,m,g,P,M=a[0].name+"",N,oe,se,G,K,y,Q,I,F,w,R,ae,W,A,ne,J,z=a[0].name+"",V,ie,X,ce,re,D,Y,S,Z,E,x,B,ee,C,q,$=[],de=new Map,ue,O,k=[],pe=new Map,T;y=new Ne({props:{js:`
|
||||
import{S as Be,i as qe,s as Oe,e as i,w as v,b as _,c as Ie,f as b,g as r,h as s,m as Se,x as U,O as Pe,P as Le,k as Me,Q as Re,n as We,t as te,a as le,o as d,d as Ee,R as ze,C as De,p as He,r as j,u as Ue,N as je}from"./index.536fddcb.js";import{S as Ne}from"./SdkTabs.3571c26b.js";function ye(a,l,o){const n=a.slice();return n[5]=l[o],n}function Ae(a,l,o){const n=a.slice();return n[5]=l[o],n}function Ce(a,l){let o,n=l[5].code+"",f,h,c,u;function m(){return l[4](l[5])}return{key:a,first:null,c(){o=i("button"),f=v(n),h=_(),b(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m(g,P){r(g,o,P),s(o,f),s(o,h),c||(u=Ue(o,"click",m),c=!0)},p(g,P){l=g,P&4&&n!==(n=l[5].code+"")&&U(f,n),P&6&&j(o,"active",l[1]===l[5].code)},d(g){g&&d(o),c=!1,u()}}}function Te(a,l){let o,n,f,h;return n=new je({props:{content:l[5].body}}),{key:a,first:null,c(){o=i("div"),Ie(n.$$.fragment),f=_(),b(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m(c,u){r(c,o,u),Se(n,o,null),s(o,f),h=!0},p(c,u){l=c;const m={};u&4&&(m.content=l[5].body),n.$set(m),(!h||u&6)&&j(o,"active",l[1]===l[5].code)},i(c){h||(te(n.$$.fragment,c),h=!0)},o(c){le(n.$$.fragment,c),h=!1},d(c){c&&d(o),Ee(n)}}}function Ge(a){var be,he,_e,ke;let l,o,n=a[0].name+"",f,h,c,u,m,g,P,M=a[0].name+"",N,oe,se,G,K,y,Q,I,F,w,R,ae,W,A,ne,J,z=a[0].name+"",V,ie,X,ce,re,D,Y,S,Z,E,x,B,ee,C,q,$=[],de=new Map,ue,O,k=[],pe=new Map,T;y=new Ne({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[3]}');
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{S as E,i as G,s as I,F as K,c as A,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as C,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.5dab115f.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=C(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=C(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,P,v,k,F,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
|
||||
import{S as E,i as G,s as I,F as K,c as A,m as B,t as H,a as N,d as T,C as M,q as J,e as c,w as q,b as C,f as u,r as L,g as b,h as _,u as h,v as O,j as Q,l as U,o as w,A as V,p as W,B as X,D as Y,x as Z,z as S}from"./index.536fddcb.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=C(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=C(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,P,v,k,F,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
|
||||
`),m&&m.c(),t=C(),A(r.$$.fragment),p=C(),A(d.$$.fragment),n=C(),i=c("button"),g=c("span"),g.textContent="Set new password",R=C(),P=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],L(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(P,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),B(r,e,null),_(e,p),B(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,P,$),_(P,v),k=!0,F||(j=[h(e,"submit",O(f[4])),Q(U.call(null,v))],F=!0)},p(a,$){a[3]?m?m.p(a,$):(m=y(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const z={};$&769&&(z.$$scope={dirty:$,ctx:a}),r.$set(z);const D={};$&770&&(D.$$scope={dirty:$,ctx:a}),d.$set(D),(!k||$&4)&&(i.disabled=a[2]),(!k||$&4)&&L(i,"btn-loading",a[2])},i(a){k||(H(r.$$.fragment,a),H(d.$$.fragment,a),k=!0)},o(a){N(r.$$.fragment,a),N(d.$$.fragment,a),k=!1},d(a){a&&w(e),m&&m.d(),T(r),T(d),a&&w(R),a&&w(P),F=!1,V(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){A(e.$$.fragment)},m(s,l){B(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(H(e.$$.fragment,s),o=!0)},o(s){N(e.$$.fragment,s),o=!1},d(s){T(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await W.admins.confirmPasswordReset(l==null?void 0:l.token,t,r),X("Successfully set a new admin password."),Y("/")}catch(g){W.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{S as M,i as T,s as j,F as z,c as H,m as L,t as w,a as y,d as S,b as g,e as _,f as p,g as k,h as d,j as A,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as E,v as I,w as h,x as J,y as P,z as R}from"./index.5dab115f.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
|
||||
import{S as M,i as T,s as j,F as z,c as H,m as L,t as w,a as y,d as S,b as g,e as _,f as p,g as k,h as d,j as A,l as B,k as N,n as D,o as v,p as C,q as G,r as F,u as E,v as I,w as h,x as J,y as P,z as R}from"./index.536fddcb.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new G({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`<h4 class="m-b-xs">Forgotten admin password</h4>
|
||||
<p>Enter the email associated with your account and we\u2019ll send you a recovery link:</p>`,n=g(),H(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),L(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=E(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),(!a||$&2)&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),S(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=E(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=A(B.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(N(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new z({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){H(e.$$.fragment)},m(n,l){L(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){S(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends M{constructor(e){super(),T(this,e,W,V,j,{})}}export{Y as default};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as z,i as A,s as G,F as I,c as T,m as L,t as v,a as y,d as R,C as J,E as M,g as _,k as N,n as W,o as b,G as Y,H as j,p as B,q as D,e as m,w as C,b as h,f as d,r as P,h as k,u as q,v as K,y as E,x as O,z as F}from"./index.5dab115f.js";function Q(r){let e,t,s,l,n,o,c,i,a,u,g,$,p=r[3]&&S(r);return o=new D({props:{class:"form-field required",name:"password",$$slots:{default:[V,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),t=m("div"),s=m("h5"),l=C(`Type your password to confirm changing your email address
|
||||
import{S as z,i as A,s as G,F as I,c as T,m as L,t as v,a as y,d as R,C as J,E as M,g as _,k as N,n as W,o as b,G as Y,H as j,p as B,q as D,e as m,w as C,b as h,f as d,r as P,h as k,u as q,v as K,y as E,x as O,z as F}from"./index.536fddcb.js";function Q(r){let e,t,s,l,n,o,c,i,a,u,g,$,p=r[3]&&S(r);return o=new D({props:{class:"form-field required",name:"password",$$slots:{default:[V,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),t=m("div"),s=m("h5"),l=C(`Type your password to confirm changing your email address
|
||||
`),p&&p.c(),n=h(),T(o.$$.fragment),c=h(),i=m("button"),a=m("span"),a.textContent="Confirm new email",d(t,"class","content txt-center m-b-base"),d(a,"class","txt"),d(i,"type","submit"),d(i,"class","btn btn-lg btn-block"),i.disabled=r[1],P(i,"btn-loading",r[1])},m(f,w){_(f,e,w),k(e,t),k(t,s),k(s,l),p&&p.m(s,null),k(e,n),L(o,e,null),k(e,c),k(e,i),k(i,a),u=!0,g||($=q(e,"submit",K(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=S(f),p.c(),p.m(s,null)):p&&(p.d(1),p=null);const H={};w&769&&(H.$$scope={dirty:w,ctx:f}),o.$set(H),(!u||w&2)&&(i.disabled=f[1]),(!u||w&2)&&P(i,"btn-loading",f[1])},i(f){u||(v(o.$$.fragment,f),u=!0)},o(f){y(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),R(o),g=!1,$()}}}function U(r){let e,t,s,l,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Successfully changed the user email address.</p>
|
||||
<p>You can now sign in with your new email address.</p></div>`,t=h(),s=m("button"),s.textContent="Close",d(e,"class","alert alert-success"),d(s,"type","button"),d(s,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,t,c),_(o,s,c),l||(n=q(s,"click",r[6]),l=!0)},p:E,i:E,o:E,d(o){o&&b(e),o&&b(t),o&&b(s),l=!1,n()}}}function S(r){let e,t,s;return{c(){e=C("to "),t=m("strong"),s=C(r[3]),d(t,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,t,n),k(t,s)},p(l,n){n&8&&O(s,l[3])},d(l){l&&b(e),l&&b(t)}}}function V(r){let e,t,s,l,n,o,c,i;return{c(){e=m("label"),t=C("Password"),l=h(),n=m("input"),d(e,"for",s=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(a,u){_(a,e,u),k(e,t),_(a,l,u),_(a,n,u),F(n,r[0]),n.focus(),c||(i=q(n,"input",r[7]),c=!0)},p(a,u){u&256&&s!==(s=a[8])&&d(e,"for",s),u&256&&o!==(o=a[8])&&d(n,"id",o),u&1&&n.value!==a[0]&&F(n,a[0])},d(a){a&&b(e),a&&b(l),a&&b(n),c=!1,i()}}}function X(r){let e,t,s,l;const n=[U,Q],o=[];function c(i,a){return i[2]?0:1}return e=c(r),t=o[e]=n[e](r),{c(){t.c(),s=M()},m(i,a){o[e].m(i,a),_(i,s,a),l=!0},p(i,a){let u=e;e=c(i),e===u?o[e].p(i,a):(N(),y(o[u],1,1,()=>{o[u]=null}),W(),t=o[e],t?t.p(i,a):(t=o[e]=n[e](i),t.c()),v(t,1),t.m(s.parentNode,s))},i(i){l||(v(t),l=!0)},o(i){y(t),l=!1},d(i){o[e].d(i),i&&b(s)}}}function Z(r){let e,t;return e=new I({props:{nobranding:!0,$$slots:{default:[X]},$$scope:{ctx:r}}}),{c(){T(e.$$.fragment)},m(s,l){L(e,s,l),t=!0},p(s,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:s}),e.$set(n)},i(s){t||(v(e.$$.fragment,s),t=!0)},o(s){y(e.$$.fragment,s),t=!1},d(s){R(e,s)}}}function x(r,e,t){let s,{params:l}=e,n="",o=!1,c=!1;async function i(){if(o)return;t(1,o=!0);const g=new Y("../");try{const $=j(l==null?void 0:l.token);await g.collection($.collectionId).confirmEmailChange(l==null?void 0:l.token,n),t(2,c=!0)}catch($){B.errorResponseHandler($)}t(1,o=!1)}const a=()=>window.close();function u(){n=this.value,t(0,n)}return r.$$set=g=>{"params"in g&&t(5,l=g.params)},r.$$.update=()=>{r.$$.dirty&32&&t(3,s=J.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,s,i,l,a,u]}class te extends z{constructor(e){super(),A(this,e,x,Z,G,{params:5})}}export{te as default};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as I,i as J,s as M,F as W,c as F,m as N,t as P,a as q,d as L,C as Y,E as j,g as _,k as B,n as D,o as m,G as K,H as O,p as Q,q as A,e as b,w as h,b as y,f as p,r as E,h as w,u as H,v as U,y as S,x as V,z as R}from"./index.5dab115f.js";function X(r){let e,l,s,n,t,o,c,u,i,a,v,k,g,C,d=r[4]&&G(r);return o=new A({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),u=new A({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),{c(){e=b("form"),l=b("div"),s=b("h5"),n=h(`Reset your user password
|
||||
import{S as I,i as J,s as M,F as W,c as F,m as N,t as P,a as q,d as L,C as Y,E as j,g as _,k as B,n as D,o as m,G as K,H as O,p as Q,q as A,e as b,w as h,b as y,f as p,r as E,h as w,u as H,v as U,y as S,x as V,z as R}from"./index.536fddcb.js";function X(r){let e,l,s,n,t,o,c,u,i,a,v,k,g,C,d=r[4]&&G(r);return o=new A({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),u=new A({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),{c(){e=b("form"),l=b("div"),s=b("h5"),n=h(`Reset your user password
|
||||
`),d&&d.c(),t=y(),F(o.$$.fragment),c=y(),F(u.$$.fragment),i=y(),a=b("button"),v=b("span"),v.textContent="Set new password",p(l,"class","content txt-center m-b-base"),p(v,"class","txt"),p(a,"type","submit"),p(a,"class","btn btn-lg btn-block"),a.disabled=r[2],E(a,"btn-loading",r[2])},m(f,$){_(f,e,$),w(e,l),w(l,s),w(s,n),d&&d.m(s,null),w(e,t),N(o,e,null),w(e,c),N(u,e,null),w(e,i),w(e,a),w(a,v),k=!0,g||(C=H(e,"submit",U(r[5])),g=!0)},p(f,$){f[4]?d?d.p(f,$):(d=G(f),d.c(),d.m(s,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const z={};$&3074&&(z.$$scope={dirty:$,ctx:f}),u.$set(z),(!k||$&4)&&(a.disabled=f[2]),(!k||$&4)&&E(a,"btn-loading",f[2])},i(f){k||(P(o.$$.fragment,f),P(u.$$.fragment,f),k=!0)},o(f){q(o.$$.fragment,f),q(u.$$.fragment,f),k=!1},d(f){f&&m(e),d&&d.d(),L(o),L(u),g=!1,C()}}}function Z(r){let e,l,s,n,t;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Successfully changed the user password.</p>
|
||||
<p>You can now sign in with your new password.</p></div>`,l=y(),s=b("button"),s.textContent="Close",p(e,"class","alert alert-success"),p(s,"type","button"),p(s,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,l,c),_(o,s,c),n||(t=H(s,"click",r[7]),n=!0)},p:S,i:S,o:S,d(o){o&&m(e),o&&m(l),o&&m(s),n=!1,t()}}}function G(r){let e,l,s;return{c(){e=h("for "),l=b("strong"),s=h(r[4])},m(n,t){_(n,e,t),_(n,l,t),w(l,s)},p(n,t){t&16&&V(s,n[4])},d(n){n&&m(e),n&&m(l)}}}function x(r){let e,l,s,n,t,o,c,u;return{c(){e=b("label"),l=h("New password"),n=y(),t=b("input"),p(e,"for",s=r[10]),p(t,"type","password"),p(t,"id",o=r[10]),t.required=!0,t.autofocus=!0},m(i,a){_(i,e,a),w(e,l),_(i,n,a),_(i,t,a),R(t,r[0]),t.focus(),c||(u=H(t,"input",r[8]),c=!0)},p(i,a){a&1024&&s!==(s=i[10])&&p(e,"for",s),a&1024&&o!==(o=i[10])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&m(e),i&&m(n),i&&m(t),c=!1,u()}}}function ee(r){let e,l,s,n,t,o,c,u;return{c(){e=b("label"),l=h("New password confirm"),n=y(),t=b("input"),p(e,"for",s=r[10]),p(t,"type","password"),p(t,"id",o=r[10]),t.required=!0},m(i,a){_(i,e,a),w(e,l),_(i,n,a),_(i,t,a),R(t,r[1]),c||(u=H(t,"input",r[9]),c=!0)},p(i,a){a&1024&&s!==(s=i[10])&&p(e,"for",s),a&1024&&o!==(o=i[10])&&p(t,"id",o),a&2&&t.value!==i[1]&&R(t,i[1])},d(i){i&&m(e),i&&m(n),i&&m(t),c=!1,u()}}}function te(r){let e,l,s,n;const t=[Z,X],o=[];function c(u,i){return u[3]?0:1}return e=c(r),l=o[e]=t[e](r),{c(){l.c(),s=j()},m(u,i){o[e].m(u,i),_(u,s,i),n=!0},p(u,i){let a=e;e=c(u),e===a?o[e].p(u,i):(B(),q(o[a],1,1,()=>{o[a]=null}),D(),l=o[e],l?l.p(u,i):(l=o[e]=t[e](u),l.c()),P(l,1),l.m(s.parentNode,s))},i(u){n||(P(l),n=!0)},o(u){q(l),n=!1},d(u){o[e].d(u),u&&m(s)}}}function se(r){let e,l;return e=new W({props:{nobranding:!0,$$slots:{default:[te]},$$scope:{ctx:r}}}),{c(){F(e.$$.fragment)},m(s,n){N(e,s,n),l=!0},p(s,[n]){const t={};n&2079&&(t.$$scope={dirty:n,ctx:s}),e.$set(t)},i(s){l||(P(e.$$.fragment,s),l=!0)},o(s){q(e.$$.fragment,s),l=!1},d(s){L(e,s)}}}function le(r,e,l){let s,{params:n}=e,t="",o="",c=!1,u=!1;async function i(){if(c)return;l(2,c=!0);const g=new K("../");try{const C=O(n==null?void 0:n.token);await g.collection(C.collectionId).confirmPasswordReset(n==null?void 0:n.token,t,o),l(3,u=!0)}catch(C){Q.errorResponseHandler(C)}l(2,c=!1)}const a=()=>window.close();function v(){t=this.value,l(0,t)}function k(){o=this.value,l(1,o)}return r.$$set=g=>{"params"in g&&l(6,n=g.params)},r.$$.update=()=>{r.$$.dirty&64&&l(4,s=Y.getJWTPayload(n==null?void 0:n.token).email||"")},[t,o,c,u,s,i,n,a,v,k]}class oe extends I{constructor(e){super(),J(this,e,le,se,M,{params:6})}}export{oe as default};
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import{S as v,i as y,s as w,F as x,c as C,m as g,t as $,a as H,d as L,G as M,H as P,E as S,g as r,o as a,e as u,b as _,f,u as b,y as p}from"./index.5dab115f.js";function T(o){let t,s,e,n,l;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
|
||||
import{S as v,i as y,s as w,F as x,c as C,m as g,t as $,a as H,d as L,G as M,H as P,E as S,g as r,o as a,e as u,b as _,f,u as b,y as p}from"./index.536fddcb.js";function T(o){let t,s,e,n,l;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-error-warning-line"></i></div>
|
||||
<div class="content txt-bold"><p>Invalid or expired verification token.</p></div>`,s=_(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-danger"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(i,c){r(i,t,c),r(i,s,c),r(i,e,c),n||(l=b(e,"click",o[4]),n=!0)},p,d(i){i&&a(t),i&&a(s),i&&a(e),n=!1,l()}}}function F(o){let t,s,e,n,l;return{c(){t=u("div"),t.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
|
||||
<div class="content txt-bold"><p>Successfully verified email address.</p></div>`,s=_(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(i,c){r(i,t,c),r(i,s,c),r(i,e,c),n||(l=b(e,"click",o[3]),n=!0)},p,d(i){i&&a(t),i&&a(s),i&&a(e),n=!1,l()}}}function I(o){let t;return{c(){t=u("div"),t.innerHTML='<div class="loader loader-lg"><em>Please wait...</em></div>',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function V(o){let t;function s(l,i){return l[1]?I:l[0]?F:T}let e=s(o),n=e(o);return{c(){n.c(),t=S()},m(l,i){n.m(l,i),r(l,t,i)},p(l,i){e===(e=s(l))&&n?n.p(l,i):(n.d(1),n=e(l),n&&(n.c(),n.m(t.parentNode,t)))},d(l){n.d(l),l&&a(t)}}}function q(o){let t,s;return t=new x({props:{nobranding:!0,$$slots:{default:[V]},$$scope:{ctx:o}}}),{c(){C(t.$$.fragment)},m(e,n){g(t,e,n),s=!0},p(e,[n]){const l={};n&67&&(l.$$scope={dirty:n,ctx:e}),t.$set(l)},i(e){s||($(t.$$.fragment,e),s=!0)},o(e){H(t.$$.fragment,e),s=!1},d(e){L(t,e)}}}function A(o,t,s){let{params:e}=t,n=!1,l=!1;i();async function i(){s(1,l=!0);const d=new M("../");try{const m=P(e==null?void 0:e.token);await d.collection(m.collectionId).confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch{s(0,n=!1)}s(1,l=!1)}const c=()=>window.close(),k=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,l,e,c,k]}class G extends v{constructor(t){super(),y(this,t,A,q,w,{params:2})}}export{G as default};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as re,i as ae,s as be,N as ue,C as P,e as u,w as y,b as a,c as te,f as p,g as t,h as I,m as ne,x as pe,t as ie,a as le,o as n,d as ce,R as me,p as de}from"./index.5dab115f.js";import{S as fe}from"./SdkTabs.25abcc83.js";function $e(o){var B,U,W,A,H,L,T,q,M,N,j,J;let i,m,l=o[0].name+"",b,d,h,f,_,$,k,c,S,v,w,R,C,g,E,r,D;return c=new fe({props:{js:`
|
||||
import{S as re,i as ae,s as be,N as ue,C as P,e as u,w as y,b as a,c as te,f as p,g as t,h as I,m as ne,x as pe,t as ie,a as le,o as n,d as ce,R as me,p as de}from"./index.536fddcb.js";import{S as fe}from"./SdkTabs.3571c26b.js";function $e(o){var B,U,W,A,H,L,T,q,M,N,j,J;let i,m,l=o[0].name+"",b,d,h,f,_,$,k,c,S,v,w,R,C,g,E,r,D;return c=new fe({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[1]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Te,i as Ee,s as Be,e as c,w as v,b as h,c as Pe,f,g as r,h as n,m as Ce,x as I,O as ve,P as Se,k as Re,Q as Me,n as Ae,t as x,a as ee,o as m,d as ye,R as We,C as ze,p as He,r as L,u as Oe,N as Ue}from"./index.5dab115f.js";import{S as je}from"./SdkTabs.25abcc83.js";function we(o,l,s){const a=o.slice();return a[5]=l[s],a}function ge(o,l,s){const a=o.slice();return a[5]=l[s],a}function $e(o,l){let s,a=l[5].code+"",_,b,i,p;function u(){return l[4](l[5])}return{key:o,first:null,c(){s=c("button"),_=v(a),b=h(),f(s,"class","tab-item"),L(s,"active",l[1]===l[5].code),this.first=s},m($,q){r($,s,q),n(s,_),n(s,b),i||(p=Oe(s,"click",u),i=!0)},p($,q){l=$,q&4&&a!==(a=l[5].code+"")&&I(_,a),q&6&&L(s,"active",l[1]===l[5].code)},d($){$&&m(s),i=!1,p()}}}function qe(o,l){let s,a,_,b;return a=new Ue({props:{content:l[5].body}}),{key:o,first:null,c(){s=c("div"),Pe(a.$$.fragment),_=h(),f(s,"class","tab-item"),L(s,"active",l[1]===l[5].code),this.first=s},m(i,p){r(i,s,p),Ce(a,s,null),n(s,_),b=!0},p(i,p){l=i;const u={};p&4&&(u.content=l[5].body),a.$set(u),(!b||p&6)&&L(s,"active",l[1]===l[5].code)},i(i){b||(x(a.$$.fragment,i),b=!0)},o(i){ee(a.$$.fragment,i),b=!1},d(i){i&&m(s),ye(a)}}}function De(o){var de,pe,ue,fe;let l,s,a=o[0].name+"",_,b,i,p,u,$,q,z=o[0].name+"",N,te,F,P,K,T,Q,w,H,le,O,E,se,G,U=o[0].name+"",J,ae,oe,j,V,B,X,S,Y,R,Z,C,M,g=[],ne=new Map,ie,A,k=[],ce=new Map,y;P=new je({props:{js:`
|
||||
import{S as Te,i as Ee,s as Be,e as c,w as v,b as h,c as Pe,f,g as r,h as n,m as Ce,x as I,O as ve,P as Se,k as Re,Q as Me,n as Ae,t as x,a as ee,o as m,d as ye,R as We,C as ze,p as He,r as L,u as Oe,N as Ue}from"./index.536fddcb.js";import{S as je}from"./SdkTabs.3571c26b.js";function we(o,l,s){const a=o.slice();return a[5]=l[s],a}function ge(o,l,s){const a=o.slice();return a[5]=l[s],a}function $e(o,l){let s,a=l[5].code+"",_,b,i,p;function u(){return l[4](l[5])}return{key:o,first:null,c(){s=c("button"),_=v(a),b=h(),f(s,"class","tab-item"),L(s,"active",l[1]===l[5].code),this.first=s},m($,q){r($,s,q),n(s,_),n(s,b),i||(p=Oe(s,"click",u),i=!0)},p($,q){l=$,q&4&&a!==(a=l[5].code+"")&&I(_,a),q&6&&L(s,"active",l[1]===l[5].code)},d($){$&&m(s),i=!1,p()}}}function qe(o,l){let s,a,_,b;return a=new Ue({props:{content:l[5].body}}),{key:o,first:null,c(){s=c("div"),Pe(a.$$.fragment),_=h(),f(s,"class","tab-item"),L(s,"active",l[1]===l[5].code),this.first=s},m(i,p){r(i,s,p),Ce(a,s,null),n(s,_),b=!0},p(i,p){l=i;const u={};p&4&&(u.content=l[5].body),a.$set(u),(!b||p&6)&&L(s,"active",l[1]===l[5].code)},i(i){b||(x(a.$$.fragment,i),b=!0)},o(i){ee(a.$$.fragment,i),b=!1},d(i){i&&m(s),ye(a)}}}function De(o){var de,pe,ue,fe;let l,s,a=o[0].name+"",_,b,i,p,u,$,q,z=o[0].name+"",N,te,F,P,K,T,Q,w,H,le,O,E,se,G,U=o[0].name+"",J,ae,oe,j,V,B,X,S,Y,R,Z,C,M,g=[],ne=new Map,ie,A,k=[],ce=new Map,y;P=new je({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[3]}');
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{S as Pe,i as $e,s as qe,e as c,w,b as v,c as ve,f as b,g as r,h as n,m as we,x as F,O as ue,P as Re,k as ge,Q as ye,n as Be,t as Z,a as x,o as d,d as he,R as Ce,C as Se,p as Te,r as L,u as Me,N as Ae}from"./index.5dab115f.js";import{S as Ue}from"./SdkTabs.25abcc83.js";function me(a,s,l){const o=a.slice();return o[5]=s[l],o}function be(a,s,l){const o=a.slice();return o[5]=s[l],o}function _e(a,s){let l,o=s[5].code+"",_,m,i,p;function u(){return s[4](s[5])}return{key:a,first:null,c(){l=c("button"),_=w(o),m=v(),b(l,"class","tab-item"),L(l,"active",s[1]===s[5].code),this.first=l},m(P,$){r(P,l,$),n(l,_),n(l,m),i||(p=Me(l,"click",u),i=!0)},p(P,$){s=P,$&4&&o!==(o=s[5].code+"")&&F(_,o),$&6&&L(l,"active",s[1]===s[5].code)},d(P){P&&d(l),i=!1,p()}}}function ke(a,s){let l,o,_,m;return o=new Ae({props:{content:s[5].body}}),{key:a,first:null,c(){l=c("div"),ve(o.$$.fragment),_=v(),b(l,"class","tab-item"),L(l,"active",s[1]===s[5].code),this.first=l},m(i,p){r(i,l,p),we(o,l,null),n(l,_),m=!0},p(i,p){s=i;const u={};p&4&&(u.content=s[5].body),o.$set(u),(!m||p&6)&&L(l,"active",s[1]===s[5].code)},i(i){m||(Z(o.$$.fragment,i),m=!0)},o(i){x(o.$$.fragment,i),m=!1},d(i){i&&d(l),he(o)}}}function je(a){var re,de;let s,l,o=a[0].name+"",_,m,i,p,u,P,$,D=a[0].name+"",N,ee,Q,q,z,B,G,R,H,te,I,C,se,J,O=a[0].name+"",K,le,V,S,W,T,X,M,Y,g,A,h=[],oe=new Map,ae,U,k=[],ne=new Map,y;q=new Ue({props:{js:`
|
||||
import{S as Pe,i as $e,s as qe,e as c,w,b as v,c as ve,f as b,g as r,h as n,m as we,x as F,O as ue,P as Re,k as ge,Q as ye,n as Be,t as Z,a as x,o as d,d as he,R as Ce,C as Se,p as Te,r as L,u as Me,N as Ae}from"./index.536fddcb.js";import{S as Ue}from"./SdkTabs.3571c26b.js";function me(a,s,l){const o=a.slice();return o[5]=s[l],o}function be(a,s,l){const o=a.slice();return o[5]=s[l],o}function _e(a,s){let l,o=s[5].code+"",_,m,i,p;function u(){return s[4](s[5])}return{key:a,first:null,c(){l=c("button"),_=w(o),m=v(),b(l,"class","tab-item"),L(l,"active",s[1]===s[5].code),this.first=l},m(P,$){r(P,l,$),n(l,_),n(l,m),i||(p=Me(l,"click",u),i=!0)},p(P,$){s=P,$&4&&o!==(o=s[5].code+"")&&F(_,o),$&6&&L(l,"active",s[1]===s[5].code)},d(P){P&&d(l),i=!1,p()}}}function ke(a,s){let l,o,_,m;return o=new Ae({props:{content:s[5].body}}),{key:a,first:null,c(){l=c("div"),ve(o.$$.fragment),_=v(),b(l,"class","tab-item"),L(l,"active",s[1]===s[5].code),this.first=l},m(i,p){r(i,l,p),we(o,l,null),n(l,_),m=!0},p(i,p){s=i;const u={};p&4&&(u.content=s[5].body),o.$set(u),(!m||p&6)&&L(l,"active",s[1]===s[5].code)},i(i){m||(Z(o.$$.fragment,i),m=!0)},o(i){x(o.$$.fragment,i),m=!1},d(i){i&&d(l),he(o)}}}function je(a){var re,de;let s,l,o=a[0].name+"",_,m,i,p,u,P,$,D=a[0].name+"",N,ee,Q,q,z,B,G,R,H,te,I,C,se,J,O=a[0].name+"",K,le,V,S,W,T,X,M,Y,g,A,h=[],oe=new Map,ae,U,k=[],ne=new Map,y;q=new Ue({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[3]}');
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user