1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-12-04 03:39:23 +02:00

filter enhancements

This commit is contained in:
Gani Georgiev 2023-01-07 22:25:56 +02:00
parent d5775ff657
commit 9b880f5ab4
102 changed files with 3693 additions and 986 deletions

View File

@ -1,4 +1,66 @@
## (WIP) ## (WIP) v0.11.0
- Added `+` and `-` body field modifiers for `number`, `files`, `select` and `relation` fields.
```js
{
// oldValue + 2
"someNumber+": 2,
// oldValue + ["id1", "id2"] - ["id3"]
"someRelation+": ["id1", "id2"],
"someRelation-": ["id3"],
// delete single file by its name (file fields supports only the "-" modifier!)
"someFile-": "filename.png",
}
```
_Note1: `@request.data.someField` will contain the final resolved value._
_Note2: The old index (`"field.0":null`) and filename (`"field.filename.png":null`) based suffixed syntax for deleting files is still supported._
- ! Added support for multi-match/match-all request data and collection multi-valued fields (`select`, `relation`) conditions.
If you want a "at least one of" type of condition, you can prefix the operator with `?`.
```js
// for each someRelA.someRelB record require the "status" field to be "active"
someRelA.someRelB.status = "active"
// OR for "at least one of" condition
someRelA.someRelB.status ?= "active"
```
_**Note: Previously the behavior for multi-valued fields was as the "at least one of" type.
The release comes with system db migration that will update your existing API rules (if needed) to preserve the compatibility.
If you have multi-select or multi-relation filter checks in your client-side code and want to preserve the old behavior, you'll have to prefix with `?` your operators.**_
- Added support for querying `@request.data.someRelField.*` relation fields.
```js
// example submitted data: {"someRel": "REL_RECORD_ID"}
@request.data.someRel.status = "active"
```
- Added `:isset` modifier for the static request data fields.
```js
// prevent changing the "role" field
@request.data.role:isset = false
```
- Added `:length` modifier for the arrayable request data and collection fields (`select`, `file`, `relation`).
```js
// example submitted data: {"someSelectField": ["val1", "val2"]}
@request.data.someSelectField:length = 2
// check existing record field length
someSelectField:length = 2
```
- Added `:each` modifier support for the multi-`select` request data and collection field.
```js
// check if all selected rows has "pb_" prefix
roles:each ~ 'pb_%'
```
- Improved the Admin UI filters autocomplete.
- Added `@random` sort key for `RANDOM()` sorted list results.
- Added Strava OAuth2 provider ([#1443](https://github.com/pocketbase/pocketbase/pull/1443); thanks @szsascha). - Added Strava OAuth2 provider ([#1443](https://github.com/pocketbase/pocketbase/pull/1443); thanks @szsascha).
@ -6,13 +68,27 @@
- Added IME status check to the textarea keydown handler ([#1370](https://github.com/pocketbase/pocketbase/pull/1370); thanks @tenthree). - Added IME status check to the textarea keydown handler ([#1370](https://github.com/pocketbase/pocketbase/pull/1370); thanks @tenthree).
- Fixed the text wrapping in the Admin UI listing searchbar ([#1416](https://github.com/pocketbase/pocketbase/issues/1416)).
- Added `filesystem.NewFileFromBytes()` helper ([#1420](https://github.com/pocketbase/pocketbase/pull/1420); thanks @dschissler). - Added `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 number field value output in the records listing ([#1447](https://github.com/pocketbase/pocketbase/issues/1447)).
- Added webp to the default images mime type presets list ([#1469](https://github.com/pocketbase/pocketbase/pull/1469); thanks @khairulhaaziq). - Fixed duplicated settings view pages caused by uncompleted transitions ([#1498](https://github.com/pocketbase/pocketbase/issues/1498)).
- Allowed sending `Authorization` header with the `/auth-with-password` record and admin login requests ([#1494](https://github.com/pocketbase/pocketbase/discussions/1494)).
- `migrate down` now reverts migrations in the applied order.
- Added additional list-bucket check in the S3 config test API.
- Other minor improvements.
## v0.10.4 ## v0.10.4

View File

@ -1,5 +1,5 @@
The MIT License (MIT) 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 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, and associated documentation files (the "Software"), to deal in the Software without restriction,

View File

@ -18,7 +18,7 @@ func bindAdminApi(app core.App, rg *echo.Group) {
api := adminApi{app: app} api := adminApi{app: app}
subGroup := rg.Group("/admins", ActivityLogger(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("/request-password-reset", api.requestPasswordReset)
subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) subGroup.POST("/confirm-password-reset", api.confirmPasswordReset)
subGroup.POST("/auth-refresh", api.authRefresh, RequireAdminAuth()) subGroup.POST("/auth-refresh", api.authRefresh, RequireAdminAuth())

View File

@ -48,6 +48,20 @@ func TestAdminAuthWithEmail(t *testing.T) {
ExpectedStatus: 400, ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`}, 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)", Name: "valid email/password (already authorized)",
Method: http.MethodPost, Method: http.MethodPost,
@ -56,14 +70,6 @@ func TestAdminAuthWithEmail(t *testing.T) {
RequestHeaders: map[string]string{ RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg", "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, ExpectedStatus: 200,
ExpectedContent: []string{ ExpectedContent: []string{
`"admin":{"id":"sywbhecnh46rhm0"`, `"admin":{"id":"sywbhecnh46rhm0"`,

View File

@ -42,7 +42,7 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{ ExpectedContent: []string{
`"page":1`, `"page":1`,
`"perPage":30`, `"perPage":30`,
`"totalItems":7`, `"totalItems":8`,
`"items":[{`, `"items":[{`,
`"id":"_pb_users_auth_"`, `"id":"_pb_users_auth_"`,
`"id":"v851q4r790rhknl"`, `"id":"v851q4r790rhknl"`,
@ -51,6 +51,7 @@ func TestCollectionsList(t *testing.T) {
`"id":"sz5l5z67tg7gku0"`, `"id":"sz5l5z67tg7gku0"`,
`"id":"wzlqyes4orhoygb"`, `"id":"wzlqyes4orhoygb"`,
`"id":"4d1blo5cuycfaca"`, `"id":"4d1blo5cuycfaca"`,
`"id":"9n89pl5vkct6330"`,
`"type":"auth"`, `"type":"auth"`,
`"type":"base"`, `"type":"base"`,
}, },
@ -69,10 +70,10 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{ ExpectedContent: []string{
`"page":2`, `"page":2`,
`"perPage":2`, `"perPage":2`,
`"totalItems":7`, `"totalItems":8`,
`"items":[{`, `"items":[{`,
`"id":"v851q4r790rhknl"`,
`"id":"4d1blo5cuycfaca"`, `"id":"4d1blo5cuycfaca"`,
`"id":"wzlqyes4orhoygb"`,
}, },
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnCollectionsListRequest": 1, "OnCollectionsListRequest": 1,
@ -99,12 +100,13 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{ ExpectedContent: []string{
`"page":1`, `"page":1`,
`"perPage":30`, `"perPage":30`,
`"totalItems":4`, `"totalItems":5`,
`"items":[{`, `"items":[{`,
`"id":"wsmn24bux7wo113"`, `"id":"wsmn24bux7wo113"`,
`"id":"sz5l5z67tg7gku0"`, `"id":"sz5l5z67tg7gku0"`,
`"id":"wzlqyes4orhoygb"`, `"id":"wzlqyes4orhoygb"`,
`"id":"4d1blo5cuycfaca"`, `"id":"4d1blo5cuycfaca"`,
`"id":"9n89pl5vkct6330"`,
}, },
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnCollectionsListRequest": 1, "OnCollectionsListRequest": 1,
@ -786,7 +788,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
} }
expected := 7 expected := 8
if len(collections) != expected { if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections)) t.Fatalf("Expected %d collections, got %d", expected, len(collections))
} }
@ -814,7 +816,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
} }
expected := 7 expected := 8
if len(collections) != expected { if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections)) t.Fatalf("Expected %d collections, got %d", expected, len(collections))
} }
@ -856,7 +858,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
} }
expected := 7 expected := 8
if len(collections) != expected { if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections)) t.Fatalf("Expected %d collections, got %d", expected, len(collections))
} }
@ -909,7 +911,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil { if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err) t.Fatal(err)
} }
expected := 10 expected := 11
if len(collections) != expected { if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections)) t.Fatalf("Expected %d collections, got %d", expected, len(collections))
} }
@ -996,8 +998,8 @@ func TestCollectionImport(t *testing.T) {
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnCollectionsAfterImportRequest": 1, "OnCollectionsAfterImportRequest": 1,
"OnCollectionsBeforeImportRequest": 1, "OnCollectionsBeforeImportRequest": 1,
"OnModelBeforeDelete": 5, "OnModelBeforeDelete": 6,
"OnModelAfterDelete": 5, "OnModelAfterDelete": 6,
"OnModelBeforeUpdate": 2, "OnModelBeforeUpdate": 2,
"OnModelAfterUpdate": 2, "OnModelAfterUpdate": 2,
"OnModelBeforeCreate": 1, "OnModelBeforeCreate": 1,

View File

@ -35,8 +35,8 @@ func bindRecordAuthApi(app core.App, rg *echo.Group) {
subGroup.GET("/auth-methods", api.authMethods) subGroup.GET("/auth-methods", api.authMethods)
subGroup.POST("/auth-refresh", api.authRefresh, RequireSameContextRecordAuth()) 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-oauth2", api.authWithOAuth2)
subGroup.POST("/auth-with-password", api.authWithPassword, RequireGuestOnly()) subGroup.POST("/auth-with-password", api.authWithPassword)
subGroup.POST("/request-password-reset", api.requestPasswordReset) subGroup.POST("/request-password-reset", api.requestPasswordReset)
subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) subGroup.POST("/confirm-password-reset", api.confirmPasswordReset)
subGroup.POST("/request-verification", api.requestVerification) subGroup.POST("/request-verification", api.requestVerification)

View File

@ -66,26 +66,6 @@ func TestRecordAuthMethodsList(t *testing.T) {
func TestRecordAuthWithPassword(t *testing.T) { func TestRecordAuthWithPassword(t *testing.T) {
scenarios := []tests.ApiScenario{ 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", Name: "invalid body format",
Method: http.MethodPost, Method: http.MethodPost,
@ -226,6 +206,52 @@ func TestRecordAuthWithPassword(t *testing.T) {
"OnRecordAuthRequest": 1, "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 { for _, scenario := range scenarios {

View File

@ -166,6 +166,20 @@ func (api *recordApi) create(c echo.Context) error {
// temporary save the record and check it against the create rule // temporary save the record and check it against the create rule
if requestData.Admin == nil && collection.CreateRule != nil { if 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 { createRuleFunc := func(q *dbx.SelectQuery) error {
if *collection.CreateRule == "" { if *collection.CreateRule == "" {
return nil // no create rule to resolve return nil // no create rule to resolve
@ -181,13 +195,6 @@ func (api *recordApi) create(c echo.Context) error {
return nil 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 { testErr := testForm.DrySubmit(func(txDao *daos.Dao) error {
foundRecord, err := txDao.FindRecordById(collection.Id, testRecord.Id, createRuleFunc) foundRecord, err := txDao.FindRecordById(collection.Id, testRecord.Id, createRuleFunc)
if err != nil { if err != nil {
@ -258,6 +265,16 @@ func (api *recordApi) update(c echo.Context) error {
return NewForbiddenError("Only admins can perform this action.", nil) return NewForbiddenError("Only admins can perform this action.", nil)
} }
// eager fetch the record so that the modifier field values are replaced
// 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 { ruleFunc := func(q *dbx.SelectQuery) error {
if requestData.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" { if requestData.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true) resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)

View File

@ -2,6 +2,7 @@ package apis_test
import ( import (
"net/http" "net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -209,6 +210,50 @@ func TestRecordCrudList(t *testing.T) {
}, },
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, 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 // auth collection checks
// ----------------------------------------------------------- // -----------------------------------------------------------
@ -716,6 +761,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 // cascade delete checks
// ----------------------------------------------------------- // -----------------------------------------------------------
@ -730,7 +794,7 @@ func TestRecordCrudDelete(t *testing.T) {
ExpectedContent: []string{`"data":{}`}, ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{ ExpectedEvents: map[string]int{
"OnRecordBeforeDeleteRequest": 1, "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 "OnModelBeforeDelete": 2, // the record itself + rel_one_cascade of test1 record
}, },
}, },
@ -1092,6 +1156,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 // auth records
// ----------------------------------------------------------- // -----------------------------------------------------------
{ {
@ -1501,6 +1622,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 // auth records
// ----------------------------------------------------------- // -----------------------------------------------------------
{ {

View File

@ -15,11 +15,6 @@ import (
const ContextRequestDataKey = "requestData" 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 // RequestData exports cached common request data fields
// (query, body, logged auth state, etc.) from the provided context. // (query, body, logged auth state, etc.) from the provided context.
func RequestData(c echo.Context) *models.RequestData { func RequestData(c echo.Context) *models.RequestData {

View File

@ -1,6 +1,7 @@
package apis package apis
import ( import (
"fmt"
"net/http" "net/http"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
@ -91,14 +92,17 @@ func (api *settingsApi) testS3(c echo.Context) error {
} }
defer fs.Close() 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 { if err := fs.Upload([]byte("test"), testFileKey); err != nil {
return NewBadRequestError("Failed to upload a test file. Raw error: \n"+err.Error(), nil) return NewBadRequestError("Failed to upload a test file. Raw error: \n"+err.Error(), nil)
} }
if err := fs.Delete(testFileKey); err != nil { // test prefix deletion (ensures that both bucket list and delete works)
return NewBadRequestError("Failed to delete a test file. Raw error: \n"+err.Error(), nil) 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) return c.NoContent(http.StatusNoContent)

View File

@ -78,7 +78,8 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
GetCertificate: certManager.GetCertificate, GetCertificate: certManager.GetCertificate,
NextProtos: []string{acme.ALPNProto}, NextProtos: []string{acme.ALPNProto},
}, },
ReadTimeout: 60 * time.Second, ReadTimeout: 5 * time.Minute,
ReadHeaderTimeout: 30 * time.Second,
// WriteTimeout: 60 * time.Second, // breaks sse! // WriteTimeout: 60 * time.Second, // breaks sse!
Handler: router, Handler: router,
Addr: mainAddr, Addr: mainAddr,

View File

@ -18,7 +18,7 @@ import (
type App interface { type App interface {
// Deprecated: // Deprecated:
// This method may get removed in the near future. // 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(). // if you want more flexibility - app.Dao().ConcurrentDB() and app.Dao().NonconcurrentDB().
// //
// DB returns the default app database instance. // DB returns the default app database instance.

View File

@ -97,8 +97,8 @@ func (dao *Dao) IsAdminEmailUnique(email string, excludeIds ...string) bool {
AndWhere(dbx.HashExp{"email": email}). AndWhere(dbx.HashExp{"email": email}).
Limit(1) Limit(1)
if len(excludeIds) > 0 { if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(excludeIds)...)) query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...))
} }
var exists bool var exists bool

View File

@ -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)})). AndWhere(dbx.NewExp("LOWER([[name]])={:name}", dbx.Params{"name": strings.ToLower(name)})).
Limit(1) Limit(1)
if len(excludeIds) > 0 { if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
uniqueExcludeIds := list.NonzeroUniques(excludeIds)
query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...)) query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...))
} }
@ -85,15 +84,17 @@ func (dao *Dao) FindCollectionReferences(collection *models.Collection, excludeI
collections := []*models.Collection{} collections := []*models.Collection{}
query := dao.CollectionQuery() 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)...)) query.AndWhere(dbx.NotIn("id", list.ToInterfaceSlice(uniqueExcludeIds)...))
} }
if err := query.All(&collections); err != nil { if err := query.All(&collections); err != nil {
return nil, err return nil, err
} }
result := map[*models.Collection][]*schema.SchemaField{} result := map[*models.Collection][]*schema.SchemaField{}
for _, c := range collections { for _, c := range collections {
for _, f := range c.Schema.Fields() { for _, f := range c.Schema.Fields() {
if f.Type != schema.FieldTypeRelation { if f.Type != schema.FieldTypeRelation {

View File

@ -37,7 +37,7 @@ func TestFindCollectionsByType(t *testing.T) {
{"", false, 0}, {"", false, 0},
{"unknown", false, 0}, {"unknown", false, 0},
{models.CollectionTypeAuth, false, 3}, {models.CollectionTypeAuth, false, 3},
{models.CollectionTypeBase, false, 4}, {models.CollectionTypeBase, false, 5},
} }
for i, scenario := range scenarios { for i, scenario := range scenarios {
@ -122,7 +122,13 @@ func TestFindCollectionReferences(t *testing.T) {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -296,7 +302,7 @@ func TestImportCollections(t *testing.T) {
name: "empty collections", name: "empty collections",
jsonData: `[]`, jsonData: `[]`,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
}, },
{ {
name: "minimal collection import", name: "minimal collection import",
@ -306,7 +312,7 @@ func TestImportCollections(t *testing.T) {
]`, ]`,
deleteMissing: false, deleteMissing: false,
expectError: false, expectError: false,
expectCollectionsCount: 9, expectCollectionsCount: 10,
}, },
{ {
name: "minimal collection import + failed beforeRecordsSync", name: "minimal collection import + failed beforeRecordsSync",
@ -318,7 +324,7 @@ func TestImportCollections(t *testing.T) {
}, },
deleteMissing: false, deleteMissing: false,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
}, },
{ {
name: "minimal collection import + successful beforeRecordsSync", name: "minimal collection import + successful beforeRecordsSync",
@ -330,7 +336,7 @@ func TestImportCollections(t *testing.T) {
}, },
deleteMissing: false, deleteMissing: false,
expectError: false, expectError: false,
expectCollectionsCount: 8, expectCollectionsCount: 9,
}, },
{ {
name: "new + update + delete system collection", name: "new + update + delete system collection",
@ -366,7 +372,7 @@ func TestImportCollections(t *testing.T) {
]`, ]`,
deleteMissing: true, deleteMissing: true,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
}, },
{ {
name: "new + update + delete non-system collection", name: "new + update + delete non-system collection",
@ -495,7 +501,7 @@ func TestImportCollections(t *testing.T) {
]`, ]`,
deleteMissing: false, deleteMissing: false,
expectError: false, expectError: false,
expectCollectionsCount: 8, expectCollectionsCount: 9,
afterTestFunc: func(testApp *tests.TestApp, resultCollections []*models.Collection) { afterTestFunc: func(testApp *tests.TestApp, resultCollections []*models.Collection) {
expectedCollectionFields := map[string]int{ expectedCollectionFields := map[string]int{
"nologin": 1, "nologin": 1,
@ -503,6 +509,7 @@ func TestImportCollections(t *testing.T) {
"demo2": 2, "demo2": 2,
"demo3": 2, "demo3": 2,
"demo4": 11, "demo4": 11,
"demo5": 5,
"new_import": 1, "new_import": 1,
} }
for name, expectedCount := range expectedCollectionFields { for name, expectedCount := range expectedCollectionFields {

View File

@ -84,7 +84,7 @@ func (dao *Dao) FindRecordsByIds(
} }
} }
rows := []dbx.NullStringMap{} rows := make([]dbx.NullStringMap, 0, len(recordIds))
if err := query.All(&rows); err != nil { if err := query.All(&rows); err != nil {
return nil, err return nil, err
} }
@ -192,8 +192,7 @@ func (dao *Dao) IsRecordValueUnique(
AndWhere(expr). AndWhere(expr).
Limit(1) Limit(1)
if len(excludeIds) > 0 { if uniqueExcludeIds := list.NonzeroUniques(excludeIds); len(uniqueExcludeIds) > 0 {
uniqueExcludeIds := list.NonzeroUniques(excludeIds)
query.AndWhere(dbx.NotIn(collection.Name+".id", list.ToInterfaceSlice(uniqueExcludeIds)...)) query.AndWhere(dbx.NotIn(collection.Name+".id", list.ToInterfaceSlice(uniqueExcludeIds)...))
} }

View File

@ -215,7 +215,7 @@ func (form *CollectionUpsert) ensureExistingRelationCollectionId(value any) erro
continue 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( return validation.Errors{fmt.Sprint(i): validation.NewError(
"validation_field_invalid_relation", "validation_field_invalid_relation",
"The relation collection doesn't exist.", "The relation collection doesn't exist.",

View File

@ -52,7 +52,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
"collections": [] "collections": []
}`, }`,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
expectEvents: nil, expectEvents: nil,
}, },
{ {
@ -82,7 +82,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
] ]
}`, }`,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
expectEvents: map[string]int{ expectEvents: map[string]int{
"OnModelBeforeCreate": 2, "OnModelBeforeCreate": 2,
}, },
@ -101,7 +101,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
] ]
}`, }`,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
expectEvents: map[string]int{ expectEvents: map[string]int{
"OnModelBeforeCreate": 2, "OnModelBeforeCreate": 2,
}, },
@ -137,7 +137,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
] ]
}`, }`,
expectError: false, expectError: false,
expectCollectionsCount: 10, expectCollectionsCount: 11,
expectEvents: map[string]int{ expectEvents: map[string]int{
"OnModelBeforeCreate": 3, "OnModelBeforeCreate": 3,
"OnModelAfterCreate": 3, "OnModelAfterCreate": 3,
@ -160,7 +160,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
] ]
}`, }`,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
expectEvents: map[string]int{ expectEvents: map[string]int{
"OnModelBeforeCreate": 1, "OnModelBeforeCreate": 1,
}, },
@ -202,7 +202,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
] ]
}`, }`,
expectError: true, expectError: true,
expectCollectionsCount: 7, expectCollectionsCount: 8,
expectEvents: map[string]int{ expectEvents: map[string]int{
"OnModelBeforeDelete": 5, "OnModelBeforeDelete": 5,
}, },
@ -253,7 +253,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
] ]
}`, }`,
expectError: false, expectError: false,
expectCollectionsCount: 9, expectCollectionsCount: 10,
expectEvents: map[string]int{ expectEvents: map[string]int{
"OnModelBeforeUpdate": 1, "OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1, "OnModelAfterUpdate": 1,
@ -341,8 +341,8 @@ func TestCollectionsImportSubmit(t *testing.T) {
"OnModelAfterUpdate": 2, "OnModelAfterUpdate": 2,
"OnModelBeforeCreate": 1, "OnModelBeforeCreate": 1,
"OnModelAfterCreate": 1, "OnModelAfterCreate": 1,
"OnModelBeforeDelete": 5, "OnModelBeforeDelete": 6,
"OnModelAfterDelete": 5, "OnModelAfterDelete": 6,
}, },
}, },
} }

View File

@ -7,7 +7,6 @@ import (
"log" "log"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"strings" "strings"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
@ -219,11 +218,6 @@ func (form *RecordUpsert) extractMultipartFormData(
// and lods it into the form. // and lods it into the form.
// //
// File upload is supported only via multipart/form-data. // 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 { func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error {
requestData, uploadedFiles, err := form.extractRequestData(r, keyPrefix) requestData, uploadedFiles, err := form.extractRequestData(r, keyPrefix)
if err != nil { 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. // 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 { func (form *RecordUpsert) LoadData(requestData map[string]any) error {
// load base system fields // load base system fields
if v, ok := requestData["id"]; ok { if v, ok := requestData[schema.FieldNameId]; ok {
form.Id = cast.ToString(v) form.Id = cast.ToString(v)
} }
// load auth system fields // load auth system fields
if form.record.Collection().IsAuth() { if form.record.Collection().IsAuth() {
if v, ok := requestData["username"]; ok { if v, ok := requestData[schema.FieldNameUsername]; ok {
form.Username = cast.ToString(v) form.Username = cast.ToString(v)
} }
if v, ok := requestData["email"]; ok { if v, ok := requestData[schema.FieldNameEmail]; ok {
form.Email = cast.ToString(v) form.Email = cast.ToString(v)
} }
if v, ok := requestData["emailVisibility"]; ok { if v, ok := requestData[schema.FieldNameEmailVisibility]; ok {
form.EmailVisibility = cast.ToBool(v) form.EmailVisibility = cast.ToBool(v)
} }
if v, ok := requestData["verified"]; ok { if v, ok := requestData[schema.FieldNameVerified]; ok {
form.Verified = cast.ToBool(v) form.Verified = cast.ToBool(v)
} }
if v, ok := requestData["password"]; ok { 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 // replace modifiers (if any)
extendedData := form.record.SchemaData() 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) rawData, err := json.Marshal(requestData)
if err != nil { if err != nil {
return err return err
@ -393,8 +390,7 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
for _, field := range form.record.Collection().Schema.Fields() { for _, field := range form.record.Collection().Schema.Fields() {
key := field.Name key := field.Name
value := extendedData[key] value := field.PrepareValue(extendedData[key])
value = field.PrepareValue(value)
if field.Type != schema.FieldTypeFile { if field.Type != schema.FieldTypeFile {
form.data[key] = value form.data[key] = value
@ -405,30 +401,32 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
// Delete previously uploaded file(s) // 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 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) form.RemoveFiles(key)
} else if len(oldNames) > 0 { } else if len(oldNames) > 0 {
toDelete := []string{} toDelete := []string{}
// search for individual file name to delete (eg. "file.test.png = null")
for _, name := range oldNames { 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) toDelete = append(toDelete, name)
} continue
}
// 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])
} }
} }
@ -436,6 +434,12 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
form.RemoveFiles(key, toDelete...) 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 return nil

View File

@ -89,9 +89,10 @@ func TestRecordUpsertLoadRequestJson(t *testing.T) {
"unknown": "test456", "unknown": "test456",
// file fields unset/delete // file fields unset/delete
"file_one": nil, "file_one": nil,
"file_many.0": "", // delete by index "file_many.0": "", // delete by index
"file_many.1": "test.png", // should be ignored "file_many-": []string{"test_MaWC6mWyrP.txt", "test_tC1Yc87DfC.txt"}, // multiple delete with modifier
"file_many.300_WlbFWSGmW9.png": nil, // delete by filename "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.text": "test123",
"a.b.unknown": "test456", "a.b.unknown": "test456",
// file fields unset/delete // file fields unset/delete
"a.b.file_one": "", "a.b.file_one-": "test_d61b33QdDU.txt", // delete with modifier
"a.b.file_many.0": "", "a.b.file_many.0": "", // delete by index
"a.b.file_many.300_WlbFWSGmW9.png": "test.png", // delete by name "a.b.file_many-": "test_tC1Yc87DfC.txt", // delete with modifier
"a.b.file_many.1": "test.png", // should be ignored "a.b.file_many.300_WlbFWSGmW9.png": "", // delete by filename
}, "file_many") "a.b.file_many.2": "test.png", // should be ignored
}, "a.b.file_many")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -191,7 +193,7 @@ func TestRecordUpsertLoadRequestMultipart(t *testing.T) {
t.Fatal("Expect file_many field to be set") t.Fatal("Expect file_many field to be set")
} }
manyfilesRemains := len(list.ToUniqueStringSlice(fileMany)) 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 { if manyfilesRemains != expectedRemains {
t.Fatalf("Expect file_many to be %d, got %d (%v)", expectedRemains, manyfilesRemains, fileMany) 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" { if v := recordAfter.Get("email"); v == "invalid" {
t.Fatalf("Expected record.email not to change, got %v", v) 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) 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")) fileMany := (recordAfter.GetStringSlice("file_many"))
if len(fileMany) != 4 { // 1 replace + 1 new if len(fileMany) != 6 { // 1 replace + 1 new
t.Fatalf("Expected 4 record.file_many, got %d (%v)", len(fileMany), fileMany) t.Fatalf("Expected 6 record.file_many, got %d (%v)", len(fileMany), fileMany)
} }
for _, f := range fileMany { for _, f := range fileMany {
if !hasRecordFile(app, recordAfter, f) { if !hasRecordFile(app, recordAfter, f) {
@ -1009,7 +1011,7 @@ func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
} }
fileMany := recordAfter.GetStringSlice("file_many") fileMany := recordAfter.GetStringSlice("file_many")
if len(fileMany) != 3 { if len(fileMany) != 5 {
t.Fatalf("Expected file_many to be 3, got %v", fileMany) t.Fatalf("Expected file_many to be 5, got %v", fileMany)
} }
} }

42
go.mod
View File

@ -4,14 +4,14 @@ go 1.18
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.6 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.174
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.3.4 github.com/domodwyer/mailyak/v3 v3.3.4
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86 github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86
github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6 github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/gabriel-vasile/mimetype v1.4.1 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/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-jwt/jwt/v4 v4.4.3 github.com/golang-jwt/jwt/v4 v4.4.3
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
@ -20,21 +20,21 @@ require (
github.com/spf13/cast v1.5.0 github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
gocloud.dev v0.27.0 gocloud.dev v0.27.0
golang.org/x/crypto v0.4.0 golang.org/x/crypto v0.5.0
golang.org/x/net v0.4.0 golang.org/x/net v0.5.0
golang.org/x/oauth2 v0.3.0 golang.org/x/oauth2 v0.4.0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
modernc.org/sqlite v1.20.0 modernc.org/sqlite v1.20.1
) )
require ( require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 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 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/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/config v1.18.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.7 // 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/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/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/endpoints/v2 v2.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // 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/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/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/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/s3 v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.28 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 // 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/aws/smithy-go v1.13.5 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
@ -60,24 +60,24 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // 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-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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
go.opencensus.io v0.24.0 // 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/mod v0.7.0 // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sys v0.4.0 // indirect
golang.org/x/term v0.3.0 // indirect golang.org/x/term v0.4.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.6.0 // indirect
golang.org/x/time v0.3.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 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/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf // indirect
google.golang.org/grpc v1.51.0 // indirect google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect

53
go.sum
View File

@ -48,7 +48,9 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU= cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k= cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
@ -206,8 +208,10 @@ github.com/aws/aws-sdk-go v1.43.11/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.68/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.68/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.165 h1:yaeKEU28EiSCp1T5XXinVA/qx9JFGbVZGUmj5COAMXI= github.com/aws/aws-sdk-go v1.44.167 h1:kQmBhGdZkQLU7AiHShSkBJ15zr8agy0QeaxXduvyp2E=
github.com/aws/aws-sdk-go v1.44.165/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.167/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.174 h1:9lR4a6MKQW/t6YCG0ZKAt1GAkjdEPP8sWch/pfcuR0c=
github.com/aws/aws-sdk-go v1.44.174/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw= github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY= github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY=
@ -218,15 +222,21 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5
github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E= github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
github.com/aws/aws-sdk-go-v2/config v1.18.7 h1:V94lTcix6jouwmAsgQMAEBozVAGJMFhVj+6/++xfe3E= github.com/aws/aws-sdk-go-v2/config v1.18.7 h1:V94lTcix6jouwmAsgQMAEBozVAGJMFhVj+6/++xfe3E=
github.com/aws/aws-sdk-go-v2/config v1.18.7/go.mod h1:OZYsyHFL5PB9UpyS78NElgKs11qI/B5KJau2XOJDXHA= github.com/aws/aws-sdk-go-v2/config v1.18.7/go.mod h1:OZYsyHFL5PB9UpyS78NElgKs11qI/B5KJau2XOJDXHA=
github.com/aws/aws-sdk-go-v2/config v1.18.8 h1:lDpy0WM8AHsywOnVrOHaSMfpaiV2igOw8D7svkFkXVA=
github.com/aws/aws-sdk-go-v2/config v1.18.8/go.mod h1:5XCmmyutmzzgkpk/6NYTjeWb6lgo9N170m1j6pQkIBs=
github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk= github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.7 h1:qUUcNS5Z1092XBFT66IJM7mYkMwgZ8fcC8YDIbEwXck= github.com/aws/aws-sdk-go-v2/credentials v1.13.7 h1:qUUcNS5Z1092XBFT66IJM7mYkMwgZ8fcC8YDIbEwXck=
github.com/aws/aws-sdk-go-v2/credentials v1.13.7/go.mod h1:AdCcbZXHQCjJh6NaH3pFaw8LUeBFn5+88BZGMVGuBT8= github.com/aws/aws-sdk-go-v2/credentials v1.13.7/go.mod h1:AdCcbZXHQCjJh6NaH3pFaw8LUeBFn5+88BZGMVGuBT8=
github.com/aws/aws-sdk-go-v2/credentials v1.13.8 h1:vTrwTvv5qAwjWIGhZDSBH/oQHuIQjGmD232k01FUh6A=
github.com/aws/aws-sdk-go-v2/credentials v1.13.8/go.mod h1:lVa4OHbvgjVot4gmh1uouF1ubgexSCN92P6CJQpT0t8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46 h1:OCX1pQ4pcqhsDV7B92HzdLWjHWOQsILvjLinpaUWhcc= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46 h1:OCX1pQ4pcqhsDV7B92HzdLWjHWOQsILvjLinpaUWhcc=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46/go.mod h1:MxCBOcyNXGJRvfpPiH+L6n/BF9zbowthGSUZdDvQF/c= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46/go.mod h1:MxCBOcyNXGJRvfpPiH+L6n/BF9zbowthGSUZdDvQF/c=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.47 h1:E884ndKWVGt8IhtUuGhXbEsmaCvdAAkTTUDu7uAok1g=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.47/go.mod h1:KybsEsmXLO0u75FyS3F0sY4OQ97syDe8z+ISq8oEczA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI=
@ -255,6 +265,8 @@ github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKa
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo= github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.0 h1:wddsyuESfviaiXk3w9N6/4iRwTg/a3gktjODY6jYQBo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.0/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM= github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU= github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
@ -262,11 +274,17 @@ github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPv
github.com/aws/aws-sdk-go-v2/service/sso v1.11.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU= github.com/aws/aws-sdk-go-v2/service/sso v1.11.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.28 h1:gItLq3zBYyRDPmqAClgzTH8PBjDQGeyptYGHIwtYYNA= github.com/aws/aws-sdk-go-v2/service/sso v1.11.28 h1:gItLq3zBYyRDPmqAClgzTH8PBjDQGeyptYGHIwtYYNA=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.28/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A= github.com/aws/aws-sdk-go-v2/service/sso v1.11.28/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 h1:/2gzjhQowRLarkkBOGPXSRnb8sQ2RVsjdG1C/UliK/c=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.0/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11 h1:KCacyVSs/wlcPGx37hcbT3IGYO8P8Jx+TgSDhAXtQMY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11 h1:KCacyVSs/wlcPGx37hcbT3IGYO8P8Jx+TgSDhAXtQMY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.11/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 h1:Jfly6mRxk2ZOSlbCvZfKNS7TukSx1mIzhSsqZ/IGSZI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E= github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 h1:9Mtq1KM6nD8/+HStvWcvYnixJ5N85DX+P+OY3kI3W2k= github.com/aws/aws-sdk-go-v2/service/sts v1.17.7 h1:9Mtq1KM6nD8/+HStvWcvYnixJ5N85DX+P+OY3kI3W2k=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.7/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I= github.com/aws/aws-sdk-go-v2/service/sts v1.17.7/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 h1:kOO++CYo50RcTFISESluhWEi5Prhg+gaSs4whWabiZU=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.0/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I=
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
@ -564,8 +582,10 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/ganigeorgiev/fexpr v0.1.1 h1:La9kYEgTcIutvOnqNZ8pOUD0O0Q/Gn15sTVEX+IeBE8= github.com/ganigeorgiev/fexpr v0.2.0 h1:j9ve0F32ENz2LKa7+5k7mf0Uzhng0L4Qsf0IwA4UHg0=
github.com/ganigeorgiev/fexpr v0.1.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= github.com/ganigeorgiev/fexpr v0.2.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/ganigeorgiev/fexpr v0.3.0 h1:RwSyJBME+g/XdzlUW0paH/4VXhLHPg+rErtLeC7K8Ew=
github.com/ganigeorgiev/fexpr v0.3.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
@ -806,6 +826,7 @@ github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@ -1061,6 +1082,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@ -1563,6 +1586,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -1578,6 +1603,8 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ= golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ=
golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI= golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI=
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1687,6 +1714,8 @@ golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfS
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1714,6 +1743,8 @@ golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7Lm
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1875,6 +1906,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -1884,6 +1917,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1896,6 +1931,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1995,6 +2032,8 @@ golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -2056,6 +2095,8 @@ google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOI
google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8= google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI= google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI=
google.golang.org/api v0.106.0 h1:ffmW0faWCwKkpbbtvlY/K/8fUl+JKvNS5CVzRoyfCv8=
google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -2166,6 +2207,8 @@ google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljW
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70= google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf h1:/JqRexUvugu6JURQ0O7RfV1EnvgrOxUV4tSjuAv0Sr0=
google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -2355,6 +2398,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY= modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw= modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/sqlite v1.20.1 h1:z6qRLw72B0VfRrJjs3l6hWkzYDx1bo0WGVrBGP4ohhM=
modernc.org/sqlite v1.20.1/go.mod h1:fODt+bFmc/j8LcoCbMSkAuKuGmhxjG45KGc25N2705M=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34= modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=

View File

@ -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, `"`) ||
strings.HasPrefix(literal, `'`) ||
strings.HasPrefix(literal, "@request.method") ||
strings.HasPrefix(literal, "@request.data") ||
strings.HasPrefix(literal, "@request.query") {
return false
}
if strings.HasPrefix(literal, "@collection.") {
return true
}
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
})
}

View File

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"regexp"
"strconv"
"time" "time"
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
@ -57,7 +59,7 @@ func nullStringMapValue(data dbx.NullStringMap, key string) any {
// NewRecordFromNullStringMap initializes a single new Record model // NewRecordFromNullStringMap initializes a single new Record model
// with data loaded from the provided NullStringMap. // with data loaded from the provided NullStringMap.
func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record { func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record {
resultMap := map[string]any{} resultMap := make(map[string]any, len(data))
// load schema fields // load schema fields
for _, field := range collection.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). // 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) { func (m *Record) MergeExpand(expand map[string]any) {
if m.expand == nil && len(expand) > 0 { 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 { 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 m.expand[key] = oldSlice
} else { } else {
m.expand[key] = oldSlice[0] 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. // SchemaData returns a shallow copy ONLY of the defined record schema fields data.
func (m *Record) SchemaData() map[string]any { 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() { for _, field := range m.collection.Schema.Fields() {
if v, ok := m.data[field.Name]; ok { if v, ok := m.data[field.Name]; ok {
@ -370,7 +372,7 @@ func (m *Record) Load(data map[string]any) {
// ColumnValueMap implements [ColumnValueMapper] interface. // ColumnValueMap implements [ColumnValueMapper] interface.
func (m *Record) ColumnValueMap() map[string]any { 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 // export schema field values
for _, field := range m.collection.Schema.Fields() { 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. // Fields marked as hidden will be exported only if `m.IgnoreEmailVisibility(true)` is set.
func (m *Record) PublicExport() map[string]any { 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 // export unknown data fields if allowed
if m.exportUnknown { if m.exportUnknown {
@ -457,6 +459,101 @@ func (m *Record) UnmarshalJSON(data []byte) error {
return nil 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. // getNormalizeDataValueForDB returns the "key" data value formatted for db storage.
func (m *Record) getNormalizeDataValueForDB(key string) any { func (m *Record) getNormalizeDataValueForDB(key string) any {
var val any var val any

View File

@ -138,7 +138,7 @@ func TestNewRecordFromNullStringMap(t *testing.T) {
Valid: true, Valid: true,
}, },
"field5": sql.NullString{ "field5": sql.NullString{
String: `["test1","test2"]`, // will select only the first elem String: `["test1","test2"]`, // will select only the last elem
Valid: true, Valid: true,
}, },
"field6": sql.NullString{ "field6": sql.NullString{
@ -157,11 +157,11 @@ func TestNewRecordFromNullStringMap(t *testing.T) {
}{ }{
{ {
models.CollectionTypeBase, 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, 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: // Auth helpers:
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -1,5 +1,11 @@
package models package models
import (
"strings"
"github.com/pocketbase/pocketbase/models/schema"
)
// RequestData defines a HTTP request data struct, usually used // RequestData defines a HTTP request data struct, usually used
// as part of the `@request.*` filter resolver. // as part of the `@request.*` filter resolver.
type RequestData struct { type RequestData struct {
@ -9,3 +15,18 @@ type RequestData struct {
AuthRecord *Record `json:"authRecord"` AuthRecord *Record `json:"authRecord"`
Admin *Admin `json:"admin"` 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
}

View File

@ -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)
}
}
}

View File

@ -15,22 +15,36 @@ import (
var schemaFieldNameRegex = regexp.MustCompile(`^\w+$`) 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 // commonly used field names
const ( const (
FieldNameId = "id" FieldNameId string = "id"
FieldNameCreated = "created" FieldNameCreated string = "created"
FieldNameUpdated = "updated" FieldNameUpdated string = "updated"
FieldNameCollectionId = "collectionId" FieldNameCollectionId string = "collectionId"
FieldNameCollectionName = "collectionName" FieldNameCollectionName string = "collectionName"
FieldNameExpand = "expand" FieldNameExpand string = "expand"
FieldNameUsername = "username" FieldNameUsername string = "username"
FieldNameEmail = "email" FieldNameEmail string = "email"
FieldNameEmailVisibility = "emailVisibility" FieldNameEmailVisibility string = "emailVisibility"
FieldNameVerified = "verified" FieldNameVerified string = "verified"
FieldNameTokenKey = "tokenKey" FieldNameTokenKey string = "tokenKey"
FieldNamePasswordHash = "passwordHash" FieldNamePasswordHash string = "passwordHash"
FieldNameLastResetSentAt = "lastResetSentAt" FieldNameLastResetSentAt string = "lastResetSentAt"
FieldNameLastVerificationSentAt = "lastVerificationSentAt" FieldNameLastVerificationSentAt string = "lastVerificationSentAt"
) )
// BaseModelFieldNames returns the field names that all models have (id, created, updated). // BaseModelFieldNames returns the field names that all models have (id, created, updated).
@ -168,8 +182,8 @@ func (f SchemaField) Validate() error {
f.InitOptions() f.InitOptions()
excludeNames := BaseModelFieldNames() excludeNames := BaseModelFieldNames()
// exclude filter literals // exclude special filter literals
excludeNames = append(excludeNames, "null", "true", "false") excludeNames = append(excludeNames, "null", "true", "false", "isset")
// exclude system literals // exclude system literals
excludeNames = append(excludeNames, SystemFieldNames()...) excludeNames = append(excludeNames, SystemFieldNames()...)
@ -276,7 +290,7 @@ func (f *SchemaField) PrepareValue(value any) any {
options, _ := f.Options.(*SelectOptions) options, _ := f.Options.(*SelectOptions)
if options.MaxSelect <= 1 { if options.MaxSelect <= 1 {
if len(val) > 0 { if len(val) > 0 {
return val[0] return val[len(val)-1] // the last selected
} }
return "" return ""
} }
@ -288,7 +302,7 @@ func (f *SchemaField) PrepareValue(value any) any {
options, _ := f.Options.(*FileOptions) options, _ := f.Options.(*FileOptions)
if options.MaxSelect <= 1 { if options.MaxSelect <= 1 {
if len(val) > 0 { if len(val) > 0 {
return val[0] return val[len(val)-1] // the last selected
} }
return "" return ""
} }
@ -300,7 +314,7 @@ func (f *SchemaField) PrepareValue(value any) any {
options, _ := f.Options.(*RelationOptions) options, _ := f.Options.(*RelationOptions)
if options.MaxSelect != nil && *options.MaxSelect <= 1 { if options.MaxSelect != nil && *options.MaxSelect <= 1 {
if len(ids) > 0 { if len(ids) > 0 {
return ids[0] return ids[len(ids)-1] // the last selected
} }
return "" 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. // FieldOptions interfaces that defines common methods that every field options struct has.

View File

@ -603,7 +603,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{schema.SchemaField{Type: schema.FieldTypeSelect}, "", `""`}, {schema.SchemaField{Type: schema.FieldTypeSelect}, "", `""`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`}, {schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`},
{schema.SchemaField{Type: schema.FieldTypeSelect}, "test", `"test"`}, {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 // no values validation/filtering
schema.SchemaField{ schema.SchemaField{
@ -680,7 +680,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{schema.SchemaField{Type: schema.FieldTypeFile}, "", `""`}, {schema.SchemaField{Type: schema.FieldTypeFile}, "", `""`},
{schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`}, {schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`},
{schema.SchemaField{Type: schema.FieldTypeFile}, "test", `"test"`}, {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) // file (multiple)
{ {
schema.SchemaField{ schema.SchemaField{
@ -785,7 +785,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
{ {
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}}, schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`, `"2ba88b4f-e9da-42f0-9764-9a55c953e724"`,
}, },
// relation (multiple) // 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 { type fieldOptionsScenario struct {
@ -1306,7 +1996,7 @@ func TestRelationOptionsValidate(t *testing.T) {
[]string{"maxSelect"}, []string{"maxSelect"},
}, },
{ {
"MaxSelect > 0 && non-empty CollectionId", "MaxSelect > 0 && nonempty CollectionId",
schema.RelationOptions{ schema.RelationOptions{
CollectionId: "abc", CollectionId: "abc",
MaxSelect: types.Pointer(1), MaxSelect: types.Pointer(1),

View File

@ -1,8 +1,10 @@
package jsvm package jsvm
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/dop251/goja_nodejs/console" "github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/require" "github.com/dop251/goja_nodejs/require"
@ -75,7 +77,7 @@ func RegisterMigrations(app core.App, options *MigrationsOptions) error {
_, err := vm.RunString(string(content)) _, err := vm.RunString(string(content))
if err != nil { 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{} result := map[string][]byte{}
for _, f := range files { for _, f := range files {
if f.IsDir() { if f.IsDir() || !strings.HasSuffix(f.Name(), ".js") {
continue continue // not a .js file
} }
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name())) raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
if err != nil { if err != nil {

View File

@ -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"),
)
}

View File

@ -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
}

View File

@ -10,18 +10,20 @@ import (
"github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema" "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/search"
"github.com/pocketbase/pocketbase/tools/security" "github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
// ensure that `search.FieldResolver` interface is implemented // filter modifiers
var _ search.FieldResolver = (*RecordFieldResolver)(nil) const (
eachModifier string = "each"
issetModifier string = "isset"
lengthModifier string = "length"
)
// list of auth filter fields that don't require join with the auth // 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{ var plainRequestAuthFields = []string{
"@request.auth." + schema.FieldNameId, "@request.auth." + schema.FieldNameId,
"@request.auth." + schema.FieldNameCollectionId, "@request.auth." + schema.FieldNameCollectionId,
@ -34,32 +36,28 @@ var plainRequestAuthFields = []string{
"@request.auth." + schema.FieldNameUpdated, "@request.auth." + schema.FieldNameUpdated,
} }
type join struct { // ensure that `search.FieldResolver` interface is implemented
id string var _ search.FieldResolver = (*RecordFieldResolver)(nil)
table string
on dbx.Expression
}
// RecordFieldResolver defines a custom search resolver struct for // RecordFieldResolver defines a custom search resolver struct for
// managing Record model search fields. // managing Record model search fields.
// //
// Usually used together with `search.Provider`. Example: // Usually used together with `search.Provider`. Example:
// resolver := resolvers.NewRecordFieldResolver( // resolver := resolvers.NewRecordFieldResolver(
// app.Dao(), // app.Dao(),
// myCollection, // myCollection,
// &models.RequestData{...}, // &models.RequestData{...},
// true, // true,
// ) // )
// provider := search.NewProvider(resolver) // provider := search.NewProvider(resolver)
// ... // ...
type RecordFieldResolver struct { type RecordFieldResolver struct {
dao *daos.Dao dao *daos.Dao
baseCollection *models.Collection baseCollection *models.Collection
allowHiddenFields bool allowHiddenFields bool
allowedFields []string allowedFields []string
loadedCollections []*models.Collection loadedCollections []*models.Collection
joins []join // we cannot use a map because the insertion order is not preserved joins []*join // we cannot use a map because the insertion order is not preserved
exprs []dbx.Expression
requestData *models.RequestData requestData *models.RequestData
staticRequestData map[string]any staticRequestData map[string]any
} }
@ -76,20 +74,18 @@ func NewRecordFieldResolver(
baseCollection: baseCollection, baseCollection: baseCollection,
requestData: requestData, requestData: requestData,
allowHiddenFields: allowHiddenFields, allowHiddenFields: allowHiddenFields,
joins: []join{}, joins: []*join{},
exprs: []dbx.Expression{},
loadedCollections: []*models.Collection{baseCollection}, loadedCollections: []*models.Collection{baseCollection},
allowedFields: []string{ allowedFields: []string{
`^\w+[\w\.]*$`, `^\w+[\w\.\:]*$`,
`^\@request\.method$`, `^\@request\.method$`,
`^\@request\.auth\.\w+[\w\.]*$`, `^\@request\.auth\.[\w\.\:]*\w+$`,
`^\@request\.data\.\w+[\w\.]*$`, `^\@request\.data\.[\w\.\:]*\w+$`,
`^\@request\.query\.\w+[\w\.]*$`, `^\@request\.query\.[\w\.\:]*\w+$`,
`^\@collection\.\w+\.\w+[\w\.]*$`, `^\@collection\.\w+\.[\w\.\:]*\w+$`,
}, },
} }
// @todo remove after IN operator and multi-match filter enhancements
r.staticRequestData = map[string]any{} r.staticRequestData = map[string]any{}
if r.requestData != nil { if r.requestData != nil {
r.staticRequestData["method"] = r.requestData.Method r.staticRequestData["method"] = r.requestData.Method
@ -115,13 +111,10 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
query.Distinct(true) query.Distinct(true)
for _, join := range r.joins { for _, join := range r.joins {
query.LeftJoin(join.table, join.on) query.LeftJoin(
} (join.tableName + " " + join.tableAlias),
} join.on,
)
for _, expr := range r.exprs {
if expr != nil {
query.AndWhere(expr)
} }
} }
@ -130,225 +123,63 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
// Resolve implements `search.FieldResolver` interface. // Resolve implements `search.FieldResolver` interface.
// //
// Example of resolvable field formats: // Example of some resolvable fieldName formats:
// id //
// project.screen.status // id
// @request.status // someSelect.each
// @request.auth.someRelation.name // project.screen.status
// @collection.product.name // @request.status
func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, placeholderParams dbx.Params, err error) { // @request.query.filter
if len(r.allowedFields) > 0 && !list.ExistInSliceWithRegex(fieldName, r.allowedFields) { // @request.auth.someRelation.name
return "", nil, fmt.Errorf("Failed to resolve field %q", fieldName) // @request.data.someRelation.name
} // @request.data.someField
// @request.data.someSelect:each
props := strings.Split(fieldName, ".") // @request.data.someField:isset
// @collection.product.name
currentCollectionName := r.baseCollection.Name func (r *RecordFieldResolver) Resolve(fieldName string) (*search.ResolverResult, error) {
currentTableAlias := inflector.Columnify(currentCollectionName) return parseAndRun(fieldName, r)
// 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)
} }
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) { func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search.ResolverResult, error) {
// ignore error because requestData is dynamic and some of the if len(path) == 0 {
// lookup keys may not be defined for the request return nil, fmt.Errorf("at least one path key should be provided")
resultVal, _ := extractNestedMapVal(r.staticRequestData, path...) }
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) { switch v := resultVal.(type) {
case nil: case nil:
return "NULL", nil, nil return &search.ResolverResult{Identifier: "NULL"}, nil
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: 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... // no further processing is needed...
default: default:
// non-plain value // non-plain value
@ -367,33 +198,11 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (resultN
} }
placeholder := "f" + security.PseudorandomString(5) placeholder := "f" + security.PseudorandomString(5)
name := fmt.Sprintf("{:%s}", placeholder)
params := dbx.Params{placeholder: resultVal}
return name, params, nil return &search.ResolverResult{
} Identifier: "{:" + placeholder + "}",
Params: dbx.Params{placeholder: resultVal},
func extractNestedMapVal(m map[string]any, keys ...string) (result any, err error) { }, nil
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:]...)
} }
func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models.Collection, error) { 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) { func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) {
tableExpr := (tableName + " " + tableAlias) join := &join{
tableName: tableName,
join := join{ tableAlias: tableAlias,
id: tableAlias, on: on,
table: tableExpr,
on: on,
} }
// replace existing join // replace existing join
for i, j := range r.joins { for i, j := range r.joins {
if j.id == join.id { if j.tableAlias == join.tableAlias {
r.joins[i] = join r.joins[i] = join
return return
} }
@ -435,6 +242,44 @@ func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string,
r.joins = append(r.joins, join) r.joins = append(r.joins, join)
} }
func (r *RecordFieldResolver) registerExpr(expr dbx.Expression) { func extractNestedMapVal(m map[string]any, keys ...string) (any, error) {
r.exprs = append(r.exprs, expr) 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)
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -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=="}

View File

@ -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=="}

View File

@ -9,13 +9,14 @@ import (
// AuthUser defines a standardized oauth2 user data structure. // AuthUser defines a standardized oauth2 user data structure.
type AuthUser struct { type AuthUser struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"` AvatarUrl string `json:"avatarUrl"`
RawUser map[string]any `json:"rawUser"` RawUser map[string]any `json:"rawUser"`
AccessToken string `json:"accessToken"` AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
} }
// Provider defines a common interface for an OAuth2 client. // Provider defines a common interface for an OAuth2 client.

View File

@ -63,12 +63,13 @@ func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
username := fmt.Sprintf("%s#%s", extracted.Username, extracted.Discriminator) username := fmt.Sprintf("%s#%s", extracted.Username, extracted.Discriminator)
user := &AuthUser{ user := &AuthUser{
Id: extracted.Id, Id: extracted.Id,
Name: username, Name: username,
Username: extracted.Username, Username: extracted.Username,
AvatarUrl: avatarUrl, AvatarUrl: avatarUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
if extracted.Verified { if extracted.Verified {
user.Email = extracted.Email user.Email = extracted.Email

View File

@ -54,12 +54,13 @@ func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: extracted.Id, Id: extracted.Id,
Name: extracted.Name, Name: extracted.Name,
Email: extracted.Email, Email: extracted.Email,
AvatarUrl: extracted.Picture.Data.Url, AvatarUrl: extracted.Picture.Data.Url,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -55,12 +55,13 @@ func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(extracted.Id), Id: strconv.Itoa(extracted.Id),
Name: extracted.Name, Name: extracted.Name,
Username: extracted.Login, Username: extracted.Login,
AvatarUrl: extracted.AvatarUrl, AvatarUrl: extracted.AvatarUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
if extracted.Email != "" && is.EmailFormat.Validate(extracted.Email) == nil { if extracted.Email != "" && is.EmailFormat.Validate(extracted.Email) == nil {

View File

@ -55,13 +55,14 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(extracted.Id), Id: strconv.Itoa(extracted.Id),
Name: extracted.Name, Name: extracted.Name,
Username: extracted.Login, Username: extracted.Login,
Email: extracted.Email, Email: extracted.Email,
AvatarUrl: extracted.AvatarUrl, AvatarUrl: extracted.AvatarUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
// in case user has set "Keep my email address private", send an // in case user has set "Keep my email address private", send an

View File

@ -53,13 +53,14 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(extracted.Id), Id: strconv.Itoa(extracted.Id),
Name: extracted.Name, Name: extracted.Name,
Username: extracted.Username, Username: extracted.Username,
Email: extracted.Email, Email: extracted.Email,
AvatarUrl: extracted.AvatarUrl, AvatarUrl: extracted.AvatarUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -52,12 +52,13 @@ func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: extracted.Id, Id: extracted.Id,
Name: extracted.Name, Name: extracted.Name,
Email: extracted.Email, Email: extracted.Email,
AvatarUrl: extracted.Picture, AvatarUrl: extracted.Picture,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -59,11 +59,12 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(extracted.Id), Id: strconv.Itoa(extracted.Id),
Username: extracted.Profile.Nickname, Username: extracted.Profile.Nickname,
AvatarUrl: extracted.Profile.ImageUrl, AvatarUrl: extracted.Profile.ImageUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
if extracted.KakaoAccount.IsEmailValid && extracted.KakaoAccount.IsEmailVerified { if extracted.KakaoAccount.IsEmailValid && extracted.KakaoAccount.IsEmailVerified {
user.Email = extracted.KakaoAccount.Email user.Email = extracted.KakaoAccount.Email

View File

@ -53,11 +53,12 @@ func (p *Microsoft) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: extracted.Id, Id: extracted.Id,
Name: extracted.Name, Name: extracted.Name,
Email: extracted.Email, Email: extracted.Email,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -61,10 +61,11 @@ func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: extracted.Id, Id: extracted.Id,
Name: extracted.Name, Name: extracted.Name,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
if len(extracted.Images) > 0 { if len(extracted.Images) > 0 {
user.AvatarUrl = extracted.Images[0].Url user.AvatarUrl = extracted.Images[0].Url

View File

@ -58,12 +58,13 @@ func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: strconv.Itoa(extracted.Id), Id: strconv.Itoa(extracted.Id),
Name: extracted.FirstName + " " + extracted.LastName, Name: extracted.FirstName + " " + extracted.LastName,
Username: extracted.Username, Username: extracted.Username,
AvatarUrl: extracted.ProfileImageUrl, AvatarUrl: extracted.ProfileImageUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -61,13 +61,14 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: extracted.Data[0].Id, Id: extracted.Data[0].Id,
Name: extracted.Data[0].DisplayName, Name: extracted.Data[0].DisplayName,
Username: extracted.Data[0].Login, Username: extracted.Data[0].Login,
Email: extracted.Data[0].Email, Email: extracted.Data[0].Email,
AvatarUrl: extracted.Data[0].ProfileImageUrl, AvatarUrl: extracted.Data[0].ProfileImageUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -63,12 +63,13 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
user := &AuthUser{ user := &AuthUser{
Id: extracted.Data.Id, Id: extracted.Data.Id,
Name: extracted.Data.Name, Name: extracted.Data.Name,
Username: extracted.Data.Username, Username: extracted.Data.Username,
AvatarUrl: extracted.Data.ProfileImageUrl, AvatarUrl: extracted.Data.ProfileImageUrl,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
} }
return user, nil return user, nil

View File

@ -10,6 +10,20 @@ import (
var cachedPatterns = map[string]*regexp.Regexp{} 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. // ExistInSlice checks whether a comparable element exists in a slice of the same type.
func ExistInSlice[T comparable](item T, list []T) bool { func ExistInSlice[T comparable](item T, list []T) bool {
if len(list) == 0 { if len(list) == 0 {

View File

@ -1,12 +1,111 @@
package list_test package list_test
import ( import (
"encoding/json"
"testing" "testing"
"github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/types" "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) { func TestExistInSliceString(t *testing.T) {
scenarios := []struct { scenarios := []struct {
item string item string

View File

@ -2,6 +2,7 @@ package migrate
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/AlecAivazis/survey/v2" "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 confirm := false
prompt := &survey.Confirm{ 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) survey.AskOne(prompt, &confirm)
if !confirm { if !confirm {
@ -138,38 +149,43 @@ func (r *Runner) Up() ([]string, error) {
return applied, nil 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. // On success returns list with the reverted migrations file names.
func (r *Runner) Down(toRevertCount int) ([]string, error) { func (r *Runner) Down(toRevertCount int) ([]string, error) {
reverted := make([]string, 0, toRevertCount) 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 { err := r.db.Transactional(func(tx *dbx.Tx) error {
for i := len(r.migrationsList.Items()) - 1; i >= 0; i-- { for _, name := range names {
m := r.migrationsList.Item(i) for _, m := range r.migrationsList.Items() {
if m.File != name {
// skip unapplied continue
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)
} }
}
if err := r.saveRevertedMigration(tx, m.File); err != nil { // revert limit reached
return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.File, err) 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 return nil
@ -178,6 +194,7 @@ func (r *Runner) Down(toRevertCount int) ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return reverted, nil 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 { func (r *Runner) saveAppliedMigration(tx dbx.Builder, file string) error {
_, err := tx.Insert(r.tableName, dbx.Params{ _, err := tx.Insert(r.tableName, dbx.Params{
"file": file, "file": file,
"applied": time.Now().Unix(), "applied": time.Now().UnixMicro(),
}).Execute() }).Execute()
return err return err
@ -218,3 +235,20 @@ func (r *Runner) saveRevertedMigration(tx dbx.Builder, file string) error {
return err 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
}

View File

@ -3,6 +3,7 @@ package migrate
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"testing" "testing"
"time" "time"
@ -52,73 +53,88 @@ func TestRunnerUpAndDown(t *testing.T) {
} }
defer testDB.Close() defer testDB.Close()
var test1UpCalled bool callsOrder := []string{}
var test1DownCalled bool
var test2UpCalled bool
var test2DownCalled bool
l := MigrationsList{} l := MigrationsList{}
l.Register(func(db dbx.Builder) error { l.Register(func(db dbx.Builder) error {
test1UpCalled = true callsOrder = append(callsOrder, "up2")
return nil return nil
}, func(db dbx.Builder) error { }, func(db dbx.Builder) error {
test1DownCalled = true callsOrder = append(callsOrder, "down2")
return nil
}, "1_test")
l.Register(func(db dbx.Builder) error {
test2UpCalled = true
return nil
}, func(db dbx.Builder) error {
test2DownCalled = true
return nil return nil
}, "2_test") }, "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) r, err := NewRunner(testDB.DB, l)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// simulate partially run migration // simulate partially out-of-order run migration
r.saveAppliedMigration(testDB, r.migrationsList.Item(0).File) r.saveAppliedMigration(testDB, "2_test")
// ---------------------------------------------------------------
// Up() // Up()
// --- // ---------------------------------------------------------------
if _, err := r.Up(); err != nil { if _, err := r.Up(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if test1UpCalled { expectedUpCallsOrder := `["up1","up3"]` // skip up2 since it was applied previously
t.Fatalf("Didn't expect 1_test to be called")
}
if !test2UpCalled { upCallsOrder, err := json.Marshal(callsOrder)
t.Fatalf("Expected 2_test to be called") if err != nil {
}
// 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 {
t.Fatal(err) t.Fatal(err)
} }
if test3DownCalled { if v := string(upCallsOrder); v != expectedUpCallsOrder {
t.Fatal("Didn't expect 3_test to be reverted.") 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 { expectedDownCallsOrder := `["down3","down1"]` // revert in the applied order
t.Fatal("Didn't expect 1_test to be reverted.")
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)
} }
} }

View File

@ -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) { func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldResolver) (dbx.Expression, error) {
lName, lParams, lErr := f.resolveToken(expr.Left, fieldResolver) lResult, lErr := resolveToken(expr.Left, fieldResolver)
if lName == "" || lErr != nil { if lErr != nil || lResult.Identifier == "" {
return nil, fmt.Errorf("Invalid left operand %q - %v.", expr.Left.Literal, lErr) return nil, fmt.Errorf("invalid left operand %q - %v", expr.Left.Literal, lErr)
} }
rName, rParams, rErr := f.resolveToken(expr.Right, fieldResolver) rResult, rErr := resolveToken(expr.Right, fieldResolver)
if rName == "" || rErr != nil { if rErr != nil || rResult.Identifier == "" {
return nil, fmt.Errorf("Invalid right operand %q - %v.", expr.Right.Literal, rErr) return nil, fmt.Errorf("invalid right operand %q - %v", expr.Right.Literal, rErr)
} }
switch expr.Op { return buildExpr(lResult, expr.Op, rResult)
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)
} }
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 { switch token.Type {
case fexpr.TokenIdentifier: case fexpr.TokenIdentifier:
// current datetime constant // current datetime constant
// --- // ---
if token.Literal == "@now" { if token.Literal == "@now" {
placeholder := "t" + security.PseudorandomString(8) placeholder := "t" + security.PseudorandomString(5)
name := fmt.Sprintf("{:%s}", placeholder)
params := dbx.Params{placeholder: types.NowDateTime().String()}
return name, params, nil return &ResolverResult{
Identifier: "{:" + placeholder + "}",
Params: dbx.Params{placeholder: types.NowDateTime().String()},
}, nil
} }
// custom resolver // 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{ m := map[string]string{
// if `null` field is missing, treat `null` identifier as NULL token // if `null` field is missing, treat `null` identifier as NULL token
"null": "NULL", "null": "NULL",
@ -160,27 +207,46 @@ func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver)
"false": "0", "false": "0",
} }
if v, ok := m[strings.ToLower(token.Literal)]; ok { 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: case fexpr.TokenText:
placeholder := "t" + security.PseudorandomString(8) placeholder := "t" + security.PseudorandomString(5)
name := fmt.Sprintf("{:%s}", placeholder)
params := dbx.Params{placeholder: token.Literal}
return name, params, nil return &ResolverResult{
Identifier: "{:" + placeholder + "}",
Params: dbx.Params{placeholder: token.Literal},
}, nil
case fexpr.TokenNumber: case fexpr.TokenNumber:
placeholder := "t" + security.PseudorandomString(8) placeholder := "t" + security.PseudorandomString(5)
name := fmt.Sprintf("{:%s}", placeholder)
params := dbx.Params{placeholder: cast.ToFloat64(token.Literal)}
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 // 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. // opExpr defines an expression that contains a raw sql operator string.
type opExpr struct { type opExpr struct {
op string op string
} }
// Build converts an expression into a SQL fragment. // Build converts the expression into a SQL fragment.
// //
// Implements [dbx.Expression] interface. // Implements [dbx.Expression] interface.
func (e *opExpr) Build(db *dbx.DB, params dbx.Params) string { func (e *opExpr) Build(db *dbx.DB, params dbx.Params) string {
return e.op return e.op
} }
// -------------------------------------------------------------------
var _ dbx.Expression = (*concatExpr)(nil)
// concatExpr defines an expression that concatenates multiple // concatExpr defines an expression that concatenates multiple
// other expressions with a specified separator. // other expressions with a specified separator.
type concatExpr struct { type concatExpr struct {
@ -237,7 +309,7 @@ type concatExpr struct {
separator string separator string
} }
// Build converts an expression into a SQL fragment. // Build converts the expression into a SQL fragment.
// //
// Implements [dbx.Expression] interface. // Implements [dbx.Expression] interface.
func (e *concatExpr) Build(db *dbx.DB, params dbx.Params) string { 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)) stringParts := make([]string, 0, len(e.parts))
for _, a := range e.parts { for _, p := range e.parts {
if a == nil { if p == nil {
continue continue
} }
if sql := a.Build(db, params); sql != "" { if sql := p.Build(db, params); sql != "" {
stringParts = append(stringParts, 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) + ")" 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...)
}
}

View File

@ -495,12 +495,12 @@ func (t *testFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
return nil 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++ t.ResolveCalls++
if field == "unknown" { if field == "unknown" {
return "", nil, errors.New("test error") return nil, errors.New("test error")
} }
return field, nil, nil return &ResolverResult{Identifier: field}, nil
} }

View File

@ -8,6 +8,25 @@ import (
"github.com/pocketbase/pocketbase/tools/list" "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. // FieldResolver defines an interface for managing search fields.
type FieldResolver interface { type FieldResolver interface {
// UpdateQuery allows to updated the provided db query based on the // 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 // Resolve parses the provided field and returns a properly
// formatted db identifier (eg. NULL, quoted column, placeholder parameter, etc.). // 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 // NewSimpleFieldResolver creates a new `SimpleFieldResolver` with the
@ -49,10 +68,12 @@ func (r *SimpleFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
// Resolve implements `search.Resolve` interface. // Resolve implements `search.Resolve` interface.
// //
// Returns error if `field` is not in `r.allowedFields`. // 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) { 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
} }

View File

@ -61,7 +61,7 @@ func TestSimpleFieldResolverResolve(t *testing.T) {
} }
for i, s := range scenarios { for i, s := range scenarios {
name, params, err := r.Resolve(s.fieldName) r, err := r.Resolve(s.fieldName)
hasErr := err != nil hasErr := err != nil
if hasErr != s.expectError { if hasErr != s.expectError {
@ -69,13 +69,17 @@ func TestSimpleFieldResolverResolve(t *testing.T) {
continue continue
} }
if name != s.expectName { if hasErr {
t.Errorf("(%d) Expected name %q, got %q", i, s.expectName, name) continue
}
if r.Identifier != s.expectName {
t.Errorf("(%d) Expected r.Identifier %q, got %q", i, s.expectName, r.Identifier)
} }
// params should be empty // params should be empty
if len(params) != 0 { if len(r.Params) != 0 {
t.Errorf("(%d) Expected 0 params, got %v", i, params) t.Errorf("(%d) Expected 0 r.Params, got %v", i, r.Params)
} }
} }
} }

View File

@ -5,6 +5,8 @@ import (
"strings" "strings"
) )
const randomSortKey string = "@random"
// sort field directions // sort field directions
const ( const (
SortAsc string = "ASC" SortAsc string = "ASC"
@ -19,14 +21,19 @@ type SortField struct {
// BuildExpr resolves the sort field into a valid db sort expression. // BuildExpr resolves the sort field into a valid db sort expression.
func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) { func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) {
name, params, err := fieldResolver.Resolve(s.Name) // special case for random sort
if s.Name == randomSortKey {
// invalidate empty fields and non-column identifiers return "RANDOM()", nil
if err != nil || len(params) > 0 || name == "" || strings.ToLower(name) == "null" {
return "", fmt.Errorf("Invalid sort field %q.", s.Name)
} }
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 // ParseSortFromString parses the provided string expression

View File

@ -27,6 +27,8 @@ func TestSortFieldBuildExpr(t *testing.T) {
{search.SortField{"test1", search.SortAsc}, false, "[[test1]] ASC"}, {search.SortField{"test1", search.SortAsc}, false, "[[test1]] ASC"},
// allowed field - desc // allowed field - desc
{search.SortField{"test1", search.SortDesc}, false, "[[test1]] 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 { for i, s := range scenarios {
@ -54,6 +56,7 @@ func TestParseSortFromString(t *testing.T) {
{"+test", `[{"name":"test","direction":"ASC"}]`}, {"+test", `[{"name":"test","direction":"ASC"}]`},
{"-test", `[{"name":"test","direction":"DESC"}]`}, {"-test", `[{"name":"test","direction":"DESC"}]`},
{"test1,-test2,+test3", `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"DESC"},{"name":"test3","direction":"ASC"}]`}, {"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 { for i, s := range scenarios {

View File

@ -7,4 +7,4 @@ PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling/"
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk" PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk" PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases" PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.10.5" PB_VERSION = "v0.11.0"

View File

@ -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.d939dbbd.js";import{S as Be}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Be}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}'); const pb = new PocketBase('${a[3]}');

View File

@ -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.d939dbbd.js";import{S as Xe}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Xe}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${r[3]}'); const pb = new PocketBase('${r[3]}');

View File

@ -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.d939dbbd.js";import{S as Ze}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Ze}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[3]}'); const pb = new PocketBase('${i[3]}');

View File

@ -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.d939dbbd.js";import{S as Ae}from"./SdkTabs.2a5180be.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&&lt(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)&&lt(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.e8d8151e.js";import{S as Ae}from"./SdkTabs.6909f1b6.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&&lt(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)&&lt(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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${n[6]}'); const pb = new PocketBase('${n[6]}');

File diff suppressed because one or more lines are too long

View File

@ -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.d939dbbd.js";import{S as Ae}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Ae}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}'); const pb = new PocketBase('${o[3]}');

View File

@ -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.d939dbbd.js";import{S as De}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as De}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}'); const pb = new PocketBase('${o[3]}');

View File

@ -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.d939dbbd.js";import{S as Ve}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Ve}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[3]}'); const pb = new PocketBase('${i[3]}');

View File

@ -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.d939dbbd.js";import{S as Nt}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Nt}from"./SdkTabs.6909f1b6.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> <span>username</span></div></td>
<td><span class="label">String</span></td> <td><span class="label">String</span></td>
<td>The username of the auth record. <td>The username of the auth record.

View File

@ -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.d939dbbd.js";import{S as He}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as He}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}'); const pb = new PocketBase('${o[3]}');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
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.d939dbbd.js";import{S as jt}from"./SdkTabs.2a5180be.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 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.e8d8151e.js";import{S as jt}from"./SdkTabs.6909f1b6.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> <code><span class="txt-success">OPERAND</span>
<span class="txt-danger">OPERATOR</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 <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
@ -15,7 +15,7 @@ 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
// fetch a paginated records list // fetch a paginated records list
const resultList = await pb.collection('${(mt=r[0])==null?void 0:mt.name}').getList(1, 50, { 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', filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
}); });
// you can also fetch all records at once via getFullList // you can also fetch all records at once via getFullList
@ -38,7 +38,7 @@ 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
final resultList = await pb.collection('${(_t=r[0])==null?void 0:_t.name}').getList( final resultList = await pb.collection('${(_t=r[0])==null?void 0:_t.name}').getList(
page: 1, page: 1,
perPage: 50, perPage: 50,
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2', filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
); );
// you can also fetch all records at once via getFullList // you can also fetch all records at once via getFullList
@ -82,7 +82,7 @@ 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
// fetch a paginated records list // fetch a paginated records list
const resultList = await pb.collection('${(wt=t[0])==null?void 0:wt.name}').getList(1, 50, { 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', filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
}); });
// you can also fetch all records at once via getFullList // you can also fetch all records at once via getFullList
@ -105,7 +105,7 @@ 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
final resultList = await pb.collection('${(vt=t[0])==null?void 0:vt.name}').getList( final resultList = await pb.collection('${(vt=t[0])==null?void 0:vt.name}').getList(
page: 1, page: 1,
perPage: 50, perPage: 50,
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2', filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
); );
// you can also fetch all records at once via getFullList // you can also fetch all records at once via getFullList

View File

@ -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.d939dbbd.js";import{S as Ne}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Ne}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}'); const pb = new PocketBase('${a[3]}');

View File

@ -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.d939dbbd.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.e8d8151e.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}; `),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};

View File

@ -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.d939dbbd.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.e8d8151e.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}; <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};

View File

@ -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.d939dbbd.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.e8d8151e.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> `),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> <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}; <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};

View File

@ -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.d939dbbd.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.e8d8151e.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> `),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> <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}; <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};

View File

@ -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.d939dbbd.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.e8d8151e.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>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}; <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};

View File

@ -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.d939dbbd.js";import{S as fe}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as fe}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[1]}'); const pb = new PocketBase('${o[1]}');

View File

@ -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.d939dbbd.js";import{S as je}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as je}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}'); const pb = new PocketBase('${o[3]}');

View File

@ -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.d939dbbd.js";import{S as Ue}from"./SdkTabs.2a5180be.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.e8d8151e.js";import{S as Ue}from"./SdkTabs.6909f1b6.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'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}'); const pb = new PocketBase('${a[3]}');

View File

@ -1,4 +1,4 @@
import{S as qe,i as we,s as Pe,e as c,w as h,b as v,c as ve,f as b,g as r,h as i,m as he,x as E,O as me,P as ge,k as ye,Q as Be,n as Ce,t as Z,a as x,o as f,d as $e,R as Se,C as Te,p as Re,r as F,u as Ve,N as Me}from"./index.d939dbbd.js";import{S as Ae}from"./SdkTabs.2a5180be.js";function pe(a,l,s){const o=a.slice();return o[5]=l[s],o}function be(a,l,s){const o=a.slice();return o[5]=l[s],o}function _e(a,l){let s,o=l[5].code+"",_,p,n,d;function m(){return l[4](l[5])}return{key:a,first:null,c(){s=c("button"),_=h(o),p=v(),b(s,"class","tab-item"),F(s,"active",l[1]===l[5].code),this.first=s},m(q,w){r(q,s,w),i(s,_),i(s,p),n||(d=Ve(s,"click",m),n=!0)},p(q,w){l=q,w&4&&o!==(o=l[5].code+"")&&E(_,o),w&6&&F(s,"active",l[1]===l[5].code)},d(q){q&&f(s),n=!1,d()}}}function ke(a,l){let s,o,_,p;return o=new Me({props:{content:l[5].body}}),{key:a,first:null,c(){s=c("div"),ve(o.$$.fragment),_=v(),b(s,"class","tab-item"),F(s,"active",l[1]===l[5].code),this.first=s},m(n,d){r(n,s,d),he(o,s,null),i(s,_),p=!0},p(n,d){l=n;const m={};d&4&&(m.content=l[5].body),o.$set(m),(!p||d&6)&&F(s,"active",l[1]===l[5].code)},i(n){p||(Z(o.$$.fragment,n),p=!0)},o(n){x(o.$$.fragment,n),p=!1},d(n){n&&f(s),$e(o)}}}function Ue(a){var re,fe;let l,s,o=a[0].name+"",_,p,n,d,m,q,w,j=a[0].name+"",L,ee,N,P,Q,C,z,g,D,te,H,S,le,G,I=a[0].name+"",J,se,K,T,W,R,X,V,Y,y,M,$=[],oe=new Map,ae,A,k=[],ie=new Map,B;P=new Ae({props:{js:` import{S as qe,i as we,s as Pe,e as c,w as h,b as v,c as ve,f as b,g as r,h as i,m as he,x as E,O as me,P as ge,k as ye,Q as Be,n as Ce,t as Z,a as x,o as f,d as $e,R as Se,C as Te,p as Re,r as F,u as Ve,N as Me}from"./index.e8d8151e.js";import{S as Ae}from"./SdkTabs.6909f1b6.js";function pe(a,l,s){const o=a.slice();return o[5]=l[s],o}function be(a,l,s){const o=a.slice();return o[5]=l[s],o}function _e(a,l){let s,o=l[5].code+"",_,p,n,d;function m(){return l[4](l[5])}return{key:a,first:null,c(){s=c("button"),_=h(o),p=v(),b(s,"class","tab-item"),F(s,"active",l[1]===l[5].code),this.first=s},m(q,w){r(q,s,w),i(s,_),i(s,p),n||(d=Ve(s,"click",m),n=!0)},p(q,w){l=q,w&4&&o!==(o=l[5].code+"")&&E(_,o),w&6&&F(s,"active",l[1]===l[5].code)},d(q){q&&f(s),n=!1,d()}}}function ke(a,l){let s,o,_,p;return o=new Me({props:{content:l[5].body}}),{key:a,first:null,c(){s=c("div"),ve(o.$$.fragment),_=v(),b(s,"class","tab-item"),F(s,"active",l[1]===l[5].code),this.first=s},m(n,d){r(n,s,d),he(o,s,null),i(s,_),p=!0},p(n,d){l=n;const m={};d&4&&(m.content=l[5].body),o.$set(m),(!p||d&6)&&F(s,"active",l[1]===l[5].code)},i(n){p||(Z(o.$$.fragment,n),p=!0)},o(n){x(o.$$.fragment,n),p=!1},d(n){n&&f(s),$e(o)}}}function Ue(a){var re,fe;let l,s,o=a[0].name+"",_,p,n,d,m,q,w,j=a[0].name+"",L,ee,N,P,Q,C,z,g,D,te,H,S,le,G,I=a[0].name+"",J,se,K,T,W,R,X,V,Y,y,M,$=[],oe=new Map,ae,A,k=[],ie=new Map,B;P=new Ae({props:{js:`
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}'); const pb = new PocketBase('${a[3]}');

View File

@ -1 +1 @@
import{S as q,i as B,s as F,e as v,b as j,f as h,g as y,h as m,O as C,P as J,k as O,Q,n as Y,t as N,a as P,o as w,w as E,r as S,u as z,x as R,N as A,c as G,m as H,d as L}from"./index.d939dbbd.js";function D(o,e,l){const s=o.slice();return s[6]=e[l],s}function K(o,e,l){const s=o.slice();return s[6]=e[l],s}function T(o,e){let l,s,g=e[6].title+"",r,i,n,k;function c(){return e[5](e[6])}return{key:o,first:null,c(){l=v("button"),s=v("div"),r=E(g),i=j(),h(s,"class","txt"),h(l,"class","tab-item svelte-1maocj6"),S(l,"active",e[1]===e[6].language),this.first=l},m(u,_){y(u,l,_),m(l,s),m(s,r),m(l,i),n||(k=z(l,"click",c),n=!0)},p(u,_){e=u,_&4&&g!==(g=e[6].title+"")&&R(r,g),_&6&&S(l,"active",e[1]===e[6].language)},d(u){u&&w(l),n=!1,k()}}}function I(o,e){let l,s,g,r,i,n,k=e[6].title+"",c,u,_,p,f;return s=new A({props:{language:e[6].language,content:e[6].content}}),{key:o,first:null,c(){l=v("div"),G(s.$$.fragment),g=j(),r=v("div"),i=v("em"),n=v("a"),c=E(k),u=E(" SDK"),p=j(),h(n,"href",_=e[6].url),h(n,"target","_blank"),h(n,"rel","noopener noreferrer"),h(i,"class","txt-sm txt-hint"),h(r,"class","txt-right"),h(l,"class","tab-item svelte-1maocj6"),S(l,"active",e[1]===e[6].language),this.first=l},m(b,t){y(b,l,t),H(s,l,null),m(l,g),m(l,r),m(r,i),m(i,n),m(n,c),m(n,u),m(l,p),f=!0},p(b,t){e=b;const a={};t&4&&(a.language=e[6].language),t&4&&(a.content=e[6].content),s.$set(a),(!f||t&4)&&k!==(k=e[6].title+"")&&R(c,k),(!f||t&4&&_!==(_=e[6].url))&&h(n,"href",_),(!f||t&6)&&S(l,"active",e[1]===e[6].language)},i(b){f||(N(s.$$.fragment,b),f=!0)},o(b){P(s.$$.fragment,b),f=!1},d(b){b&&w(l),L(s)}}}function U(o){let e,l,s=[],g=new Map,r,i,n=[],k=new Map,c,u,_=o[2];const p=t=>t[6].language;for(let t=0;t<_.length;t+=1){let a=K(o,_,t),d=p(a);g.set(d,s[t]=T(d,a))}let f=o[2];const b=t=>t[6].language;for(let t=0;t<f.length;t+=1){let a=D(o,f,t),d=b(a);k.set(d,n[t]=I(d,a))}return{c(){e=v("div"),l=v("div");for(let t=0;t<s.length;t+=1)s[t].c();r=j(),i=v("div");for(let t=0;t<n.length;t+=1)n[t].c();h(l,"class","tabs-header compact left"),h(i,"class","tabs-content"),h(e,"class",c="tabs sdk-tabs "+o[0]+" svelte-1maocj6")},m(t,a){y(t,e,a),m(e,l);for(let d=0;d<s.length;d+=1)s[d].m(l,null);m(e,r),m(e,i);for(let d=0;d<n.length;d+=1)n[d].m(i,null);u=!0},p(t,[a]){a&6&&(_=t[2],s=C(s,a,p,1,t,_,g,l,J,T,null,K)),a&6&&(f=t[2],O(),n=C(n,a,b,1,t,f,k,i,Q,I,null,D),Y()),(!u||a&1&&c!==(c="tabs sdk-tabs "+t[0]+" svelte-1maocj6"))&&h(e,"class",c)},i(t){if(!u){for(let a=0;a<f.length;a+=1)N(n[a]);u=!0}},o(t){for(let a=0;a<n.length;a+=1)P(n[a]);u=!1},d(t){t&&w(e);for(let a=0;a<s.length;a+=1)s[a].d();for(let a=0;a<n.length;a+=1)n[a].d()}}}const M="pb_sdk_preference";function V(o,e,l){let s,{class:g="m-b-base"}=e,{js:r=""}=e,{dart:i=""}=e,n=localStorage.getItem(M)||"javascript";const k=c=>l(1,n=c.language);return o.$$set=c=>{"class"in c&&l(0,g=c.class),"js"in c&&l(3,r=c.js),"dart"in c&&l(4,i=c.dart)},o.$$.update=()=>{o.$$.dirty&2&&n&&localStorage.setItem(M,n),o.$$.dirty&24&&l(2,s=[{title:"JavaScript",language:"javascript",content:r,url:"https://github.com/pocketbase/js-sdk"},{title:"Dart",language:"dart",content:i,url:"https://github.com/pocketbase/dart-sdk"}])},[g,n,s,r,i,k]}class X extends q{constructor(e){super(),B(this,e,V,U,F,{class:0,js:3,dart:4})}}export{X as S}; import{S as q,i as B,s as F,e as v,b as j,f as h,g as y,h as m,O as C,P as J,k as O,Q,n as Y,t as N,a as P,o as w,w as E,r as S,u as z,x as R,N as A,c as G,m as H,d as L}from"./index.e8d8151e.js";function D(o,e,l){const s=o.slice();return s[6]=e[l],s}function K(o,e,l){const s=o.slice();return s[6]=e[l],s}function T(o,e){let l,s,g=e[6].title+"",r,i,n,k;function c(){return e[5](e[6])}return{key:o,first:null,c(){l=v("button"),s=v("div"),r=E(g),i=j(),h(s,"class","txt"),h(l,"class","tab-item svelte-1maocj6"),S(l,"active",e[1]===e[6].language),this.first=l},m(u,_){y(u,l,_),m(l,s),m(s,r),m(l,i),n||(k=z(l,"click",c),n=!0)},p(u,_){e=u,_&4&&g!==(g=e[6].title+"")&&R(r,g),_&6&&S(l,"active",e[1]===e[6].language)},d(u){u&&w(l),n=!1,k()}}}function I(o,e){let l,s,g,r,i,n,k=e[6].title+"",c,u,_,p,f;return s=new A({props:{language:e[6].language,content:e[6].content}}),{key:o,first:null,c(){l=v("div"),G(s.$$.fragment),g=j(),r=v("div"),i=v("em"),n=v("a"),c=E(k),u=E(" SDK"),p=j(),h(n,"href",_=e[6].url),h(n,"target","_blank"),h(n,"rel","noopener noreferrer"),h(i,"class","txt-sm txt-hint"),h(r,"class","txt-right"),h(l,"class","tab-item svelte-1maocj6"),S(l,"active",e[1]===e[6].language),this.first=l},m(b,t){y(b,l,t),H(s,l,null),m(l,g),m(l,r),m(r,i),m(i,n),m(n,c),m(n,u),m(l,p),f=!0},p(b,t){e=b;const a={};t&4&&(a.language=e[6].language),t&4&&(a.content=e[6].content),s.$set(a),(!f||t&4)&&k!==(k=e[6].title+"")&&R(c,k),(!f||t&4&&_!==(_=e[6].url))&&h(n,"href",_),(!f||t&6)&&S(l,"active",e[1]===e[6].language)},i(b){f||(N(s.$$.fragment,b),f=!0)},o(b){P(s.$$.fragment,b),f=!1},d(b){b&&w(l),L(s)}}}function U(o){let e,l,s=[],g=new Map,r,i,n=[],k=new Map,c,u,_=o[2];const p=t=>t[6].language;for(let t=0;t<_.length;t+=1){let a=K(o,_,t),d=p(a);g.set(d,s[t]=T(d,a))}let f=o[2];const b=t=>t[6].language;for(let t=0;t<f.length;t+=1){let a=D(o,f,t),d=b(a);k.set(d,n[t]=I(d,a))}return{c(){e=v("div"),l=v("div");for(let t=0;t<s.length;t+=1)s[t].c();r=j(),i=v("div");for(let t=0;t<n.length;t+=1)n[t].c();h(l,"class","tabs-header compact left"),h(i,"class","tabs-content"),h(e,"class",c="tabs sdk-tabs "+o[0]+" svelte-1maocj6")},m(t,a){y(t,e,a),m(e,l);for(let d=0;d<s.length;d+=1)s[d].m(l,null);m(e,r),m(e,i);for(let d=0;d<n.length;d+=1)n[d].m(i,null);u=!0},p(t,[a]){a&6&&(_=t[2],s=C(s,a,p,1,t,_,g,l,J,T,null,K)),a&6&&(f=t[2],O(),n=C(n,a,b,1,t,f,k,i,Q,I,null,D),Y()),(!u||a&1&&c!==(c="tabs sdk-tabs "+t[0]+" svelte-1maocj6"))&&h(e,"class",c)},i(t){if(!u){for(let a=0;a<f.length;a+=1)N(n[a]);u=!0}},o(t){for(let a=0;a<n.length;a+=1)P(n[a]);u=!1},d(t){t&&w(e);for(let a=0;a<s.length;a+=1)s[a].d();for(let a=0;a<n.length;a+=1)n[a].d()}}}const M="pb_sdk_preference";function V(o,e,l){let s,{class:g="m-b-base"}=e,{js:r=""}=e,{dart:i=""}=e,n=localStorage.getItem(M)||"javascript";const k=c=>l(1,n=c.language);return o.$$set=c=>{"class"in c&&l(0,g=c.class),"js"in c&&l(3,r=c.js),"dart"in c&&l(4,i=c.dart)},o.$$.update=()=>{o.$$.dirty&2&&n&&localStorage.setItem(M,n),o.$$.dirty&24&&l(2,s=[{title:"JavaScript",language:"javascript",content:r,url:"https://github.com/pocketbase/js-sdk"},{title:"Dart",language:"dart",content:i,url:"https://github.com/pocketbase/dart-sdk"}])},[g,n,s,r,i,k]}class X extends q{constructor(e){super(),B(this,e,V,U,F,{class:0,js:3,dart:4})}}export{X as S};

View File

@ -1,4 +1,4 @@
import{S as qe,i as Oe,s as De,e as i,w as v,b as h,c as Se,f,g as r,h as s,m as Be,x as R,O as ye,P as Me,k as We,Q as ze,n as He,t as le,a as oe,o as d,d as Ue,R as Ie,C as Le,p as Re,r as j,u as je,N as Ne}from"./index.d939dbbd.js";import{S as Ke}from"./SdkTabs.2a5180be.js";function Ae(n,l,o){const a=n.slice();return a[5]=l[o],a}function Ce(n,l,o){const a=n.slice();return a[5]=l[o],a}function Te(n,l){let o,a=l[5].code+"",_,b,c,u;function m(){return l[4](l[5])}return{key:n,first:null,c(){o=i("button"),_=v(a),b=h(),f(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m($,P){r($,o,P),s(o,_),s(o,b),c||(u=je(o,"click",m),c=!0)},p($,P){l=$,P&4&&a!==(a=l[5].code+"")&&R(_,a),P&6&&j(o,"active",l[1]===l[5].code)},d($){$&&d(o),c=!1,u()}}}function Ee(n,l){let o,a,_,b;return a=new Ne({props:{content:l[5].body}}),{key:n,first:null,c(){o=i("div"),Se(a.$$.fragment),_=h(),f(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m(c,u){r(c,o,u),Be(a,o,null),s(o,_),b=!0},p(c,u){l=c;const m={};u&4&&(m.content=l[5].body),a.$set(m),(!b||u&6)&&j(o,"active",l[1]===l[5].code)},i(c){b||(le(a.$$.fragment,c),b=!0)},o(c){oe(a.$$.fragment,c),b=!1},d(c){c&&d(o),Ue(a)}}}function Qe(n){var he,_e,ke,ve;let l,o,a=n[0].name+"",_,b,c,u,m,$,P,M=n[0].name+"",N,se,ae,K,Q,A,F,E,G,g,W,ne,z,y,ie,J,H=n[0].name+"",V,ce,X,re,Y,de,I,Z,S,x,B,ee,U,te,C,q,w=[],ue=new Map,pe,O,k=[],me=new Map,T;A=new Ke({props:{js:` import{S as qe,i as Oe,s as De,e as i,w as v,b as h,c as Se,f,g as r,h as s,m as Be,x as R,O as ye,P as Me,k as We,Q as ze,n as He,t as le,a as oe,o as d,d as Ue,R as Ie,C as Le,p as Re,r as j,u as je,N as Ne}from"./index.e8d8151e.js";import{S as Ke}from"./SdkTabs.6909f1b6.js";function Ae(n,l,o){const a=n.slice();return a[5]=l[o],a}function Ce(n,l,o){const a=n.slice();return a[5]=l[o],a}function Te(n,l){let o,a=l[5].code+"",_,b,c,u;function m(){return l[4](l[5])}return{key:n,first:null,c(){o=i("button"),_=v(a),b=h(),f(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m($,P){r($,o,P),s(o,_),s(o,b),c||(u=je(o,"click",m),c=!0)},p($,P){l=$,P&4&&a!==(a=l[5].code+"")&&R(_,a),P&6&&j(o,"active",l[1]===l[5].code)},d($){$&&d(o),c=!1,u()}}}function Ee(n,l){let o,a,_,b;return a=new Ne({props:{content:l[5].body}}),{key:n,first:null,c(){o=i("div"),Se(a.$$.fragment),_=h(),f(o,"class","tab-item"),j(o,"active",l[1]===l[5].code),this.first=o},m(c,u){r(c,o,u),Be(a,o,null),s(o,_),b=!0},p(c,u){l=c;const m={};u&4&&(m.content=l[5].body),a.$set(m),(!b||u&6)&&j(o,"active",l[1]===l[5].code)},i(c){b||(le(a.$$.fragment,c),b=!0)},o(c){oe(a.$$.fragment,c),b=!1},d(c){c&&d(o),Ue(a)}}}function Qe(n){var he,_e,ke,ve;let l,o,a=n[0].name+"",_,b,c,u,m,$,P,M=n[0].name+"",N,se,ae,K,Q,A,F,E,G,g,W,ne,z,y,ie,J,H=n[0].name+"",V,ce,X,re,Y,de,I,Z,S,x,B,ee,U,te,C,q,w=[],ue=new Map,pe,O,k=[],me=new Map,T;A=new Ke({props:{js:`
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${n[3]}'); const pb = new PocketBase('${n[3]}');

View File

@ -1,4 +1,4 @@
import{S as Ct,i as St,s as Ot,C as I,N as Tt,e as r,w as y,b as m,c as Ae,f as T,g as a,h as i,m as Be,x as U,O as Pe,P as ut,k as Mt,Q as $t,n as Rt,t as pe,a as fe,o,d as Fe,R as qt,p as Dt,r as ce,u as Ht,y as G}from"./index.d939dbbd.js";import{S as Lt}from"./SdkTabs.2a5180be.js";function bt(p,t,l){const s=p.slice();return s[7]=t[l],s}function mt(p,t,l){const s=p.slice();return s[7]=t[l],s}function _t(p,t,l){const s=p.slice();return s[12]=t[l],s}function yt(p){let t;return{c(){t=r("p"),t.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",T(t,"class","txt-hint txt-sm txt-right")},m(l,s){a(l,t,s)},d(l){l&&o(t)}}}function kt(p){let t,l,s,b,u,d,f,k,C,v,O,D,A,F,M,N,B;return{c(){t=r("tr"),t.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=r("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span> import{S as Ct,i as St,s as Ot,C as I,N as Tt,e as r,w as y,b as m,c as Ae,f as T,g as a,h as i,m as Be,x as U,O as Pe,P as ut,k as Mt,Q as $t,n as Rt,t as pe,a as fe,o,d as Fe,R as qt,p as Dt,r as ce,u as Ht,y as G}from"./index.e8d8151e.js";import{S as Lt}from"./SdkTabs.6909f1b6.js";function bt(p,t,l){const s=p.slice();return s[7]=t[l],s}function mt(p,t,l){const s=p.slice();return s[7]=t[l],s}function _t(p,t,l){const s=p.slice();return s[12]=t[l],s}function yt(p){let t;return{c(){t=r("p"),t.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",T(t,"class","txt-hint txt-sm txt-right")},m(l,s){a(l,t,s)},d(l){l&&o(t)}}}function kt(p){let t,l,s,b,u,d,f,k,C,v,O,D,A,F,M,N,B;return{c(){t=r("tr"),t.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=r("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
<span>username</span></div></td> <span>username</span></div></td>
<td><span class="label">String</span></td> <td><span class="label">String</span></td>
<td>The username of the auth record.</td>`,b=m(),u=r("tr"),u.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span> <td>The username of the auth record.</td>`,b=m(),u=r("tr"),u.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>

View File

@ -1,4 +1,4 @@
import{S as Ze,i as et,s as tt,N as Ye,e as o,w as m,b as f,c as _e,f as _,g as r,h as l,m as ke,x as me,O as Ve,P as lt,k as st,Q as nt,n as ot,t as z,a as G,o as d,d as he,R as it,C as ze,p as at,r as J,u as rt}from"./index.d939dbbd.js";import{S as dt}from"./SdkTabs.2a5180be.js";function Ge(i,s,n){const a=i.slice();return a[6]=s[n],a}function Je(i,s,n){const a=i.slice();return a[6]=s[n],a}function Ke(i){let s;return{c(){s=o("p"),s.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",_(s,"class","txt-hint txt-sm txt-right")},m(n,a){r(n,s,a)},d(n){n&&d(s)}}}function We(i,s){let n,a=s[6].code+"",w,c,p,u;function C(){return s[5](s[6])}return{key:i,first:null,c(){n=o("button"),w=m(a),c=f(),_(n,"class","tab-item"),J(n,"active",s[2]===s[6].code),this.first=n},m(h,F){r(h,n,F),l(n,w),l(n,c),p||(u=rt(n,"click",C),p=!0)},p(h,F){s=h,F&20&&J(n,"active",s[2]===s[6].code)},d(h){h&&d(n),p=!1,u()}}}function Xe(i,s){let n,a,w,c;return a=new Ye({props:{content:s[6].body}}),{key:i,first:null,c(){n=o("div"),_e(a.$$.fragment),w=f(),_(n,"class","tab-item"),J(n,"active",s[2]===s[6].code),this.first=n},m(p,u){r(p,n,u),ke(a,n,null),l(n,w),c=!0},p(p,u){s=p,(!c||u&20)&&J(n,"active",s[2]===s[6].code)},i(p){c||(z(a.$$.fragment,p),c=!0)},o(p){G(a.$$.fragment,p),c=!1},d(p){p&&d(n),he(a)}}}function ct(i){var Ne,Ue;let s,n,a=i[0].name+"",w,c,p,u,C,h,F,N=i[0].name+"",K,ve,W,g,X,B,Y,$,U,we,j,E,ye,Z,Q=i[0].name+"",ee,$e,te,Ce,le,I,se,x,ne,A,oe,O,ie,Re,ae,D,re,Fe,de,ge,k,Oe,S,De,Pe,Te,ce,Ee,pe,Se,Be,Ie,fe,xe,ue,M,be,P,H,R=[],Ae=new Map,Me,q,y=[],He=new Map,T;g=new dt({props:{js:` import{S as Ze,i as et,s as tt,N as Ye,e as o,w as m,b as f,c as _e,f as _,g as r,h as l,m as ke,x as me,O as Ve,P as lt,k as st,Q as nt,n as ot,t as z,a as G,o as d,d as he,R as it,C as ze,p as at,r as J,u as rt}from"./index.e8d8151e.js";import{S as dt}from"./SdkTabs.6909f1b6.js";function Ge(i,s,n){const a=i.slice();return a[6]=s[n],a}function Je(i,s,n){const a=i.slice();return a[6]=s[n],a}function Ke(i){let s;return{c(){s=o("p"),s.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",_(s,"class","txt-hint txt-sm txt-right")},m(n,a){r(n,s,a)},d(n){n&&d(s)}}}function We(i,s){let n,a=s[6].code+"",w,c,p,u;function C(){return s[5](s[6])}return{key:i,first:null,c(){n=o("button"),w=m(a),c=f(),_(n,"class","tab-item"),J(n,"active",s[2]===s[6].code),this.first=n},m(h,F){r(h,n,F),l(n,w),l(n,c),p||(u=rt(n,"click",C),p=!0)},p(h,F){s=h,F&20&&J(n,"active",s[2]===s[6].code)},d(h){h&&d(n),p=!1,u()}}}function Xe(i,s){let n,a,w,c;return a=new Ye({props:{content:s[6].body}}),{key:i,first:null,c(){n=o("div"),_e(a.$$.fragment),w=f(),_(n,"class","tab-item"),J(n,"active",s[2]===s[6].code),this.first=n},m(p,u){r(p,n,u),ke(a,n,null),l(n,w),c=!0},p(p,u){s=p,(!c||u&20)&&J(n,"active",s[2]===s[6].code)},i(p){c||(z(a.$$.fragment,p),c=!0)},o(p){G(a.$$.fragment,p),c=!1},d(p){p&&d(n),he(a)}}}function ct(i){var Ne,Ue;let s,n,a=i[0].name+"",w,c,p,u,C,h,F,N=i[0].name+"",K,ve,W,g,X,B,Y,$,U,we,j,E,ye,Z,Q=i[0].name+"",ee,$e,te,Ce,le,I,se,x,ne,A,oe,O,ie,Re,ae,D,re,Fe,de,ge,k,Oe,S,De,Pe,Te,ce,Ee,pe,Se,Be,Ie,fe,xe,ue,M,be,P,H,R=[],Ae=new Map,Me,q,y=[],He=new Map,T;g=new dt({props:{js:`
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[3]}'); const pb = new PocketBase('${i[3]}');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
ui/dist/index.html vendored
View File

@ -24,8 +24,8 @@
window.Prism = window.Prism || {}; window.Prism = window.Prism || {};
window.Prism.manual = true; window.Prism.manual = true;
</script> </script>
<script type="module" crossorigin src="./assets/index.d939dbbd.js"></script> <script type="module" crossorigin src="./assets/index.e8d8151e.js"></script>
<link rel="stylesheet" href="./assets/index.c5a3774d.css"> <link rel="stylesheet" href="./assets/index.5fccb547.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -236,12 +236,23 @@
result.push(key); result.push(key);
if (field.type === "relation" && field.options.collectionId) { // add relation fields
if (field.type === "relation" && field.options?.collectionId) {
const subKeys = getCollectionFieldKeys(field.options.collectionId, key + ".", level + 1); const subKeys = getCollectionFieldKeys(field.options.collectionId, key + ".", level + 1);
if (subKeys.length) { if (subKeys.length) {
result = result.concat(subKeys); result = result.concat(subKeys);
} }
} }
// add ":each" field modifier
if (field.type === "select" && field.options?.maxSelect != 1) {
result.push(key + ":each");
}
// add ":length" field modifier to arrayble fields
if (field.options?.maxSelect != 1 && ["select", "file", "relation"].includes(field.type)) {
result.push(key + ":length");
}
} }
return result; return result;
@ -259,7 +270,6 @@
result.push("@request.method"); result.push("@request.method");
result.push("@request.query."); result.push("@request.query.");
result.push("@request.data."); result.push("@request.data.");
result.push("@request.auth.");
result.push("@request.auth.id"); result.push("@request.auth.id");
result.push("@request.auth.collectionId"); result.push("@request.auth.collectionId");
result.push("@request.auth.collectionName"); result.push("@request.auth.collectionName");
@ -279,6 +289,27 @@
} }
} }
// load base collection fields into @request.data.*
const issetExcludeList = ["created", "updated"];
if (baseCollection?.id) {
const keys = getCollectionFieldKeys(baseCollection.name, "@request.data.");
for (const key of keys) {
result.push(key);
// add ":isset" modifier to non-base keys
const parts = key.split(".");
if (
parts.length === 3 &&
// doesn't contain another modifier
parts[2].indexOf(":") === -1 &&
// is not from the exclude list
!issetExcludeList.includes(parts[2])
) {
result.push(key + ":isset");
}
}
}
return result; return result;
} }

View File

@ -9,7 +9,7 @@
</script> </script>
<div class="block m-b-base"> <div class="block m-b-base">
<div class="flex txt-sm m-b-5"> <div class="flex txt-sm txt-hint m-b-5">
<p> <p>
All rules follow the All rules follow the
<a href={import.meta.env.PB_RULES_SYNTAX_DOCS} target="_blank" rel="noopener noreferrer"> <a href={import.meta.env.PB_RULES_SYNTAX_DOCS} target="_blank" rel="noopener noreferrer">

View File

@ -108,6 +108,17 @@
return CommonHelper.slugify(name); return CommonHelper.slugify(name);
} }
function requiredLabel(field) {
switch (field?.type) {
case "bool":
return "Nonfalsey";
case "number":
return "Nonzero";
default:
return "Nonempty";
}
}
onMount(() => { onMount(() => {
// auto expand new fields // auto expand new fields
if (!field.id) { if (!field.id) {
@ -150,7 +161,7 @@
<span class="label" class:label-warning={interactive && !field.toDelete}>New</span> <span class="label" class:label-warning={interactive && !field.toDelete}>New</span>
{/if} {/if}
{#if field.required} {#if field.required}
<span class="label label-success">Nonempty</span> <span class="label label-success">{requiredLabel(field)}</span>
{/if} {/if}
{#if field.unique} {#if field.unique}
<span class="label label-success">Unique</span> <span class="label label-success">Unique</span>
@ -270,13 +281,13 @@
<Field class="form-field form-field-toggle m-0" name="requried" let:uniqueId> <Field class="form-field form-field-toggle m-0" name="requried" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={field.required} /> <input type="checkbox" id={uniqueId} bind:checked={field.required} />
<label for={uniqueId}> <label for={uniqueId}>
<span class="txt">Nonempty</span> <span class="txt">{requiredLabel(field)}</span>
<i <i
class="ri-information-line link-hint" class="ri-information-line link-hint"
use:tooltip={{ use:tooltip={{
text: `Requires the field value to be nonempty\n(aka. not ${CommonHelper.zeroDefaultStr( text: `Requires the field value to be ${requiredLabel(
field field
)}).`, )}\n(aka. not ${CommonHelper.zeroDefaultStr(field)}).`,
position: "right", position: "right",
}} }}
/> />

View File

@ -4,7 +4,6 @@
<script> <script>
import { tick } from "svelte"; import { tick } from "svelte";
import tooltip from "@/actions/tooltip";
import Field from "@/components/base/Field.svelte"; import Field from "@/components/base/Field.svelte";
export let collection = null; export let collection = null;
@ -20,6 +19,8 @@
$: isAdminOnly = rule === null; $: isAdminOnly = rule === null;
loadEditorComponent();
async function loadEditorComponent() { async function loadEditorComponent() {
if (ruleInputComponent || isRuleComponentLoading) { if (ruleInputComponent || isRuleComponentLoading) {
return; // already loaded or in the process return; // already loaded or in the process
@ -34,7 +35,16 @@
isRuleComponentLoading = false; isRuleComponentLoading = false;
} }
loadEditorComponent(); async function unlock() {
rule = tempValue || "";
await tick();
editorRef?.focus();
}
async function lock() {
tempValue = rule;
rule = null;
}
</script> </script>
{#if isRuleComponentLoading} {#if isRuleComponentLoading}
@ -42,80 +52,60 @@
<span class="loader" /> <span class="loader" />
</div> </div>
{:else} {:else}
<div class="rule-block"> <Field
{#if isAdminOnly} class="form-field rule-field m-0 {required ? 'requied' : ''} {isAdminOnly ? 'disabled' : ''}"
<button name={formKey}
type="button" let:uniqueId
class="rule-toggle-btn btn btn-circle btn-outline btn-success" >
use:tooltip={{ <label for={uniqueId}>
text: "Unlock and set custom rule", <span class="txt">
position: "left",
}}
on:click={async () => {
rule = tempValue || "";
await tick();
editorRef?.focus();
}}
>
<i class="ri-lock-unlock-line" />
</button>
{:else}
<button
type="button"
class="rule-toggle-btn btn btn-circle btn-outline"
use:tooltip={{
text: "Lock and set to Admins only",
position: "left",
}}
on:click={() => {
tempValue = rule;
rule = null;
}}
>
<i class="ri-lock-line" />
</button>
{/if}
<Field
class="form-field rule-field m-0 {required ? 'requied' : ''} {isAdminOnly ? 'disabled' : ''}"
name={formKey}
let:uniqueId
>
<label for={uniqueId}>
{label} - {isAdminOnly ? "Admins only" : "Custom rule"} {label} - {isAdminOnly ? "Admins only" : "Custom rule"}
</label> </span>
<svelte:component {#if isAdminOnly}
this={ruleInputComponent} <button type="button" class="lock-toggle link-success" on:click={unlock}>
id={uniqueId} <i class="ri-lock-unlock-line" />
bind:this={editorRef} <span class="txt">Set custom rule</span>
bind:value={rule} </button>
baseCollection={collection} {:else}
disabled={isAdminOnly} <button type="button" class="lock-toggle link-hint" on:click={lock}>
/> <i class="ri-lock-line" />
<span class="txt">Set Admins only</span>
</button>
{/if}
</label>
<div class="help-block"> <svelte:component
<slot {isAdminOnly}> this={ruleInputComponent}
<p> id={uniqueId}
{#if isAdminOnly} bind:this={editorRef}
Only admins will be able to perform this action (unlock to change) bind:value={rule}
{:else} baseCollection={collection}
Leave empty to grant everyone access disabled={isAdminOnly}
{/if} />
</p>
</slot> <div class="help-block">
</div> <slot {isAdminOnly}>
</Field> <p>
</div> {#if isAdminOnly}
Only admins will be able to perform this action (
<button type="button" class="link-hint" on:click={unlock}>unlock to change</button>
).
{:else}
Leave empty to grant everyone access.
{/if}
</p>
</slot>
</div>
</Field>
{/if} {/if}
<style> <style>
.rule-block { .lock-toggle {
display: flex; margin-left: auto;
align-items: flex-start; display: inline-flex;
gap: var(--xsSpacing); align-items: center;
} gap: 5px;
.rule-toggle-btn { font-size: var(--smFontSize);
margin-top: 15px;
} }
</style> </style>

View File

@ -77,7 +77,7 @@
// fetch a paginated records list // fetch a paginated records list
const resultList = await pb.collection('${collection?.name}').getList(1, 50, { const resultList = await pb.collection('${collection?.name}').getList(1, 50, {
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2', filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
}); });
// you can also fetch all records at once via getFullList // you can also fetch all records at once via getFullList
@ -101,7 +101,7 @@
final resultList = await pb.collection('${collection?.name}').getList( final resultList = await pb.collection('${collection?.name}').getList(
page: 1, page: 1,
perPage: 50, perPage: 50,
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2', filter: 'created >= "2022-01-01 00:00:00" && someField1 != someField2',
); );
// you can also fetch all records at once via getFullList // you can also fetch all records at once via getFullList

View File

@ -54,7 +54,7 @@
{#if hasErrors} {#if hasErrors}
<i <i
class="ri-error-warning-fill txt-danger" class="ri-error-warning-fill txt-danger"
transition:scale={{ duration: 150, start: 0.7 }} transition:scale|local={{ duration: 150, start: 0.7 }}
use:tooltip={{ text: "Has errors", position: "left" }} use:tooltip={{ text: "Has errors", position: "left" }}
/> />
{/if} {/if}

Some files were not shown because too many files have changed in this diff Show More