Compare commits

...
42 Commits
Author SHA1 Message Date
Gani Georgiev cce9116fa7 updated ui/dist 2023-03-12 17:31:24 +02:00
Gani Georgiev 7cfbe49b32 updated changelog 2023-03-12 17:22:50 +02:00
Gani Georgiev b564be06f4 updated RecordsPicker to show proper view colllection relations 2023-03-12 17:12:35 +02:00
Gani Georgiev f5493dd608 updated ui/dist 2023-03-12 17:00:57 +02:00
Gani Georgiev 49227f5436 fixed views relation picker load action and updated the record preview 2023-03-12 16:44:24 +02:00
Gani Georgiev 94d2f296b2 [#2044] fixed view collections import 2023-03-12 16:43:27 +02:00
Gani Georgiev af34eb544d [#1989] fixed RecordInfo multiple files preview 2023-03-06 13:33:15 +02:00
Gani Georgiev 42e288c71a [#1976] added HEAD requests support for the file download action 2023-03-05 15:39:18 +02:00
Gani Georgiev dc96a12dc0 updated ui/dist 2023-02-26 11:00:52 +02:00
Gani Georgiev 0bb58eb52c minor UI fixes 2023-02-26 10:47:00 +02:00
Gani Georgiev cf6d325add added model query autocancellation test 2023-02-24 22:07:25 +02:00
Gani Georgiev 578e1c9bc1 [#223] updated the internal redirects to allow easier subpath deployment when behind a reverse proxy 2023-02-24 18:49:46 +02:00
Gani Georgiev 4778fc7a46 added min select relation field option 2023-02-24 16:34:02 +02:00
Gani Georgiev f1a6a82bd3 prevent collectionId relation field change 2023-02-24 14:12:27 +02:00
Gani Georgiev 21b152b58c fixed formatting and typos 2023-02-23 21:51:42 +02:00
Gani Georgiev aa4e405f92 replaced authentik with generic oidc provider 2023-02-23 21:07:00 +02:00
Gani Georgiev e529fe7e2a added --queryTimeout flag 2023-02-23 18:59:23 +02:00
Gani Georgiev 6ab2fa9489 updated default query timeout to 1m 2023-02-23 12:04:16 +02:00
Gani Georgiev 010a396b0e updated dao fail/retry handling 2023-02-22 22:20:19 +02:00
Gani Georgiev 65a148b741 added UploadedFiles to the record create/update events 2023-02-22 22:09:13 +02:00
Gani Georgiev 503c65a767 [#1896] run files upload after record save 2023-02-22 22:06:59 +02:00
Gani Georgiev eec3261d67 added cancelKey to the RecordsList component 2023-02-22 18:02:08 +02:00
Gani Georgiev 0db6c783cd minor ui improvements 2023-02-21 22:24:49 +02:00
Gani Georgiev 4fdc8feafc updated daos.HasTable to check also for views 2023-02-21 20:03:27 +02:00
Gani Georgiev 41c3cc8a90 added select auto fail/retry 2023-02-21 16:54:08 +02:00
Gani Georgiev 0afb09b3bd updated view ui 2023-02-21 16:32:58 +02:00
Gani Georgiev 1075292321 fixed godoc formatting 2023-02-20 13:31:03 +02:00
Gani Georgiev b184ef6c3a fixed daos.SaveView test 2023-02-18 20:31:41 +02:00
Gani Georgiev 5fd7f61656 temporary change the github action's go version to 1.19 2023-02-18 20:06:22 +02:00
Gani Georgiev 851711e231 updated go deps 2023-02-18 20:02:47 +02:00
Gani Georgiev a07f67002f added view collection type 2023-02-18 19:33:42 +02:00
Gani Georgiev 0052e2ab2a synced with master 2023-02-12 14:11:12 +02:00
Gani Georgiev fa65a20202 updated changelog 2023-02-06 19:57:43 +02:00
Gani Georgiev 668698feb2 [#1797] enabled goja/process module for accessing the os.Environ 2023-02-06 19:53:21 +02:00
Gani Georgiev f475967a4a updated default tokenizer separators 2023-02-06 16:30:47 +02:00
Gani Georgiev 23dfa9c634 added generic tokenizer helper 2023-02-05 20:59:17 +02:00
Gani Georgiev 1b21e86be6 synced with master 2023-02-02 15:35:03 +02:00
Gani Georgiev 7d38971850 updated changelog example 2023-02-01 22:19:36 +02:00
Gani Georgiev 15102138c3 updated changelog 2023-02-01 22:10:00 +02:00
Gani Georgiev 2378bc72c5 [#1728] normalized mailer.Message recipient fields 2023-02-01 22:07:46 +02:00
Gani Georgiev 69b80123de Merge branch 'master' into develop 2023-01-30 17:26:40 +02:00
Gani Georgiev 250642a8f9 allowed overwriting the default file serve headers if an explicit response header is set 2023-01-30 12:54:51 +02:00
161 changed files with 5175 additions and 1502 deletions
+82
View File
@@ -1,3 +1,85 @@
## v0.13.3
- Fixed view collections import ([#2044](https://github.com/pocketbase/pocketbase/issues/2044)).
- Updated the records picker Admin UI to show properly view collection relations.
## v0.13.2
- Fixed Admin UI js error when selecting multiple `file` field as `relation` "Display fields" ([#1989](https://github.com/pocketbase/pocketbase/issues/1989)).
## v0.13.1
- Added `HEAD` request method support for the `/api/files/:collection/:recordId/:filename` route ([#1976](https://github.com/pocketbase/pocketbase/discussions/1976)).
## v0.13.0
- Added new "View" collection type allowing you to create a read-only collection from a custom SQL `SELECT` statement. It supports:
- aggregations (`COUNT()`, `MIN()`, `MAX()`, `GROUP BY`, etc.)
- column and table aliases
- CTEs and subquery expressions
- auto `relation` fields association
- `file` fields proxying (up to 5 linked relations, eg. view1->view2->...->base)
- `filter`, `sort` and `expand`
- List and View API rules
- Added auto fail/retry (default to 8 attempts) for the `SELECT` queries to gracefully handle the `database is locked` errors ([#1795](https://github.com/pocketbase/pocketbase/discussions/1795#discussioncomment-4882169)).
_The default max attempts can be accessed or changed via `Dao.MaxLockRetries`._
- Added default max query execution timeout (30s).
_The default timeout can be accessed or changed via `Dao.ModelQueryTimeout`._
_For the prebuilt executables it can be also changed via the `--queryTimeout=10` flag._
- Added support for `dao.RecordQuery(collection)` to scan directly the `One()` and `All()` results in `*models.Record` or `[]*models.Record` without the need of explicit `NullStringMap`.
- Added support to overwrite the default file serve headers if an explicit response header is set.
- Added file thumbs when visualizing `relation` display file fields.
- Added "Min select" `relation` field option.
- Enabled `process.env` in JS migrations to allow accessing `os.Environ()`.
- Added `UploadedFiles` field to the `RecordCreateEvent` and `RecordUpdateEvent` event structs.
- **!** Moved file upload after the record persistent to allow setting custom record id safely from the `OnModelBeforeCreate` hook.
- **!** Changed `System.GetFile()` to return directly `*blob.Reader` instead of the `io.ReadCloser` interface.
- **!** Changed `To`, `Cc` and `Bcc` of `mailer.Message` to `[]mail.Address` for consistency and to allow multiple recipients and optional name.
If you are sending custom emails, you'll have to replace:
```go
message := &mailer.Message{
...
// (old) To: mail.Address{Address: "to@example.com"}
To: []mail.Address{{Address: "to@example.com", Name: "Some optional name"}},
// (old) Cc: []string{"cc@example.com"}
Cc: []mail.Address{{Address: "cc@example.com", Name: "Some optional name"}},
// (old) Bcc: []string{"bcc@example.com"}
Bcc: []mail.Address{{Address: "bcc@example.com", Name: "Some optional name"}},
...
}
```
- **!** Refactored the Authentik integration as a more generic "OpenID Connect" provider (`oidc`) to support any OIDC provider (Okta, Keycloak, etc.).
_If you've previously used Authentik, make sure to rename the provider key in your code to `oidc`._
_To enable more than one OIDC provider you can use the additional `oidc2` and `oidc3` provider keys._
- **!** Removed the previously deprecated `Dao.Block()` and `Dao.Continue()` helpers in favor of `Dao.NonconcurrentDB()`.
- Updated the internal redirects to allow easier subpath deployment when behind a reverse proxy.
- Other minor Admin UI improvements.
## v0.12.3
- Fixed "Toggle column" reactivity when navigating between collections ([#1836](https://github.com/pocketbase/pocketbase/pull/1836)).
+4 -4
View File
@@ -163,7 +163,7 @@ func bindStaticAdminUI(app core.App, e *echo.Echo) error {
e.GET(
strings.TrimRight(trailedAdminPath, "/"),
func(c echo.Context) error {
return c.Redirect(http.StatusTemporaryRedirect, trailedAdminPath)
return c.Redirect(http.StatusTemporaryRedirect, strings.TrimLeft(trailedAdminPath, "/"))
},
)
@@ -224,12 +224,12 @@ func installerRedirect(app core.App) echo.MiddlewareFunc {
if totalAdmins == 0 && !hasInstallerParam {
// redirect to the installer page
return c.Redirect(http.StatusTemporaryRedirect, trailedAdminPath+"?installer#")
return c.Redirect(http.StatusTemporaryRedirect, "?installer#")
}
if totalAdmins != 0 && hasInstallerParam {
// redirect to the home page
return c.Redirect(http.StatusTemporaryRedirect, trailedAdminPath+"#/")
// clear the installer param
return c.Redirect(http.StatusTemporaryRedirect, "?")
}
return next(c)
+139 -16
View File
@@ -45,7 +45,7 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":8`,
`"totalItems":10`,
`"items":[{`,
`"id":"_pb_users_auth_"`,
`"id":"v851q4r790rhknl"`,
@@ -73,10 +73,10 @@ func TestCollectionsList(t *testing.T) {
ExpectedContent: []string{
`"page":2`,
`"perPage":2`,
`"totalItems":8`,
`"totalItems":10`,
`"items":[{`,
`"id":"v851q4r790rhknl"`,
`"id":"4d1blo5cuycfaca"`,
`"id":"kpv709sk2lqbqk8"`,
`"id":"9n89pl5vkct6330"`,
},
ExpectedEvents: map[string]int{
"OnCollectionsListRequest": 1,
@@ -231,7 +231,7 @@ func TestCollectionDelete(t *testing.T) {
{
Name: "authorized as admin + using the collection name",
Method: http.MethodDelete,
Url: "/api/collections/demo1",
Url: "/api/collections/demo5",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
@@ -244,13 +244,13 @@ func TestCollectionDelete(t *testing.T) {
"OnCollectionAfterDeleteRequest": 1,
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "wsmn24bux7wo113")
ensureDeletedFiles(app, "9n89pl5vkct6330")
},
},
{
Name: "authorized as admin + using the collection id",
Method: http.MethodDelete,
Url: "/api/collections/wsmn24bux7wo113",
Url: "/api/collections/9n89pl5vkct6330",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
@@ -263,7 +263,7 @@ func TestCollectionDelete(t *testing.T) {
"OnCollectionAfterDeleteRequest": 1,
},
AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
ensureDeletedFiles(app, "wsmn24bux7wo113")
ensureDeletedFiles(app, "9n89pl5vkct6330")
},
},
{
@@ -292,6 +292,22 @@ func TestCollectionDelete(t *testing.T) {
"OnCollectionBeforeDeleteRequest": 1,
},
},
{
Name: "authorized as admin + deleting a view",
Method: http.MethodDelete,
Url: "/api/collections/view2",
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"OnModelBeforeDelete": 1,
"OnModelAfterDelete": 1,
"OnCollectionBeforeDeleteRequest": 1,
"OnCollectionAfterDeleteRequest": 1,
},
},
}
for _, scenario := range scenarios {
@@ -520,6 +536,56 @@ func TestCollectionCreate(t *testing.T) {
`"options":{"minPasswordLength":{"code":"validation_required"`,
},
},
// view
// -----------------------------------------------------------
{
Name: "trying to create view collection with invalid options",
Method: http.MethodPost,
Url: "/api/collections",
Body: strings.NewReader(`{
"name":"new",
"type":"view",
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options":{"query": "invalid"}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"options":{"query":{"code":"validation_invalid_view_query`,
},
},
{
Name: "creating view collection",
Method: http.MethodPost,
Url: "/api/collections",
Body: strings.NewReader(`{
"name":"new",
"type":"view",
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options": {
"query": "select 1 as id from _admins"
}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"new"`,
`"type":"view"`,
`"schema":[]`,
},
ExpectedEvents: map[string]int{
"OnModelBeforeCreate": 1,
"OnModelAfterCreate": 1,
"OnCollectionBeforeCreateRequest": 1,
"OnCollectionAfterCreateRequest": 1,
},
},
}
for _, scenario := range scenarios {
@@ -660,7 +726,7 @@ func TestCollectionUpdate(t *testing.T) {
{
Name: "updating base collection with reserved auth fields",
Method: http.MethodPatch,
Url: "/api/collections/demo1",
Url: "/api/collections/demo4",
Body: strings.NewReader(`{
"schema":[
{"type":"text","name":"email"},
@@ -681,7 +747,7 @@ func TestCollectionUpdate(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"demo1"`,
`"name":"demo4"`,
`"type":"base"`,
`"schema":[{`,
`"email"`,
@@ -751,6 +817,7 @@ func TestCollectionUpdate(t *testing.T) {
},
// rel field change displayFields propagation
// -----------------------------------------------------------
{
Name: "renaming a display field should also update the referenced displayFields value",
Method: http.MethodPatch,
@@ -830,6 +897,60 @@ func TestCollectionUpdate(t *testing.T) {
}
},
},
// view
// -----------------------------------------------------------
{
Name: "trying to update view collection with invalid options",
Method: http.MethodPatch,
Url: "/api/collections/view1",
Body: strings.NewReader(`{
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options":{"query": "invalid"}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"options":{"query":{"code":"validation_invalid_view_query`,
},
},
{
Name: "updating view collection",
Method: http.MethodPatch,
Url: "/api/collections/view2",
Body: strings.NewReader(`{
"name":"view2_update",
"schema":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
"options": {
"query": "select 2 as id, created, updated, email from _admins"
}
}`),
RequestHeaders: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"name":"view2_update"`,
`"type":"view"`,
`"schema":[{`,
`"name":"email"`,
},
NotExpectedContent: []string{
// base model fields are not part of the schema
`"name":"id"`,
`"name":"created"`,
`"name":"updated"`,
},
ExpectedEvents: map[string]int{
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
"OnCollectionBeforeUpdateRequest": 1,
"OnCollectionAfterUpdateRequest": 1,
},
},
}
for _, scenario := range scenarios {
@@ -838,6 +959,8 @@ func TestCollectionUpdate(t *testing.T) {
}
func TestCollectionImport(t *testing.T) {
totalCollections := 10
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
@@ -874,7 +997,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 8
expected := totalCollections
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
@@ -902,7 +1025,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 8
expected := totalCollections
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
@@ -944,7 +1067,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 8
expected := totalCollections
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
@@ -997,7 +1120,7 @@ func TestCollectionImport(t *testing.T) {
if err := app.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
expected := 11
expected := totalCollections + 3
if len(collections) != expected {
t.Fatalf("Expected %d collections, got %d", expected, len(collections))
}
@@ -1084,8 +1207,8 @@ func TestCollectionImport(t *testing.T) {
ExpectedEvents: map[string]int{
"OnCollectionsAfterImportRequest": 1,
"OnCollectionsBeforeImportRequest": 1,
"OnModelBeforeDelete": 6,
"OnModelAfterDelete": 6,
"OnModelBeforeDelete": 8,
"OnModelAfterDelete": 8,
"OnModelBeforeUpdate": 2,
"OnModelAfterUpdate": 2,
"OnModelBeforeCreate": 1,
+22 -2
View File
@@ -1,6 +1,8 @@
package apis
import (
"fmt"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
@@ -16,6 +18,7 @@ func bindFileApi(app core.App, rg *echo.Group) {
api := fileApi{app: app}
subGroup := rg.Group("/files", ActivityLogger(app))
subGroup.HEAD("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app))
subGroup.GET("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app))
}
@@ -45,15 +48,27 @@ func (api *fileApi) download(c echo.Context) error {
if fileField == nil {
return NewNotFoundError("", nil)
}
options, _ := fileField.Options.(*schema.FileOptions)
baseFilesPath := record.BaseFilesPath()
// fetch the original view file field related record
if collection.IsView() {
fileRecord, err := api.app.Dao().FindRecordByViewFile(collection.Id, fileField.Name, filename)
if err != nil {
return NewNotFoundError("", fmt.Errorf("Failed to fetch view file field record: %w", err))
}
baseFilesPath = fileRecord.BaseFilesPath()
}
fs, err := api.app.NewFilesystem()
if err != nil {
return NewBadRequestError("Filesystem initialization failure.", err)
}
defer fs.Close()
originalPath := record.BaseFilesPath() + "/" + filename
originalPath := baseFilesPath + "/" + filename
servedPath := originalPath
servedName := filename
@@ -70,7 +85,7 @@ func (api *fileApi) download(c echo.Context) error {
if list.ExistInSlice(oAttrs.ContentType, imageContentTypes) {
// add thumb size as file suffix
servedName = thumbSize + "_" + filename
servedPath = record.BaseFilesPath() + "/thumbs_" + filename + "/" + servedName
servedPath = baseFilesPath + "/thumbs_" + filename + "/" + servedName
// check if the thumb exists:
// - if doesn't exist - create a new thumb with the specified thumb size
@@ -92,6 +107,11 @@ func (api *fileApi) download(c echo.Context) error {
event.ServedPath = servedPath
event.ServedName = servedName
// clickjacking shouldn't be a concern when serving uploaded files,
// so it safe to unset the global X-Frame-Options to allow files embedding
// (note: it is out of the hook to allow users to customize the behavior)
c.Response().Header().Del("X-Frame-Options")
return api.app.OnFileDownloadRequest().Trigger(event, func(e *core.FileDownloadEvent) error {
res := e.HttpContext.Response()
req := e.HttpContext.Request()
+8
View File
@@ -179,6 +179,14 @@ func TestFileDownload(t *testing.T) {
}
for _, scenario := range scenarios {
// clone for the HEAD test (the same as the original scenario but without body)
head := scenario
head.Method = http.MethodHead
head.Name = ("(HEAD) " + scenario.Name)
head.ExpectedContent = nil
head.Test(t)
// regular request test
scenario.Test(t)
}
}
+6 -4
View File
@@ -59,9 +59,12 @@ func RequireGuestOnly() echo.MiddlewareFunc {
// specifying their names.
//
// Example:
// apis.RequireRecordAuth()
//
// apis.RequireRecordAuth()
//
// Or:
// apis.RequireRecordAuth("users", "supervisors")
//
// apis.RequireRecordAuth("users", "supervisors")
//
// To restrict the auth record only to the loaded context collection,
// use [apis.RequireSameContextRecordAuth()] instead.
@@ -83,7 +86,6 @@ func RequireRecordAuth(optCollectionNames ...string) echo.MiddlewareFunc {
}
}
//
// RequireSameContextRecordAuth middleware requires a request to have
// a valid record Authorization header.
//
@@ -261,7 +263,7 @@ func LoadCollectionContext(app core.App, optCollectionTypes ...string) echo.Midd
}
if len(optCollectionTypes) > 0 && !list.ExistInSlice(collection.Type, optCollectionTypes) {
return NewBadRequestError("Invalid collection type.", nil)
return NewBadRequestError("Unsupported collection type.", nil)
}
c.Set(ContextCollectionKey, collection)
+10 -12
View File
@@ -26,14 +26,13 @@ func bindRecordCrudApi(app core.App, rg *echo.Group) {
subGroup := rg.Group(
"/collections/:collection",
ActivityLogger(app),
LoadCollectionContext(app),
)
subGroup.GET("/records", api.list)
subGroup.POST("/records", api.create)
subGroup.GET("/records/:id", api.view)
subGroup.PATCH("/records/:id", api.update)
subGroup.DELETE("/records/:id", api.delete)
subGroup.GET("/records", api.list, LoadCollectionContext(app))
subGroup.GET("/records/:id", api.view, LoadCollectionContext(app))
subGroup.POST("/records", api.create, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth))
subGroup.PATCH("/records/:id", api.update, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth))
subGroup.DELETE("/records/:id", api.delete, LoadCollectionContext(app, models.CollectionTypeBase, models.CollectionTypeAuth))
}
type recordApi struct {
@@ -73,16 +72,13 @@ func (api *recordApi) list(c echo.Context) error {
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
}
var rawRecords = []dbx.NullStringMap{}
result, err := searchProvider.ParseAndExec(c.QueryParams().Encode(), &rawRecords)
records := []*models.Record{}
result, err := searchProvider.ParseAndExec(c.QueryParams().Encode(), &records)
if err != nil {
return NewBadRequestError("Invalid filter parameters.", err)
}
records := models.NewRecordsFromNullStringMaps(collection, rawRecords)
result.Items = records
event := new(core.RecordsListEvent)
event.HttpContext = c
event.Collection = collection
@@ -221,6 +217,7 @@ func (api *recordApi) create(c echo.Context) error {
event.HttpContext = c
event.Collection = collection
event.Record = record
event.UploadedFiles = form.FilesToUpload()
// create the record
submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
@@ -309,6 +306,7 @@ func (api *recordApi) update(c echo.Context) error {
event.HttpContext = c
event.Collection = collection
event.Record = record
event.UploadedFiles = form.FilesToUpload()
// update the record
submitErr := form.Submit(func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
+125 -2
View File
@@ -256,7 +256,7 @@ func TestRecordCrudList(t *testing.T) {
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
// auth collection checks
// auth collection
// -----------------------------------------------------------
{
Name: "check email visibility as guest",
@@ -403,6 +403,63 @@ func TestRecordCrudList(t *testing.T) {
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
// view collection
// -----------------------------------------------------------
{
Name: "public view records",
Method: http.MethodGet,
Url: "/api/collections/view2/records?filter=state=false",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":2`,
`"items":[{`,
`"id":"al1h9ijdeojtsjy"`,
`"id":"imy661ixudk5izi"`,
},
NotExpectedContent: []string{
`"created"`,
`"updated"`,
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "guest that doesn't match the view collection list rule",
Method: http.MethodGet,
Url: "/api/collections/view1/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":0`,
`"totalItems":0`,
`"items":[]`,
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "authenticated record that matches the view collection list rule",
Method: http.MethodGet,
Url: "/api/collections/view1/records",
RequestHeaders: map[string]string{
// users, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":1`,
`"items":[{`,
`"id":"84nmscqy84lsi1t"`,
`"bool":true`,
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
}
for _, scenario := range scenarios {
@@ -531,7 +588,7 @@ func TestRecordCrudView(t *testing.T) {
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
// auth collection checks
// auth collection
// -----------------------------------------------------------
{
Name: "check email visibility as guest",
@@ -629,6 +686,49 @@ func TestRecordCrudView(t *testing.T) {
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
// view collection
// -----------------------------------------------------------
{
Name: "public view record",
Method: http.MethodGet,
Url: "/api/collections/view2/records/84nmscqy84lsi1t",
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"84nmscqy84lsi1t"`,
`"state":true`,
`"file_many":["`,
`"rel_many":["`,
},
NotExpectedContent: []string{
`"created"`,
`"updated"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "guest that doesn't match the view collection view rule",
Method: http.MethodGet,
Url: "/api/collections/view1/records/84nmscqy84lsi1t",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authenticated record that matches the view collection view rule",
Method: http.MethodGet,
Url: "/api/collections/view1/records/84nmscqy84lsi1t",
RequestHeaders: map[string]string{
// users, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"84nmscqy84lsi1t"`,
`"bool":true`,
`"text":"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
}
for _, scenario := range scenarios {
@@ -690,6 +790,13 @@ func TestRecordCrudDelete(t *testing.T) {
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "trying to delete a view collection record",
Method: http.MethodDelete,
Url: "/api/collections/view1/records/imy661ixudk5izi",
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "public collection record delete",
Method: http.MethodDelete,
@@ -885,6 +992,14 @@ func TestRecordCrudCreate(t *testing.T) {
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "trying to create a new view collection record",
Method: http.MethodPost,
Url: "/api/collections/view1/records",
Body: strings.NewReader(`{"text":"new"}`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "submit nil body",
Method: http.MethodPost,
@@ -1428,6 +1543,14 @@ func TestRecordCrudUpdate(t *testing.T) {
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "trying to update a view collection record",
Method: http.MethodPatch,
Url: "/api/collections/view1/records/imy661ixudk5izi",
Body: strings.NewReader(`{"text":"new"}`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "submit nil body",
Method: http.MethodPatch,
+27 -9
View File
@@ -61,8 +61,10 @@ func TestSettingsList(t *testing.T) {
`"stravaAuth":{`,
`"giteeAuth":{`,
`"livechatAuth":{`,
`"authentikAuth":{`,
`"giteaAuth":{`,
`"oidcAuth":{`,
`"oidc2Auth":{`,
`"oidc3Auth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
},
@@ -133,8 +135,10 @@ func TestSettingsSet(t *testing.T) {
`"stravaAuth":{`,
`"giteeAuth":{`,
`"livechatAuth":{`,
`"authentikAuth":{`,
`"giteaAuth":{`,
`"oidcAuth":{`,
`"oidc2Auth":{`,
`"oidc3Auth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"acme_test"`,
@@ -194,8 +198,10 @@ func TestSettingsSet(t *testing.T) {
`"stravaAuth":{`,
`"giteeAuth":{`,
`"livechatAuth":{`,
`"authentikAuth":{`,
`"giteaAuth":{`,
`"oidcAuth":{`,
`"oidc2Auth":{`,
`"oidc3Auth":{`,
`"secret":"******"`,
`"clientSecret":"******"`,
`"appName":"update_test"`,
@@ -318,8 +324,12 @@ func TestSettingsTestEmail(t *testing.T) {
t.Fatalf("[verification] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}
if app.TestMailer.LastMessage.To.Address != "test@example.com" {
t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To.Address)
if len(app.TestMailer.LastMessage.To) != 1 {
t.Fatalf("[verification] Expected 1 recipient, got %v", app.TestMailer.LastMessage.To)
}
if app.TestMailer.LastMessage.To[0].Address != "test@example.com" {
t.Fatalf("[verification] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To[0].Address)
}
if !strings.Contains(app.TestMailer.LastMessage.HTML, "Verify") {
@@ -349,8 +359,12 @@ func TestSettingsTestEmail(t *testing.T) {
t.Fatalf("[password-reset] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}
if app.TestMailer.LastMessage.To.Address != "test@example.com" {
t.Fatalf("[password-reset] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To.Address)
if len(app.TestMailer.LastMessage.To) != 1 {
t.Fatalf("[password-reset] Expected 1 recipient, got %v", app.TestMailer.LastMessage.To)
}
if app.TestMailer.LastMessage.To[0].Address != "test@example.com" {
t.Fatalf("[password-reset] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To[0].Address)
}
if !strings.Contains(app.TestMailer.LastMessage.HTML, "Reset password") {
@@ -380,8 +394,12 @@ func TestSettingsTestEmail(t *testing.T) {
t.Fatalf("[email-change] Expected 1 sent email, got %d", app.TestMailer.TotalSend)
}
if app.TestMailer.LastMessage.To.Address != "test@example.com" {
t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To.Address)
if len(app.TestMailer.LastMessage.To) != 1 {
t.Fatalf("[email-change] Expected 1 recipient, got %v", app.TestMailer.LastMessage.To)
}
if app.TestMailer.LastMessage.To[0].Address != "test@example.com" {
t.Fatalf("[email-change] Expected the email to be sent to %s, got %s", "test@example.com", app.TestMailer.LastMessage.To[0].Address)
}
if !strings.Contains(app.TestMailer.LastMessage.HTML, "Confirm new email") {
+2 -2
View File
@@ -23,7 +23,7 @@ import (
)
const (
DefaultDataMaxOpenConns int = 100
DefaultDataMaxOpenConns int = 120
DefaultDataMaxIdleConns int = 20
DefaultLogsMaxOpenConns int = 10
DefaultLogsMaxIdleConns int = 2
@@ -472,7 +472,7 @@ func (app *BaseApp) NewMailClient() mailer.Mailer {
// NB! Make sure to call `Close()` on the returned result
// after you are done working with it.
func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
if app.settings.S3.Enabled {
if app.settings != nil && app.settings.S3.Enabled {
return filesystem.NewS3(
app.settings.S3.Bucket,
app.settings.S3.Region,
+7 -4
View File
@@ -6,6 +6,7 @@ import (
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/models/settings"
"github.com/pocketbase/pocketbase/tools/auth"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/mailer"
"github.com/pocketbase/pocketbase/tools/search"
@@ -160,15 +161,17 @@ type RecordViewEvent struct {
type RecordCreateEvent struct {
BaseCollectionEvent
HttpContext echo.Context
Record *models.Record
HttpContext echo.Context
Record *models.Record
UploadedFiles map[string][]*filesystem.File
}
type RecordUpdateEvent struct {
BaseCollectionEvent
HttpContext echo.Context
Record *models.Record
HttpContext echo.Context
Record *models.Record
UploadedFiles map[string][]*filesystem.File
}
type RecordDeleteEvent struct {
+38 -89
View File
@@ -4,19 +4,13 @@
package daos
import (
"context"
"errors"
"strings"
"sync"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"golang.org/x/sync/semaphore"
)
const DefaultMaxFailRetries = 5
// New creates a new Dao instance with the provided db builder
// (for both async and sync db operations).
func New(db dbx.Builder) *Dao {
@@ -27,8 +21,10 @@ func New(db dbx.Builder) *Dao {
// async and sync db builders.
func NewMultiDB(concurrentDB, nonconcurrentDB dbx.Builder) *Dao {
return &Dao{
concurrentDB: concurrentDB,
nonconcurrentDB: nonconcurrentDB,
concurrentDB: concurrentDB,
nonconcurrentDB: nonconcurrentDB,
MaxLockRetries: 8,
ModelQueryTimeout: 30 * time.Second,
}
}
@@ -39,9 +35,13 @@ type Dao struct {
concurrentDB dbx.Builder
nonconcurrentDB dbx.Builder
// @todo delete after removing Block and Continue
sem *semaphore.Weighted
mux sync.RWMutex
// MaxLockRetries specifies the default max "database is locked" auto retry attempts.
MaxLockRetries int
// ModelQueryTimeout is the default max duration of a running ModelQuery().
//
// This field has no effect if an explicit query context is already specified.
ModelQueryTimeout time.Duration
BeforeCreateFunc func(eventDao *Dao, m models.Model) error
AfterCreateFunc func(eventDao *Dao, m models.Model)
@@ -74,55 +74,17 @@ func (dao *Dao) NonconcurrentDB() dbx.Builder {
return dao.nonconcurrentDB
}
// Deprecated: Will be removed in the next releases. Use [Dao.NonconcurrentDB()] instead.
//
// Block acquires a lock and blocks all other go routines that uses
// the Dao instance until dao.Continue() is called, effectively making
// the concurrent requests to perform synchronous db operations.
//
// This method should be used only as a last resort and as a workaround
// for the SQLITE_BUSY error when mixing read&write in a transaction.
//
// Example:
//
// func someLongRunningTransaction() error {
// ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
// defer cancel()
// if err := app.Dao().Block(ctx); err != nil {
// return err
// }
// defer app.Dao().Continue()
//
// return app.Dao().RunInTransaction(func (txDao *daos.Dao) error {
// // some long running read&write transaction...
// })
// }
func (dao *Dao) Block(ctx context.Context) error {
if dao.sem == nil {
dao.mux.Lock()
dao.sem = semaphore.NewWeighted(1)
dao.mux.Unlock()
}
return dao.sem.Acquire(ctx, 1)
}
// Deprecated: Will be removed in the next releases. Use [Dao.NonconcurrentDB()] instead.
//
// Continue releases the previously acquired Block() lock.
func (dao *Dao) Continue() {
if dao.sem == nil {
return
}
dao.sem.Release(1)
}
// ModelQuery creates a new query with preset Select and From fields
// based on the provided model argument.
// ModelQuery creates a new preconfigured select query with preset
// SELECT, FROM and other common fields based on the provided model.
func (dao *Dao) ModelQuery(m models.Model) *dbx.SelectQuery {
tableName := m.TableName()
return dao.DB().Select("{{" + tableName + "}}.*").From(tableName)
return dao.DB().
Select("{{" + tableName + "}}.*").
From(tableName).
WithBuildHook(func(query *dbx.Query) {
query.WithExecHook(execLockRetry(dao.ModelQueryTimeout, dao.MaxLockRetries))
})
}
// FindById finds a single db record with the specified id and
@@ -224,7 +186,7 @@ func (dao *Dao) Delete(m models.Model) error {
return errors.New("ID is not set")
}
return dao.failRetry(func(retryDao *Dao) error {
return dao.lockRetry(func(retryDao *Dao) error {
if retryDao.BeforeDeleteFunc != nil {
if err := retryDao.BeforeDeleteFunc(retryDao, m); err != nil {
return err
@@ -240,20 +202,20 @@ func (dao *Dao) Delete(m models.Model) error {
}
return nil
}, DefaultMaxFailRetries)
})
}
// Save upserts (update or create if primary key is not set) the provided model.
func (dao *Dao) Save(m models.Model) error {
if m.IsNew() {
return dao.failRetry(func(retryDao *Dao) error {
return dao.lockRetry(func(retryDao *Dao) error {
return retryDao.create(m)
}, DefaultMaxFailRetries)
})
}
return dao.failRetry(func(retryDao *Dao) error {
return dao.lockRetry(func(retryDao *Dao) error {
return retryDao.update(m)
}, DefaultMaxFailRetries)
})
}
func (dao *Dao) update(m models.Model) error {
@@ -347,32 +309,19 @@ func (dao *Dao) create(m models.Model) error {
return nil
}
func (dao *Dao) failRetry(op func(retryDao *Dao) error, maxRetries int) error {
func (dao *Dao) lockRetry(op func(retryDao *Dao) error) error {
retryDao := dao
attempts := 1
Retry:
if attempts == 2 {
// assign new Dao without the before hooks to avoid triggering
// the already fired before callbacks multiple times
retryDao = NewMultiDB(dao.concurrentDB, dao.nonconcurrentDB)
retryDao.AfterCreateFunc = dao.AfterCreateFunc
retryDao.AfterUpdateFunc = dao.AfterUpdateFunc
retryDao.AfterDeleteFunc = dao.AfterDeleteFunc
}
return baseLockRetry(func(attempt int) error {
if attempt == 2 {
// assign new Dao without the before hooks to avoid triggering
// the already fired before callbacks multiple times
retryDao = NewMultiDB(dao.concurrentDB, dao.nonconcurrentDB)
retryDao.AfterCreateFunc = dao.AfterCreateFunc
retryDao.AfterUpdateFunc = dao.AfterUpdateFunc
retryDao.AfterDeleteFunc = dao.AfterDeleteFunc
}
// execute
err := op(retryDao)
if err != nil &&
attempts < maxRetries &&
// note: we are checking the err message so that we can handle both the cgo and noncgo errors
strings.Contains(err.Error(), "database is locked") {
// wait and retry
time.Sleep(time.Duration(200*attempts) * time.Millisecond)
attempts++
goto Retry
}
return err
return op(retryDao)
}, dao.MaxLockRetries)
}
+57
View File
@@ -0,0 +1,57 @@
package daos
import (
"context"
"strings"
"time"
"github.com/pocketbase/dbx"
)
// default retries intervals (in ms)
var defaultRetryIntervals = []int{100, 250, 350, 500, 700, 1000}
func execLockRetry(timeout time.Duration, maxRetries int) dbx.ExecHookFunc {
return func(q *dbx.Query, op func() error) error {
if q.Context() == nil {
cancelCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer func() {
cancel()
//nolint:staticcheck
q.WithContext(nil) // reset
}()
q.WithContext(cancelCtx)
}
return baseLockRetry(func(attempt int) error {
return op()
}, maxRetries)
}
}
func baseLockRetry(op func(attempt int) error, maxRetries int) error {
attempt := 1
Retry:
err := op(attempt)
if err != nil &&
attempt <= maxRetries &&
// we are checking the err message to handle both the cgo and noncgo errors
strings.Contains(err.Error(), "database is locked") {
// wait and retry
time.Sleep(getDefaultRetryInterval(attempt))
attempt++
goto Retry
}
return err
}
func getDefaultRetryInterval(attempt int) time.Duration {
if attempt < 0 || attempt > len(defaultRetryIntervals)-1 {
return time.Duration(defaultRetryIntervals[len(defaultRetryIntervals)-1]) * time.Millisecond
}
return time.Duration(defaultRetryIntervals[attempt]) * time.Millisecond
}
+60
View File
@@ -0,0 +1,60 @@
package daos
import (
"errors"
"testing"
)
func TestGetDefaultRetryInterval(t *testing.T) {
if i := getDefaultRetryInterval(-1); i.Milliseconds() != 1000 {
t.Fatalf("Expected 1000ms, got %v", i)
}
if i := getDefaultRetryInterval(999); i.Milliseconds() != 1000 {
t.Fatalf("Expected 1000ms, got %v", i)
}
if i := getDefaultRetryInterval(3); i.Milliseconds() != 500 {
t.Fatalf("Expected 500ms, got %v", i)
}
}
func TestBaseLockRetry(t *testing.T) {
scenarios := []struct {
err error
failUntilAttempt int
expectedAttempts int
}{
{nil, 3, 1},
{errors.New("test"), 3, 1},
{errors.New("database is locked"), 3, 3},
}
for i, s := range scenarios {
lastAttempt := 0
err := baseLockRetry(func(attempt int) error {
lastAttempt = attempt
if attempt < s.failUntilAttempt {
return s.err
}
return nil
}, s.failUntilAttempt+2)
if lastAttempt != s.expectedAttempts {
t.Errorf("[%d] Expected lastAttempt to be %d, got %d", i, s.expectedAttempts, lastAttempt)
}
if s.failUntilAttempt == s.expectedAttempts && err != nil {
t.Errorf("[%d] Expected nil, got err %v", i, err)
continue
}
if s.failUntilAttempt != s.expectedAttempts && s.err != nil && err == nil {
t.Errorf("[%d] Expected error %q, got nil", i, s.err)
continue
}
}
}
+19
View File
@@ -3,6 +3,7 @@ package daos_test
import (
"errors"
"testing"
"time"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
@@ -71,6 +72,24 @@ func TestDaoModelQuery(t *testing.T) {
}
}
func TestDaoModelQueryCancellation(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
dao := daos.New(testApp.DB())
m := &models.Admin{}
if err := dao.ModelQuery(m).One(m); err != nil {
t.Fatalf("Failed to execute control query: %v", err)
}
dao.ModelQueryTimeout = 0 * time.Millisecond
if err := dao.ModelQuery(m).One(m); err == nil {
t.Fatal("Expected to be cancelled, got nil")
}
}
func TestDaoFindById(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
+98 -27
View File
@@ -129,13 +129,23 @@ func (dao *Dao) DeleteCollection(collection *models.Collection) error {
return err
}
if total := len(result); total > 0 {
return fmt.Errorf("The collection %q has external relation field references (%d).", collection.Name, total)
names := make([]string, 0, len(result))
for ref := range result {
names = append(names, ref.Name)
}
return fmt.Errorf("The collection %q has external relation field references (%s).", collection.Name, strings.Join(names, ", "))
}
return dao.RunInTransaction(func(txDao *Dao) error {
// delete the related records table
if err := txDao.DeleteTable(collection.Name); err != nil {
return err
// delete the related view or records table
if collection.IsView() {
if err := txDao.DeleteView(collection.Name); err != nil {
return err
}
} else {
if err := txDao.DeleteTable(collection.Name); err != nil {
return err
}
}
return txDao.Delete(collection)
@@ -163,13 +173,18 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
collection.Type = models.CollectionTypeBase
}
// persist the collection model
if err := txDao.Save(collection); err != nil {
return err
}
switch collection.Type {
case models.CollectionTypeView:
return txDao.saveViewCollection(collection, oldCollection)
default:
// persist the collection model
if err := txDao.Save(collection); err != nil {
return err
}
// sync the changes with the related records table
return txDao.SyncRecordTableSchema(collection, oldCollection)
// sync the changes with the related records table
return txDao.SyncRecordTableSchema(collection, oldCollection)
}
})
}
@@ -183,7 +198,7 @@ func (dao *Dao) SaveCollection(collection *models.Collection) error {
func (dao *Dao) ImportCollections(
importedCollections []*models.Collection,
deleteMissing bool,
beforeRecordsSync func(txDao *Dao, mappedImported, mappedExisting map[string]*models.Collection) error,
afterSync func(txDao *Dao, mappedImported, mappedExisting map[string]*models.Collection) error,
) error {
if len(importedCollections) == 0 {
return errors.New("No collections to import")
@@ -191,7 +206,7 @@ func (dao *Dao) ImportCollections(
return dao.RunInTransaction(func(txDao *Dao) error {
existingCollections := []*models.Collection{}
if err := txDao.CollectionQuery().OrderBy("created ASC").All(&existingCollections); err != nil {
if err := txDao.CollectionQuery().OrderBy("updated ASC").All(&existingCollections); err != nil {
return err
}
mappedExisting := make(map[string]*models.Collection, len(existingCollections))
@@ -247,6 +262,17 @@ func (dao *Dao) ImportCollections(
return fmt.Errorf("System collection %q cannot be deleted.", existing.Name)
}
// delete the related records table or view
if existing.IsView() {
if err := txDao.DeleteView(existing.Name); err != nil {
return err
}
} else {
if err := txDao.DeleteTable(existing.Name); err != nil {
return err
}
}
// delete the collection
if err := txDao.Delete(existing); err != nil {
return err
@@ -261,29 +287,34 @@ func (dao *Dao) ImportCollections(
}
}
if beforeRecordsSync != nil {
if err := beforeRecordsSync(txDao, mappedImported, mappedExisting); err != nil {
// sync record tables
for _, imported := range importedCollections {
if imported.IsView() {
continue
}
existing := mappedExisting[imported.GetId()]
if err := txDao.SyncRecordTableSchema(imported, existing); err != nil {
return err
}
}
// delete the record tables of the deleted collections
if deleteMissing {
for _, existing := range existingCollections {
if mappedImported[existing.GetId()] != nil {
continue // exist
}
// sync views
for _, imported := range importedCollections {
if !imported.IsView() {
continue
}
if err := txDao.DeleteTable(existing.Name); err != nil {
return err
}
existing := mappedExisting[imported.GetId()]
if err := txDao.saveViewCollection(imported, existing); err != nil {
return err
}
}
// sync the upserted collections with the related records table
for _, imported := range importedCollections {
existing := mappedExisting[imported.GetId()]
if err := txDao.SyncRecordTableSchema(imported, existing); err != nil {
if afterSync != nil {
if err := afterSync(txDao, mappedImported, mappedExisting); err != nil {
return err
}
}
@@ -291,3 +322,43 @@ func (dao *Dao) ImportCollections(
return nil
})
}
// saveViewCollection persists the provided View collection changes:
// - deletes the old related SQL view (if any)
// - creates a new SQL view with the latest newCollection.Options.Query
// - generates a new schema based on newCollection.Options.Query
// - updates newCollection.Schema based on the generated view table info and query
// - saves the newCollection
//
// This method returns an error if newCollection is not a "view".
func (dao *Dao) saveViewCollection(newCollection *models.Collection, oldCollection *models.Collection) error {
if newCollection.IsAuth() {
return errors.New("not a view collection")
}
return dao.RunInTransaction(func(txDao *Dao) error {
query := newCollection.ViewOptions().Query
// generate collection schema from the query
schema, err := txDao.CreateViewSchema(query)
if err != nil {
return err
}
// delete old renamed view
if oldCollection != nil && newCollection.Name != oldCollection.Name {
if err := txDao.DeleteView(oldCollection.Name); err != nil {
return err
}
}
// (re)create the view
if err := txDao.SaveView(newCollection.Name, query); err != nil {
return err
}
newCollection.Schema = schema
return txDao.Save(newCollection)
})
}
+60 -32
View File
@@ -45,16 +45,16 @@ func TestFindCollectionsByType(t *testing.T) {
hasErr := err != nil
if hasErr != scenario.expectError {
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err)
t.Errorf("[%d] Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err)
}
if len(collections) != scenario.expectTotal {
t.Errorf("(%d) Expected %d collections, got %d", i, scenario.expectTotal, len(collections))
t.Errorf("[%d] Expected %d collections, got %d", i, scenario.expectTotal, len(collections))
}
for _, c := range collections {
if c.Type != scenario.collectionType {
t.Errorf("(%d) Expected collection with type %s, got %s: \n%v", i, scenario.collectionType, c.Type, c)
t.Errorf("[%d] Expected collection with type %s, got %s: \n%v", i, scenario.collectionType, c.Type, c)
}
}
}
@@ -80,11 +80,11 @@ func TestFindCollectionByNameOrId(t *testing.T) {
hasErr := err != nil
if hasErr != scenario.expectError {
t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err)
t.Errorf("[%d] Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err)
}
if model != nil && model.Id != scenario.nameOrId && !strings.EqualFold(model.Name, scenario.nameOrId) {
t.Errorf("(%d) Expected model with identifier %s, got %v", i, scenario.nameOrId, model)
t.Errorf("[%d] Expected model with identifier %s, got %v", i, scenario.nameOrId, model)
}
}
}
@@ -108,7 +108,7 @@ func TestIsCollectionNameUnique(t *testing.T) {
for i, scenario := range scenarios {
result := app.Dao().IsCollectionNameUnique(scenario.name, scenario.excludeId)
if result != scenario.expected {
t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result)
t.Errorf("[%d] Expected %v, got %v", i, scenario.expected, result)
}
}
}
@@ -155,7 +155,7 @@ func TestFindCollectionReferences(t *testing.T) {
}
for i, f := range fields {
if !list.ExistInSlice(f.Name, expectedFields) {
t.Fatalf("(%d) Didn't expect field %v", i, f)
t.Fatalf("[%d] Didn't expect field %v", i, f)
}
}
}
@@ -165,21 +165,29 @@ func TestDeleteCollection(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
c0 := &models.Collection{}
c1, err := app.Dao().FindCollectionByNameOrId("clients")
colEmpty := &models.Collection{}
colAuth, err := app.Dao().FindCollectionByNameOrId("clients")
if err != nil {
t.Fatal(err)
}
c2, err := app.Dao().FindCollectionByNameOrId("demo2")
colReferenced, err := app.Dao().FindCollectionByNameOrId("demo2")
if err != nil {
t.Fatal(err)
}
c3, err := app.Dao().FindCollectionByNameOrId("demo1")
colSystem, err := app.Dao().FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
c3.System = true
if err := app.Dao().Save(c3); err != nil {
colSystem.System = true
if err := app.Dao().Save(colSystem); err != nil {
t.Fatal(err)
}
colView, err := app.Dao().FindCollectionByNameOrId("view1")
if err != nil {
t.Fatal(err)
}
@@ -187,18 +195,28 @@ func TestDeleteCollection(t *testing.T) {
model *models.Collection
expectError bool
}{
{c0, true},
{c1, false},
{c2, true}, // is part of a reference
{c3, true}, // system
{colEmpty, true},
{colAuth, false},
{colReferenced, true},
{colSystem, true},
{colView, false},
}
for i, scenario := range scenarios {
err := app.Dao().DeleteCollection(scenario.model)
hasErr := err != nil
for i, s := range scenarios {
err := app.Dao().DeleteCollection(s.model)
if hasErr != scenario.expectError {
t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%d] Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err)
continue
}
if hasErr {
continue
}
if app.Dao().HasTable(s.model.Name) {
t.Errorf("[%d] Expected table/view %s to be deleted", i, s.model.Name)
}
}
}
@@ -244,7 +262,7 @@ func TestSaveCollectionCreate(t *testing.T) {
}
for i, c := range columns {
if !list.ExistInSlice(c, expectedColumns) {
t.Fatalf("(%d) Didn't expect record column %s", i, c)
t.Fatalf("[%d] Didn't expect record column %s", i, c)
}
}
}
@@ -282,12 +300,14 @@ func TestSaveCollectionUpdate(t *testing.T) {
}
for i, c := range columns {
if !list.ExistInSlice(c, expectedColumns) {
t.Fatalf("(%d) Didn't expect record column %s", i, c)
t.Fatalf("[%d] Didn't expect record column %s", i, c)
}
}
}
func TestImportCollections(t *testing.T) {
totalCollections := 10
scenarios := []struct {
name string
jsonData string
@@ -302,7 +322,7 @@ func TestImportCollections(t *testing.T) {
name: "empty collections",
jsonData: `[]`,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
},
{
name: "minimal collection import",
@@ -312,7 +332,7 @@ func TestImportCollections(t *testing.T) {
]`,
deleteMissing: false,
expectError: false,
expectCollectionsCount: 10,
expectCollectionsCount: totalCollections + 2,
},
{
name: "minimal collection import + failed beforeRecordsSync",
@@ -324,7 +344,7 @@ func TestImportCollections(t *testing.T) {
},
deleteMissing: false,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
},
{
name: "minimal collection import + successful beforeRecordsSync",
@@ -336,7 +356,7 @@ func TestImportCollections(t *testing.T) {
},
deleteMissing: false,
expectError: false,
expectCollectionsCount: 9,
expectCollectionsCount: totalCollections + 1,
},
{
name: "new + update + delete system collection",
@@ -372,7 +392,7 @@ func TestImportCollections(t *testing.T) {
]`,
deleteMissing: true,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
},
{
name: "new + update + delete non-system collection",
@@ -442,11 +462,19 @@ func TestImportCollections(t *testing.T) {
"type":"bool"
}
]
},
{
"id": "test_new_view",
"name": "new_view",
"type": "view",
"options": {
"query": "select id from demo2"
}
}
]`,
deleteMissing: true,
expectError: false,
expectCollectionsCount: 3,
expectCollectionsCount: 4,
},
{
name: "test with deleteMissing: false",
@@ -501,7 +529,7 @@ func TestImportCollections(t *testing.T) {
]`,
deleteMissing: false,
expectError: false,
expectCollectionsCount: 9,
expectCollectionsCount: totalCollections + 1,
afterTestFunc: func(testApp *tests.TestApp, resultCollections []*models.Collection) {
expectedCollectionFields := map[string]int{
"nologin": 1,
@@ -509,7 +537,7 @@ func TestImportCollections(t *testing.T) {
"demo2": 2,
"demo3": 2,
"demo4": 11,
"demo5": 5,
"demo5": 6,
"new_import": 1,
}
for name, expectedCount := range expectedCollectionFields {
+2 -2
View File
@@ -12,8 +12,8 @@ func (dao *Dao) ExternalAuthQuery() *dbx.SelectQuery {
return dao.ModelQuery(&models.ExternalAuth{})
}
/// FindAllExternalAuthsByRecord returns all ExternalAuth models
/// linked to the provided auth record.
// FindAllExternalAuthsByRecord returns all ExternalAuth models
// linked to the provided auth record.
func (dao *Dao) FindAllExternalAuthsByRecord(authRecord *models.Record) ([]*models.ExternalAuth, error) {
auths := []*models.ExternalAuth{}
+99 -27
View File
@@ -20,7 +20,74 @@ func (dao *Dao) RecordQuery(collection *models.Collection) *dbx.SelectQuery {
tableName := collection.Name
selectCols := fmt.Sprintf("%s.*", dao.DB().QuoteSimpleColumnName(tableName))
return dao.DB().Select(selectCols).From(tableName)
return dao.DB().
Select(selectCols).
From(tableName).
WithBuildHook(func(query *dbx.Query) {
query.WithExecHook(execLockRetry(dao.ModelQueryTimeout, dao.MaxLockRetries)).
WithOneHook(func(q *dbx.Query, a any, op func(b any) error) error {
switch v := a.(type) {
case *models.Record:
if v == nil {
return op(a)
}
row := dbx.NullStringMap{}
if err := op(&row); err != nil {
return err
}
record := models.NewRecordFromNullStringMap(collection, row)
*v = *record
return nil
default:
return op(a)
}
}).
WithAllHook(func(q *dbx.Query, sliceA any, op func(sliceB any) error) error {
switch v := sliceA.(type) {
case *[]*models.Record:
if v == nil {
return op(sliceA)
}
rows := []dbx.NullStringMap{}
if err := op(&rows); err != nil {
return err
}
records := models.NewRecordsFromNullStringMaps(collection, rows)
*v = records
return nil
case *[]models.Record:
if v == nil {
return op(sliceA)
}
rows := []dbx.NullStringMap{}
if err := op(&rows); err != nil {
return err
}
records := models.NewRecordsFromNullStringMaps(collection, rows)
nonPointers := make([]models.Record, len(records))
for i, r := range records {
nonPointers[i] = *r
}
*v = nonPointers
return nil
default:
return op(sliceA)
}
})
})
}
// FindRecordById finds the Record model by its id.
@@ -34,10 +101,8 @@ func (dao *Dao) FindRecordById(
return nil, err
}
tableName := collection.Name
query := dao.RecordQuery(collection).
AndWhere(dbx.HashExp{tableName + ".id": recordId})
AndWhere(dbx.HashExp{collection.Name + ".id": recordId})
for _, filter := range optFilters {
if filter == nil {
@@ -48,12 +113,13 @@ func (dao *Dao) FindRecordById(
}
}
row := dbx.NullStringMap{}
if err := query.Limit(1).One(row); err != nil {
record := &models.Record{}
if err := query.Limit(1).One(record); err != nil {
return nil, err
}
return models.NewRecordFromNullStringMap(collection, row), nil
return record, nil
}
// FindRecordsByIds finds all Record models by the provided ids.
@@ -83,12 +149,13 @@ func (dao *Dao) FindRecordsByIds(
}
}
rows := make([]dbx.NullStringMap, 0, len(recordIds))
if err := query.All(&rows); err != nil {
records := make([]*models.Record, 0, len(recordIds))
if err := query.All(&records); err != nil {
return nil, err
}
return models.NewRecordsFromNullStringMaps(collection, rows), nil
return records, nil
}
// FindRecordsByExpr finds all records by the specified db expression.
@@ -117,35 +184,38 @@ func (dao *Dao) FindRecordsByExpr(collectionNameOrId string, exprs ...dbx.Expres
}
}
rows := []dbx.NullStringMap{}
var records []*models.Record
if err := query.All(&rows); err != nil {
if err := query.All(&records); err != nil {
return nil, err
}
return models.NewRecordsFromNullStringMaps(collection, rows), nil
return records, nil
}
// FindFirstRecordByData returns the first found record matching
// the provided key-value pair.
func (dao *Dao) FindFirstRecordByData(collectionNameOrId string, key string, value any) (*models.Record, error) {
func (dao *Dao) FindFirstRecordByData(
collectionNameOrId string,
key string,
value any,
) (*models.Record, error) {
collection, err := dao.FindCollectionByNameOrId(collectionNameOrId)
if err != nil {
return nil, err
}
row := dbx.NullStringMap{}
record := &models.Record{}
err = dao.RecordQuery(collection).
AndWhere(dbx.HashExp{inflector.Columnify(key): value}).
Limit(1).
One(row)
One(record)
if err != nil {
return nil, err
}
return models.NewRecordFromNullStringMap(collection, row), nil
return record, nil
}
// IsRecordValueUnique checks if the provided key-value pair is a unique Record value.
@@ -248,18 +318,17 @@ func (dao *Dao) FindAuthRecordByEmail(collectionNameOrId string, email string) (
return nil, fmt.Errorf("%q is not an auth collection", collectionNameOrId)
}
row := dbx.NullStringMap{}
record := &models.Record{}
err = dao.RecordQuery(collection).
AndWhere(dbx.HashExp{schema.FieldNameEmail: email}).
Limit(1).
One(row)
One(record)
if err != nil {
return nil, err
}
return models.NewRecordFromNullStringMap(collection, row), nil
return record, nil
}
// FindAuthRecordByUsername finds the auth record associated with the provided username (case insensitive).
@@ -274,20 +343,19 @@ func (dao *Dao) FindAuthRecordByUsername(collectionNameOrId string, username str
return nil, fmt.Errorf("%q is not an auth collection", collectionNameOrId)
}
row := dbx.NullStringMap{}
record := &models.Record{}
err = dao.RecordQuery(collection).
AndWhere(dbx.NewExp("LOWER([["+schema.FieldNameUsername+"]])={:username}", dbx.Params{
"username": strings.ToLower(username),
})).
Limit(1).
One(row)
One(record)
if err != nil {
return nil, err
}
return models.NewRecordFromNullStringMap(collection, row), nil
return record, nil
}
// SuggestUniqueAuthRecordUsername checks if the provided username is unique
@@ -396,11 +464,15 @@ func (dao *Dao) cascadeRecordDelete(mainRecord *models.Record, refs map[*models.
uniqueJsonEachAlias := "__je__" + security.PseudorandomString(4)
for refCollection, fields := range refs {
if refCollection.IsView() {
continue // skip view collections
}
for _, field := range fields {
recordTableName := inflector.Columnify(refCollection.Name)
prefixedFieldName := recordTableName + "." + inflector.Columnify(field.Name)
// @todo optimize single relation lookup in v0.12+
// @todo optimize single relation lookup
query := dao.RecordQuery(refCollection).
Distinct(true).
InnerJoin(fmt.Sprintf(
+85
View File
@@ -36,6 +36,91 @@ func TestRecordQuery(t *testing.T) {
}
}
func TestRecordQueryOneWithRecord(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := app.Dao().FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
id := "84nmscqy84lsi1t"
q := app.Dao().RecordQuery(collection).
Where(dbx.HashExp{"id": id})
record := &models.Record{}
if err := q.One(record); err != nil {
t.Fatal(err)
}
if record.GetString("id") != id {
t.Fatalf("Expected record with id %q, got %q", id, record.GetString("id"))
}
}
func TestRecordQueryAllWithRecordsSlices(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
collection, err := app.Dao().FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
id1 := "84nmscqy84lsi1t"
id2 := "al1h9ijdeojtsjy"
{
records := []models.Record{}
q := app.Dao().RecordQuery(collection).
Where(dbx.HashExp{"id": []any{id1, id2}}).
OrderBy("created asc")
if err := q.All(&records); err != nil {
t.Fatal(err)
}
if len(records) != 2 {
t.Fatalf("Expected %d records, got %d", 2, len(records))
}
if records[0].Id != id1 {
t.Fatalf("Expected record with id %q, got %q", id1, records[0].Id)
}
if records[1].Id != id2 {
t.Fatalf("Expected record with id %q, got %q", id2, records[1].Id)
}
}
{
records := []*models.Record{}
q := app.Dao().RecordQuery(collection).
Where(dbx.HashExp{"id": []any{id1, id2}}).
OrderBy("created asc")
if err := q.All(&records); err != nil {
t.Fatal(err)
}
if len(records) != 2 {
t.Fatalf("Expected %d records, got %d", 2, len(records))
}
if records[0].Id != id1 {
t.Fatalf("Expected record with id %q, got %q", id1, records[0].Id)
}
if records[1].Id != id2 {
t.Fatalf("Expected record with id %q, got %q", id2, records[1].Id)
}
}
}
func TestFindRecordById(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
+34 -3
View File
@@ -1,16 +1,19 @@
package daos
import (
"fmt"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
)
// HasTable checks if a table with the provided name exists (case insensitive).
// HasTable checks if a table (or view) with the provided name exists (case insensitive).
func (dao *Dao) HasTable(tableName string) bool {
var exists bool
err := dao.DB().Select("count(*)").
From("sqlite_schema").
AndWhere(dbx.HashExp{"type": "table"}).
AndWhere(dbx.HashExp{"type": []any{"table", "view"}}).
AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:tableName})", dbx.Params{"tableName": tableName})).
Limit(1).
Row(&exists)
@@ -29,9 +32,37 @@ func (dao *Dao) GetTableColumns(tableName string) ([]string, error) {
return columns, err
}
// GetTableInfo returns the `table_info` pragma result for the specified table.
func (dao *Dao) GetTableInfo(tableName string) ([]*models.TableInfoRow, error) {
info := []*models.TableInfoRow{}
err := dao.DB().NewQuery("SELECT * FROM PRAGMA_TABLE_INFO({:tableName})").
Bind(dbx.Params{"tableName": tableName}).
All(&info)
if err != nil {
return nil, err
}
// mattn/go-sqlite3 doesn't throw an error on invalid or missing table
// so we additionally have to check whether the loaded info result is nonempty
if len(info) == 0 {
return nil, fmt.Errorf("empty table info probably due to invalid or missing table %s", tableName)
}
return info, nil
}
// DeleteTable drops the specified table.
//
// This method is a no-op if a table with the provided name doesn't exist.
//
// Be aware that this method is vulnerable to SQL injection and the
// "tableName" argument must come only from trusted input!
func (dao *Dao) DeleteTable(tableName string) error {
_, err := dao.DB().DropTable(tableName).Execute()
_, err := dao.DB().NewQuery(fmt.Sprintf(
"DROP TABLE IF EXISTS {{%s}}",
tableName,
)).Execute()
return err
}
+44 -12
View File
@@ -3,6 +3,7 @@ package daos_test
import (
"context"
"database/sql"
"encoding/json"
"testing"
"time"
@@ -23,12 +24,13 @@ func TestHasTable(t *testing.T) {
{"_admins", true},
{"demo3", true},
{"DEMO3", true}, // table names are case insensitives by default
{"view1", true}, // view
}
for i, scenario := range scenarios {
result := app.Dao().HasTable(scenario.tableName)
if result != scenario.expected {
t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result)
t.Errorf("[%d] Expected %v, got %v", i, scenario.expected, result)
}
}
}
@@ -45,21 +47,50 @@ func TestGetTableColumns(t *testing.T) {
{"_params", []string{"id", "key", "value", "created", "updated"}},
}
for i, scenario := range scenarios {
columns, _ := app.Dao().GetTableColumns(scenario.tableName)
for i, s := range scenarios {
columns, _ := app.Dao().GetTableColumns(s.tableName)
if len(columns) != len(scenario.expected) {
t.Errorf("(%d) Expected columns %v, got %v", i, scenario.expected, columns)
if len(columns) != len(s.expected) {
t.Errorf("[%d] Expected columns %v, got %v", i, s.expected, columns)
continue
}
for _, c := range columns {
if !list.ExistInSlice(c, scenario.expected) {
t.Errorf("(%d) Didn't expect column %s", i, c)
if !list.ExistInSlice(c, s.expected) {
t.Errorf("[%d] Didn't expect column %s", i, c)
}
}
}
}
func TestGetTableInfo(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
tableName string
expected string
}{
{"", "null"},
{"missing", "null"},
{
"_admins",
`[{"PK":1,"Index":0,"Name":"id","Type":"TEXT","NotNull":false,"DefaultValue":null},{"PK":0,"Index":1,"Name":"avatar","Type":"INTEGER","NotNull":true,"DefaultValue":0},{"PK":0,"Index":2,"Name":"email","Type":"TEXT","NotNull":true,"DefaultValue":null},{"PK":0,"Index":3,"Name":"tokenKey","Type":"TEXT","NotNull":true,"DefaultValue":null},{"PK":0,"Index":4,"Name":"passwordHash","Type":"TEXT","NotNull":true,"DefaultValue":null},{"PK":0,"Index":5,"Name":"lastResetSentAt","Type":"TEXT","NotNull":true,"DefaultValue":""},{"PK":0,"Index":6,"Name":"created","Type":"TEXT","NotNull":true,"DefaultValue":""},{"PK":0,"Index":7,"Name":"updated","Type":"TEXT","NotNull":true,"DefaultValue":""}]`,
},
}
for i, s := range scenarios {
rows, _ := app.Dao().GetTableInfo(s.tableName)
raw, _ := json.Marshal(rows)
rawStr := string(raw)
if rawStr != s.expected {
t.Errorf("[%d] Expected \n%v, \ngot \n%v", i, s.expected, rawStr)
}
}
}
func TestDeleteTable(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
@@ -69,16 +100,17 @@ func TestDeleteTable(t *testing.T) {
expectError bool
}{
{"", true},
{"test", true},
{"test", false}, // missing tables are ignored
{"_admins", false},
{"demo3", false},
}
for i, scenario := range scenarios {
err := app.Dao().DeleteTable(scenario.tableName)
for i, s := range scenarios {
err := app.Dao().DeleteTable(s.tableName)
hasErr := err != nil
if hasErr != scenario.expectError {
t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr)
if hasErr != s.expectError {
t.Errorf("[%d] Expected hasErr %v, got %v", i, s.expectError, hasErr)
}
}
}
+583
View File
@@ -0,0 +1,583 @@
package daos
import (
"errors"
"fmt"
"io"
"regexp"
"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/security"
"github.com/pocketbase/pocketbase/tools/tokenizer"
"github.com/pocketbase/pocketbase/tools/types"
)
// DeleteView drops the specified view name.
//
// This method is a no-op if a view with the provided name doesn't exist.
//
// Be aware that this method is vulnerable to SQL injection and the
// "name" argument must come only from trusted input!
func (dao *Dao) DeleteView(name string) error {
_, err := dao.DB().NewQuery(fmt.Sprintf(
"DROP VIEW IF EXISTS {{%s}}",
name,
)).Execute()
return err
}
// SaveView creates (or updates already existing) persistent SQL view.
//
// Be aware that this method is vulnerable to SQL injection and the
// "selectQuery" argument must come only from trusted input!
func (dao *Dao) SaveView(name string, selectQuery string) error {
return dao.RunInTransaction(func(txDao *Dao) error {
// delete old view (if exists)
if err := txDao.DeleteView(name); err != nil {
return err
}
trimmed := strings.Trim(selectQuery, ";")
// try to eagerly detect multiple inline statements
tk := tokenizer.NewFromString(trimmed)
tk.Separators(';')
if queryParts, _ := tk.ScanAll(); len(queryParts) > 1 {
return errors.New("multiple statements are not supported")
}
// (re)create the view
//
// note: the query is wrapped in a secondary SELECT as a rudimentary
// measure to discourage multiple inline sql statements execution.
viewQuery := fmt.Sprintf("CREATE VIEW {{%s}} AS SELECT * FROM (%s)", name, trimmed)
if _, err := txDao.DB().NewQuery(viewQuery).Execute(); err != nil {
return err
}
// fetch the view table info to ensure that the view was created
// because missing tables or columns won't return an error
if _, err := txDao.GetTableInfo(name); err != nil {
return err
}
return nil
})
}
// CreateViewSchema creates a new view schema from the provided select query.
//
// There are some caveats:
// - The select query must have an "id" column.
// - Wildcard ("*") columns are not supported to avoid accidentally leaking sensitive data.
func (dao *Dao) CreateViewSchema(selectQuery string) (schema.Schema, error) {
result := schema.NewSchema()
suggestedFields, err := dao.parseQueryToFields(selectQuery)
if err != nil {
return result, err
}
// note wrap in a transaction in case the selectQuery contains
// multiple statements allowing us to rollback on any error
txErr := dao.RunInTransaction(func(txDao *Dao) error {
tempView := "_temp_" + security.PseudorandomString(5)
// create a temp view with the provided query
if err := txDao.SaveView(tempView, selectQuery); err != nil {
return err
}
defer txDao.DeleteView(tempView)
// extract the generated view table info
info, err := txDao.GetTableInfo(tempView)
if err != nil {
return err
}
var hasId bool
for _, row := range info {
if row.Name == schema.FieldNameId {
hasId = true
}
if list.ExistInSlice(row.Name, schema.BaseModelFieldNames()) {
continue // skip base model fields since they are not part of the schema
}
var field *schema.SchemaField
if f, ok := suggestedFields[row.Name]; ok {
field = f.field
} else {
field = defaultViewField(row.Name)
}
result.AddField(field)
}
if !hasId {
return errors.New("missing required id column (you ca use `(ROW_NUMBER() OVER()) as id` if you don't have one)")
}
return nil
})
return result, txErr
}
// FindRecordByViewFile returns the original models.Record of the
// provided view collection file.
func (dao *Dao) FindRecordByViewFile(
viewCollectionNameOrId string,
fileFieldName string,
filename string,
) (*models.Record, error) {
view, err := dao.FindCollectionByNameOrId(viewCollectionNameOrId)
if err != nil {
return nil, err
}
if !view.IsView() {
return nil, errors.New("not a view collection")
}
var findFirstNonViewQueryFileField func(int) (*queryField, error)
findFirstNonViewQueryFileField = func(level int) (*queryField, error) {
// check the level depth to prevent infinite circular recursion
// (the limit is arbitrary and may change in the future)
if level > 5 {
return nil, errors.New("reached the max recursion level of view collection file field queries")
}
queryFields, err := dao.parseQueryToFields(view.ViewOptions().Query)
if err != nil {
return nil, err
}
for _, item := range queryFields {
if item.collection == nil ||
item.original == nil ||
item.field.Name != fileFieldName {
continue
}
if item.collection.IsView() {
view = item.collection
fileFieldName = item.original.Name
return findFirstNonViewQueryFileField(level + 1)
}
return item, nil
}
return nil, errors.New("no query file field found")
}
qf, err := findFirstNonViewQueryFileField(1)
if err != nil {
return nil, err
}
cleanFieldName := inflector.Columnify(qf.original.Name)
record := &models.Record{}
err = dao.RecordQuery(qf.collection).
InnerJoin(fmt.Sprintf(
// note: the case is used to normalize the value access
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) as {{_je_file}}`,
cleanFieldName, cleanFieldName, cleanFieldName,
), dbx.HashExp{"_je_file.value": filename}).
Limit(1).
One(record)
if err != nil {
return nil, err
}
return record, nil
}
// -------------------------------------------------------------------
// Raw query to schema helpers
// -------------------------------------------------------------------
type queryField struct {
// field is the final resolved field.
field *schema.SchemaField
// collection refers to the original field's collection model.
// It could be nil if the found query field is not from a collection schema.
collection *models.Collection
// original is the original found collection field.
// It could be nil if the found query field is not from a collection schema.
original *schema.SchemaField
}
func defaultViewField(name string) *schema.SchemaField {
return &schema.SchemaField{
Name: name,
Type: schema.FieldTypeJson,
}
}
func (dao *Dao) parseQueryToFields(selectQuery string) (map[string]*queryField, error) {
p := new(identifiersParser)
if err := p.parse(selectQuery); err != nil {
return nil, err
}
collections, err := dao.findCollectionsByIdentifiers(p.tables)
if err != nil {
return nil, err
}
result := make(map[string]*queryField, len(p.columns))
var mainTable identifier
if len(p.tables) > 0 {
mainTable = p.tables[0]
}
for _, col := range p.columns {
colLower := strings.ToLower(col.original)
// numeric expression cast
if strings.Contains(colLower, "(") &&
(strings.HasPrefix(colLower, "count(") ||
strings.HasPrefix(colLower, "total(") ||
strings.Contains(colLower, " as numeric") ||
strings.Contains(colLower, " as real") ||
strings.Contains(colLower, " as int") ||
strings.Contains(colLower, " as integer") ||
strings.Contains(colLower, " as decimal")) {
result[col.alias] = &queryField{
field: &schema.SchemaField{
Name: col.alias,
Type: schema.FieldTypeNumber,
},
}
continue
}
parts := strings.Split(col.original, ".")
var fieldName string
var collection *models.Collection
var isMainTableField bool
if len(parts) == 2 {
fieldName = parts[1]
collection = collections[parts[0]]
isMainTableField = parts[0] == mainTable.alias
} else {
fieldName = parts[0]
collection = collections[mainTable.alias]
isMainTableField = true
}
// fallback to the default field if the found column is not from a collection schema
if collection == nil {
result[col.alias] = &queryField{
field: defaultViewField(col.alias),
}
continue
}
if fieldName == "*" {
return nil, errors.New("dynamic column names are not supported")
}
// find the first field by name (case insensitive)
var field *schema.SchemaField
for _, f := range collection.Schema.Fields() {
if strings.EqualFold(f.Name, fieldName) {
field = f
break
}
}
if field != nil {
clone := *field
clone.Name = col.alias
result[col.alias] = &queryField{
field: &clone,
collection: collection,
original: field,
}
continue
}
if fieldName == schema.FieldNameId && !isMainTableField {
// convert to relation since it is a direct id reference to non-maintable collection
result[col.alias] = &queryField{
field: &schema.SchemaField{
Name: col.alias,
Type: schema.FieldTypeRelation,
Options: &schema.RelationOptions{
MaxSelect: types.Pointer(1),
CollectionId: collection.Id,
},
},
collection: collection,
}
} else if fieldName == schema.FieldNameCreated || fieldName == schema.FieldNameUpdated {
result[col.alias] = &queryField{
field: &schema.SchemaField{
Name: col.alias,
Type: schema.FieldTypeDate,
},
collection: collection,
}
} else if fieldName == schema.FieldNameUsername && collection.IsAuth() {
result[col.alias] = &queryField{
field: &schema.SchemaField{
Name: col.alias,
Type: schema.FieldTypeText,
},
collection: collection,
}
} else if fieldName == schema.FieldNameEmail && collection.IsAuth() {
result[col.alias] = &queryField{
field: &schema.SchemaField{
Name: col.alias,
Type: schema.FieldTypeEmail,
},
collection: collection,
}
} else if (fieldName == schema.FieldNameVerified || fieldName == schema.FieldNameEmailVisibility) && collection.IsAuth() {
result[col.alias] = &queryField{
field: &schema.SchemaField{
Name: col.alias,
Type: schema.FieldTypeBool,
},
collection: collection,
}
} else {
result[col.alias] = &queryField{
field: defaultViewField(col.alias),
collection: collection,
}
}
}
return result, nil
}
func (dao *Dao) findCollectionsByIdentifiers(tables []identifier) (map[string]*models.Collection, error) {
names := make([]any, 0, len(tables))
for _, table := range tables {
if strings.Contains(table.alias, "(") {
continue // skip expressions
}
names = append(names, table.original)
}
if len(names) == 0 {
return nil, nil
}
result := make(map[string]*models.Collection, len(names))
collections := make([]*models.Collection, 0, len(names))
err := dao.CollectionQuery().
AndWhere(dbx.In("name", names...)).
All(&collections)
if err != nil {
return nil, err
}
for _, table := range tables {
for _, collection := range collections {
if collection.Name == table.original {
result[table.alias] = collection
}
}
}
return result, nil
}
// -------------------------------------------------------------------
// Raw query identifiers parser
// -------------------------------------------------------------------
var joinReplaceRegex = regexp.MustCompile(`(?im)\s+(inner join|outer join|left join|right join|join)\s+?`)
var discardReplaceRegex = regexp.MustCompile(`(?im)\s+(where|group by|having|order|limit|with)\s+?`)
var commentsReplaceRegex = regexp.MustCompile(`(?m)(\/\*[\s\S]+\*\/)|(--.+$)`)
type identifier struct {
original string
alias string
}
type identifiersParser struct {
columns []identifier
tables []identifier
}
func (p *identifiersParser) parse(selectQuery string) error {
str := strings.Trim(selectQuery, ";")
str = joinReplaceRegex.ReplaceAllString(str, " _join_ ")
str = discardReplaceRegex.ReplaceAllString(str, " _discard_ ")
str = commentsReplaceRegex.ReplaceAllString(str, "")
tk := tokenizer.NewFromString(str)
tk.Separators(',', ' ', '\n', '\t')
tk.KeepSeparator(true)
var skip bool
var partType string
var activeBuilder *strings.Builder
var selectParts strings.Builder
var fromParts strings.Builder
var joinParts strings.Builder
for {
token, err := tk.Scan()
if err != nil {
if err != io.EOF {
return err
}
break
}
trimmed := strings.ToLower(strings.TrimSpace(token))
switch trimmed {
case "select":
skip = false
partType = "select"
activeBuilder = &selectParts
case "from":
skip = false
partType = "from"
activeBuilder = &fromParts
case "_join_":
skip = false
// the previous part was also a join
if partType == "join" {
joinParts.WriteString(",")
}
partType = "join"
activeBuilder = &joinParts
case "_discard_":
// do nothing...
skip = true
default:
isJoin := partType == "join"
if isJoin && trimmed == "on" {
skip = true
}
if !skip && activeBuilder != nil {
activeBuilder.WriteString(" ")
activeBuilder.WriteString(token)
}
}
}
selects, err := extractIdentifiers(selectParts.String())
if err != nil {
return err
}
froms, err := extractIdentifiers(fromParts.String())
if err != nil {
return err
}
joins, err := extractIdentifiers(joinParts.String())
if err != nil {
return err
}
p.columns = selects
p.tables = froms
p.tables = append(p.tables, joins...)
return nil
}
func extractIdentifiers(rawExpression string) ([]identifier, error) {
rawTk := tokenizer.NewFromString(rawExpression)
rawTk.Separators(',')
rawIdentifiers, err := rawTk.ScanAll()
if err != nil {
return nil, err
}
result := make([]identifier, 0, len(rawIdentifiers))
for _, rawIdentifier := range rawIdentifiers {
tk := tokenizer.NewFromString(rawIdentifier)
tk.Separators(' ', '\n', '\t')
parts, err := tk.ScanAll()
if err != nil {
return nil, err
}
resolved, err := identifierFromParts(parts)
if err != nil {
return nil, err
}
result = append(result, resolved)
}
return result, nil
}
func identifierFromParts(parts []string) (identifier, error) {
var result identifier
switch len(parts) {
case 3:
if !strings.EqualFold(parts[1], "as") {
return result, fmt.Errorf(`invalid identifier part - expected "as", got %v`, parts[1])
}
result.original = parts[0]
result.alias = parts[2]
case 2:
result.original = parts[0]
result.alias = parts[1]
case 1:
subParts := strings.Split(parts[0], ".")
result.original = parts[0]
result.alias = subParts[len(subParts)-1]
default:
return result, fmt.Errorf(`invalid identifier parts %v`, parts)
}
result.original = trimRawIdentifier(result.original)
result.alias = trimRawIdentifier(result.alias)
return result, nil
}
func trimRawIdentifier(rawIdentifier string) string {
const trimChars = "`\"[];"
parts := strings.Split(rawIdentifier, ".")
for i := range parts {
parts[i] = strings.Trim(parts[i], trimChars)
}
return strings.Join(parts, ".")
}
+539
View File
@@ -0,0 +1,539 @@
package daos_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/list"
)
func TestDeleteView(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
viewName string
expectError bool
}{
{"", true},
{"demo1", true}, // not a view table
{"missing", false}, // missing or already deleted
{"view1", false}, // existing
{"VieW1", false}, // view names are case insensitives
}
for i, s := range scenarios {
err := app.Dao().DeleteView(s.viewName)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%d - %q] Expected hasErr %v, got %v (%v)", i, s.viewName, s.expectError, hasErr, err)
}
}
}
func TestSaveView(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
scenarioName string
viewName string
query string
expectError bool
expectColumns []string
}{
{
"empty name and query",
"",
"",
true,
nil,
},
{
"empty name",
"",
"select * from _admins",
true,
nil,
},
{
"empty query",
"123Test",
"",
true,
nil,
},
{
"invalid query",
"123Test",
"123 456",
true,
nil,
},
{
"missing table",
"123Test",
"select * from missing",
true,
nil,
},
{
"non select query",
"123Test",
"drop table _admins",
true,
nil,
},
{
"multiple select queries",
"123Test",
"select *, count(id) as c from _admins; select * from demo1;",
true,
nil,
},
{
"try to break the parent parenthesis",
"123Test",
"select *, count(id) as c from `_admins`)",
true,
nil,
},
{
"simple select query (+ trimmed semicolon)",
"123Test",
";select *, count(id) as c from _admins;",
false,
[]string{
"id", "created", "updated",
"passwordHash", "tokenKey", "email",
"lastResetSentAt", "avatar", "c",
},
},
{
"update old view with new query",
"123Test",
"select 1 as test from _admins",
false,
[]string{"test"},
},
}
for _, s := range scenarios {
err := app.Dao().SaveView(s.viewName, s.query)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.scenarioName, s.expectError, hasErr, err)
continue
}
if hasErr {
continue
}
infoRows, err := app.Dao().GetTableInfo(s.viewName)
if err != nil {
t.Errorf("[%s] Failed to fetch table info for %s: %v", s.scenarioName, s.viewName, err)
continue
}
if len(s.expectColumns) != len(infoRows) {
t.Errorf("[%s] Expected %d columns, got %d", s.scenarioName, len(s.expectColumns), len(infoRows))
continue
}
for _, row := range infoRows {
if !list.ExistInSlice(row.Name, s.expectColumns) {
t.Errorf("[%s] Missing %q column in %v", s.scenarioName, row.Name, s.expectColumns)
}
}
}
}
func TestCreateViewSchema(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
scenarios := []struct {
name string
query string
expectError bool
expectFields map[string]string // name-type pairs
}{
{
"empty query",
"",
true,
nil,
},
{
"invalid query",
"test 123456",
true,
nil,
},
{
"missing table",
"select * from missing",
true,
nil,
},
{
"query with wildcard column",
"select a.id, a.* from demo1 a",
true,
nil,
},
{
"query without id",
"select text, url, created, updated from demo1",
true,
nil,
},
{
"query with comments",
`
select
-- test single line
id,
text,
/* multi
line comment */
url, created, updated from demo1
`,
false,
map[string]string{
"text": schema.FieldTypeText,
"url": schema.FieldTypeUrl,
},
},
{
"query with all fields and quoted identifiers",
`
select
"id",
"created",
"updated",
[text],
` + "`bool`" + `,
"url",
"select_one",
"select_many",
"file_one",
"demo1"."file_many",
` + "`demo1`." + "`number`" + ` number_alias,
"email",
"datetime",
"json",
"rel_one",
"rel_many"
from demo1
`,
false,
map[string]string{
"text": schema.FieldTypeText,
"bool": schema.FieldTypeBool,
"url": schema.FieldTypeUrl,
"select_one": schema.FieldTypeSelect,
"select_many": schema.FieldTypeSelect,
"file_one": schema.FieldTypeFile,
"file_many": schema.FieldTypeFile,
"number_alias": schema.FieldTypeNumber,
"email": schema.FieldTypeEmail,
"datetime": schema.FieldTypeDate,
"json": schema.FieldTypeJson,
"rel_one": schema.FieldTypeRelation,
"rel_many": schema.FieldTypeRelation,
},
},
{
"query with indirect relations fields",
"select a.id, b.id as bid, b.created from demo1 as a left join demo2 b",
false,
map[string]string{
"bid": schema.FieldTypeRelation,
},
},
{
"query with multiple froms, joins and style of aliasses",
`
select
a.id as id,
b.id as bid,
lj.id cid,
ij.id as did,
a.bool,
_admins.id as eid,
_admins.email
from demo1 a, demo2 as b
left join demo3 lj on lj.id = 123
inner join demo4 as ij on ij.id = 123
join _admins
where 1=1
group by a.id
limit 10
`,
false,
map[string]string{
"bid": schema.FieldTypeRelation,
"cid": schema.FieldTypeRelation,
"did": schema.FieldTypeRelation,
"bool": schema.FieldTypeBool,
"eid": schema.FieldTypeJson, // not from collection
"email": schema.FieldTypeJson, // not from collection
},
},
{
"query with numeric casts",
`select
a.id,
count(a.id) count,
cast(a.id as int) cast_int,
cast(a.id as integer) cast_integer,
cast(a.id as real) cast_real,
cast(a.id as decimal) cast_decimal,
cast(a.id as numeric) cast_numeric,
avg(a.id) avg,
sum(a.id) sum,
total(a.id) total,
min(a.id) min,
max(a.id) max
from demo1 a`,
false,
map[string]string{
"count": schema.FieldTypeNumber,
"total": schema.FieldTypeNumber,
"cast_int": schema.FieldTypeNumber,
"cast_integer": schema.FieldTypeNumber,
"cast_real": schema.FieldTypeNumber,
"cast_decimal": schema.FieldTypeNumber,
"cast_numeric": schema.FieldTypeNumber,
// json because they are nullable
"sum": schema.FieldTypeJson,
"avg": schema.FieldTypeJson,
"min": schema.FieldTypeJson,
"max": schema.FieldTypeJson,
},
},
{
"query with reserved auth collection fields",
`
select
a.id,
a.username,
a.email,
a.emailVisibility,
a.verified,
demo1.id relid
from users a
left join demo1
`,
false,
map[string]string{
"username": schema.FieldTypeText,
"email": schema.FieldTypeEmail,
"emailVisibility": schema.FieldTypeBool,
"verified": schema.FieldTypeBool,
"relid": schema.FieldTypeRelation,
},
},
{
"query with unknown fields and aliases",
`select
id,
id as id2,
text as text_alias,
url as url_alias,
"demo1"."bool" as bool_alias,
number as number_alias,
created created_alias,
updated updated_alias,
123 as custom
from demo1
`,
false,
map[string]string{
"id2": schema.FieldTypeJson,
"text_alias": schema.FieldTypeText,
"url_alias": schema.FieldTypeUrl,
"bool_alias": schema.FieldTypeBool,
"number_alias": schema.FieldTypeNumber,
"created_alias": schema.FieldTypeDate,
"updated_alias": schema.FieldTypeDate,
"custom": schema.FieldTypeJson,
},
},
}
for _, s := range scenarios {
result, err := app.Dao().CreateViewSchema(s.query)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
continue
}
if hasErr {
continue
}
if len(s.expectFields) != len(result.Fields()) {
serialized, _ := json.Marshal(result)
t.Errorf("[%s] Expected %d fields, got %d: \n%s", s.name, len(s.expectFields), len(result.Fields()), serialized)
continue
}
for name, typ := range s.expectFields {
field := result.GetFieldByName(name)
if field == nil {
t.Errorf("[%s] Expected to find field %s, got nil", s.name, name)
continue
}
if field.Type != typ {
t.Errorf("[%s] Expected field %s to be %q, got %s", s.name, name, typ, field.Type)
continue
}
}
}
}
func TestFindRecordByViewFile(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
prevCollection, err := app.Dao().FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
totalLevels := 6
// create collection view mocks
fileOneAlias := "file_one one0"
fileManyAlias := "file_many many0"
mockCollections := make([]*models.Collection, 0, totalLevels)
for i := 0; i <= totalLevels; i++ {
view := new(models.Collection)
view.Type = models.CollectionTypeView
view.Name = fmt.Sprintf("_test_view%d", i)
view.SetOptions(&models.CollectionViewOptions{
Query: fmt.Sprintf(
"select id, %s, %s from %s",
fileOneAlias,
fileManyAlias,
prevCollection.Name,
),
})
// save view
if err := app.Dao().SaveCollection(view); err != nil {
t.Fatalf("Failed to save view%d: %v", i, err)
}
mockCollections = append(mockCollections, view)
prevCollection = view
fileOneAlias = fmt.Sprintf("one%d one%d", i, i+1)
fileManyAlias = fmt.Sprintf("many%d many%d", i, i+1)
}
fileOneName := "test_d61b33QdDU.txt"
fileManyName := "test_QZFjKjXchk.txt"
expectedRecordId := "84nmscqy84lsi1t"
scenarios := []struct {
name string
collectionNameOrId string
fileFieldName string
filename string
expectError bool
expectRecordId string
}{
{
"missing collection",
"missing",
"a",
fileOneName,
true,
"",
},
{
"non-view collection",
"demo1",
"file_one",
fileOneName,
true,
"",
},
{
"view collection after the max recursion limit",
mockCollections[totalLevels-1].Name,
fmt.Sprintf("one%d", totalLevels-1),
fileOneName,
true,
"",
},
{
"first view collection (single file)",
mockCollections[0].Name,
"one0",
fileOneName,
false,
expectedRecordId,
},
{
"first view collection (many files)",
mockCollections[0].Name,
"many0",
fileManyName,
false,
expectedRecordId,
},
{
"last view collection before the recursion limit (single file)",
mockCollections[totalLevels-2].Name,
fmt.Sprintf("one%d", totalLevels-2),
fileOneName,
false,
expectedRecordId,
},
{
"last view collection before the recursion limit (many files)",
mockCollections[totalLevels-2].Name,
fmt.Sprintf("many%d", totalLevels-2),
fileManyName,
false,
expectedRecordId,
},
}
for _, s := range scenarios {
record, err := app.Dao().FindRecordByViewFile(
s.collectionNameOrId,
s.fileFieldName,
s.filename,
)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
continue
}
if hasErr {
continue
}
if record.Id != s.expectRecordId {
t.Errorf("[%s] Expected recordId %q, got %q", s.name, s.expectRecordId, record.Id)
}
}
}
+14
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
@@ -52,6 +53,14 @@ func main() {
"fallback the request to index.html on missing static path (eg. when pretty urls are used with SPA)",
)
var queryTimeout int
app.RootCmd.PersistentFlags().IntVar(
&queryTimeout,
"queryTimeout",
30,
"the default SELECT queries timeout in seconds",
)
app.RootCmd.ParseFlags(os.Args[1:])
// ---------------------------------------------------------------
@@ -70,6 +79,11 @@ func main() {
Dir: migrationsDir,
})
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
app.Dao().ModelQueryTimeout = time.Duration(queryTimeout) * time.Second
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// serves static files from the provided public dir (if exists)
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS(publicDir), indexFallback))
+105 -26
View File
@@ -69,7 +69,7 @@ func NewCollectionUpsert(app core.App, collection *models.Collection) *Collectio
}
clone, _ := form.collection.Schema.Clone()
if clone != nil {
if clone != nil && form.Type != models.CollectionTypeView {
form.Schema = *clone
} else {
form.Schema = schema.Schema{}
@@ -86,6 +86,16 @@ func (form *CollectionUpsert) SetDao(dao *daos.Dao) {
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *CollectionUpsert) Validate() error {
isAuth := form.Type == models.CollectionTypeAuth
isView := form.Type == models.CollectionTypeView
// generate schema from the query (overwriting any explicit user defined schema)
if isView {
options := models.CollectionViewOptions{}
if err := decodeOptions(form.Options, &options); err != nil {
return err
}
form.Schema, _ = form.dao.CreateViewSchema(options.Query)
}
return validation.ValidateStruct(form,
validation.Field(
@@ -104,7 +114,11 @@ func (form *CollectionUpsert) Validate() error {
validation.Field(
&form.Type,
validation.Required,
validation.In(models.CollectionTypeAuth, models.CollectionTypeBase),
validation.In(
models.CollectionTypeBase,
models.CollectionTypeAuth,
models.CollectionTypeView,
),
validation.By(form.ensureNoTypeChange),
),
validation.Field(
@@ -115,23 +129,32 @@ func (form *CollectionUpsert) Validate() error {
validation.By(form.ensureNoSystemNameChange),
validation.By(form.checkUniqueName),
),
// validates using the type's own validation rules + some collection's specific
// validates using the type's own validation rules + some collection's specifics
validation.Field(
&form.Schema,
validation.By(form.checkMinSchemaFields),
validation.By(form.ensureNoSystemFieldsChange),
validation.By(form.ensureNoFieldsTypeChange),
validation.By(form.checkRelationFields),
validation.When(
isAuth,
validation.By(form.ensureNoAuthFieldName),
),
validation.When(isAuth, validation.By(form.ensureNoAuthFieldName)),
),
validation.Field(&form.ListRule, validation.By(form.checkRule)),
validation.Field(&form.ViewRule, validation.By(form.checkRule)),
validation.Field(&form.CreateRule, validation.By(form.checkRule)),
validation.Field(&form.UpdateRule, validation.By(form.checkRule)),
validation.Field(&form.DeleteRule, validation.By(form.checkRule)),
validation.Field(
&form.CreateRule,
validation.When(isView, validation.Nil),
validation.By(form.checkRule),
),
validation.Field(
&form.UpdateRule,
validation.When(isView, validation.Nil),
validation.By(form.checkRule),
),
validation.Field(
&form.DeleteRule,
validation.When(isView, validation.Nil),
validation.By(form.checkRule),
),
validation.Field(&form.Options, validation.By(form.checkOptions)),
)
}
@@ -222,7 +245,28 @@ func (form *CollectionUpsert) checkRelationFields(value any) error {
options, _ := field.Options.(*schema.RelationOptions)
if options == nil {
continue
return validation.Errors{fmt.Sprint(i): validation.Errors{
"options": validation.NewError(
"validation_schema_invalid_relation_field_options",
"The relation field has invalid field options.",
)},
}
}
// prevent collectionId change
oldField := form.collection.Schema.GetFieldById(field.Id)
if oldField != nil {
oldOptions, _ := oldField.Options.(*schema.RelationOptions)
if oldOptions != nil && oldOptions.CollectionId != options.CollectionId {
return validation.Errors{fmt.Sprint(i): validation.Errors{
"options": validation.Errors{
"collectionId": validation.NewError(
"validation_field_relation_change",
"The relation collection cannot be changed.",
),
}},
}
}
}
collection, err := form.dao.FindCollectionByNameOrId(options.CollectionId)
@@ -288,13 +332,15 @@ func (form *CollectionUpsert) ensureNoAuthFieldName(value any) error {
}
func (form *CollectionUpsert) checkMinSchemaFields(value any) error {
if form.Type == models.CollectionTypeAuth {
return nil // auth collections doesn't require having additional schema fields
}
v, _ := value.(schema.Schema)
v, ok := value.(schema.Schema)
if !ok || len(v.Fields()) == 0 {
return validation.ErrRequired
switch form.Type {
case models.CollectionTypeAuth, models.CollectionTypeView:
return nil // no schema fields constraint
default:
if len(v.Fields()) == 0 {
return validation.ErrRequired
}
}
return nil
@@ -343,15 +389,11 @@ func (form *CollectionUpsert) checkRule(value any) error {
func (form *CollectionUpsert) checkOptions(value any) error {
v, _ := value.(types.JsonMap)
if form.Type == models.CollectionTypeAuth {
raw, err := v.MarshalJSON()
if err != nil {
return validation.NewError("validation_invalid_options", "Invalid options.")
}
switch form.Type {
case models.CollectionTypeAuth:
options := models.CollectionAuthOptions{}
if err := json.Unmarshal(raw, &options); err != nil {
return validation.NewError("validation_invalid_options", "Invalid options.")
if err := decodeOptions(v, &options); err != nil {
return err
}
// check the generic validations
@@ -363,6 +405,39 @@ func (form *CollectionUpsert) checkOptions(value any) error {
if err := form.checkRule(options.ManageRule); err != nil {
return validation.Errors{"manageRule": err}
}
case models.CollectionTypeView:
options := models.CollectionViewOptions{}
if err := decodeOptions(v, &options); err != nil {
return err
}
// check the generic validations
if err := options.Validate(); err != nil {
return err
}
// check the query option
if _, err := form.dao.CreateViewSchema(options.Query); err != nil {
return validation.Errors{
"query": validation.NewError(
"validation_invalid_view_query",
fmt.Sprintf("Invalid query - %s", err.Error()),
),
}
}
}
return nil
}
func decodeOptions(options types.JsonMap, result any) error {
raw, err := options.MarshalJSON()
if err != nil {
return validation.NewError("validation_invalid_options", "Invalid options.")
}
if err := json.Unmarshal(raw, result); err != nil {
return validation.NewError("validation_invalid_options", "Invalid options.")
}
return nil
@@ -398,7 +473,11 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc[*models.Col
form.collection.Name = form.Name
}
form.collection.Schema = form.Schema
// view schema is autogenerated on save
if !form.collection.IsView() {
form.collection.Schema = form.Schema
}
form.collection.ListRule = form.ListRule
form.collection.ViewRule = form.ViewRule
form.collection.CreateRule = form.CreateRule
+139 -6
View File
@@ -22,7 +22,7 @@ func TestNewCollectionUpsert(t *testing.T) {
collection.Name = "test_name"
collection.Type = "test_type"
collection.System = true
listRule := "testview"
listRule := "test_list"
collection.ListRule = &listRule
viewRule := "test_view"
collection.ViewRule = &viewRule
@@ -98,6 +98,7 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
}{
{"empty create (base)", "", "{}", []string{"name", "schema"}},
{"empty create (auth)", "", `{"type":"auth"}`, []string{"name"}},
{"empty create (view)", "", `{"type":"view"}`, []string{"name", "options"}},
{"empty update", "demo2", "{}", []string{}},
{
"create failure",
@@ -188,7 +189,7 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
[]string{"schema"},
},
{
"create failure - check type options validators",
"create failure - check auth options validators",
"",
`{
"name": "test_new",
@@ -200,6 +201,16 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
}`,
[]string{"options"},
},
{
"create failure - check view options validators",
"",
`{
"name": "test_new",
"type": "view",
"options": { "query": "invalid query" }
}`,
[]string{"options"},
},
{
"create success",
"",
@@ -280,6 +291,26 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
}`,
[]string{"type"},
},
{
"update failure - changing relation collection",
"users",
`{
"schema": [
{
"id": "lkeigvv3",
"name": "rel",
"type": "relation",
"options": {
"collectionId": "wzlqyes4orhoygb",
"cascadeDelete": false,
"maxSelect": 1,
"displayFields": null
}
}
]
}`,
[]string{"schema"},
},
{
"update failure - all fields",
"demo2",
@@ -356,6 +387,99 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
}`,
[]string{},
},
// view tests
// -----------------------------------------------------------
{
"view create failure",
"",
`{
"name": "upsert_view",
"type": "view",
"listRule": "id='123' && verified = true",
"viewRule": "id='123' && emailVisibility = true",
"schema": [
{"id":"abc123","name":"some invalid field name that will be overwritten !@#$","type":"bool"}
],
"options": {
"query": "select id, email from users; drop table _admins;"
}
}`,
[]string{
"listRule",
"viewRule",
"options",
},
},
{
"view create success",
"",
`{
"name": "upsert_view",
"type": "view",
"listRule": "id='123' && verified = true",
"viewRule": "id='123' && emailVisibility = true",
"schema": [
{"id":"abc123","name":"some invalid field name that will be overwritten !@#$","type":"bool"}
],
"options": {
"query": "select id, emailVisibility, verified from users"
}
}`,
[]string{
// "schema", should be overwritten by an autogenerated from the query
},
},
{
"view update failure (schema autogeneration and rule fields check)",
"upsert_view",
`{
"name": "upsert_view_2",
"listRule": "id='456' && verified = true",
"viewRule": "id='456'",
"createRule": "id='123'",
"updateRule": "id='123'",
"deleteRule": "id='123'",
"schema": [
{"id":"abc123","name":"verified","type":"bool"}
],
"options": {
"query": "select 1 as id"
}
}`,
[]string{
"listRule", // missing field (ignoring the old or explicit schema)
"createRule", // not allowed
"updateRule", // not allowed
"deleteRule", // not allowed
},
},
{
"view update failure (check query identifiers format)",
"upsert_view",
`{
"listRule": null,
"viewRule": null,
"options": {
"query": "select 1 as id, 2 as [invalid!@#]"
}
}`,
[]string{
"schema", // should fail due to invalid field name
},
},
{
"view update success",
"upsert_view",
`{
"listRule": null,
"viewRule": null,
"options": {
"query": "select 1 as id, 2 as valid"
}
}`,
[]string{},
},
}
for _, s := range scenarios {
@@ -454,10 +578,19 @@ func TestCollectionUpsertValidateAndSubmit(t *testing.T) {
t.Errorf("[%s] Expected DeleteRule %v, got %v", s.testName, collection.DeleteRule, form.DeleteRule)
}
formSchema, _ := form.Schema.MarshalJSON()
collectionSchema, _ := collection.Schema.MarshalJSON()
if string(formSchema) != string(collectionSchema) {
t.Errorf("[%s] Expected Schema %v, got %v", s.testName, string(collectionSchema), string(formSchema))
rawFormSchema, _ := form.Schema.MarshalJSON()
rawCollectionSchema, _ := collection.Schema.MarshalJSON()
if len(form.Schema.Fields()) != len(collection.Schema.Fields()) {
t.Errorf("[%s] Expected Schema \n%v, \ngot \n%v", s.testName, string(rawCollectionSchema), string(rawFormSchema))
continue
}
for _, f := range form.Schema.Fields() {
if collection.Schema.GetFieldByName(f.Name) == nil {
t.Errorf("[%s] Missing field %s \nin \n%v", s.testName, f.Name, string(rawFormSchema))
continue
}
}
}
}
+3 -3
View File
@@ -66,7 +66,7 @@ func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc[[]*models.
importErr := txDao.ImportCollections(
collections,
form.DeleteMissing,
form.beforeRecordsSync,
form.afterSync,
)
if importErr == nil {
return nil
@@ -89,10 +89,10 @@ func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc[[]*models.
}, interceptors...)
}
func (form *CollectionsImport) beforeRecordsSync(txDao *daos.Dao, mappedNew, mappedOld map[string]*models.Collection) error {
func (form *CollectionsImport) afterSync(txDao *daos.Dao, mappedNew, mappedOld map[string]*models.Collection) error {
// refresh the actual persisted collections list
refreshedCollections := []*models.Collection{}
if err := txDao.CollectionQuery().OrderBy("created ASC").All(&refreshedCollections); err != nil {
if err := txDao.CollectionQuery().OrderBy("updated ASC").All(&refreshedCollections); err != nil {
return err
}
+57 -9
View File
@@ -38,6 +38,8 @@ func TestCollectionsImportValidate(t *testing.T) {
}
func TestCollectionsImportSubmit(t *testing.T) {
totalCollections := 10
scenarios := []struct {
name string
jsonData string
@@ -52,7 +54,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
"collections": []
}`,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
expectEvents: nil,
},
{
@@ -82,7 +84,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
]
}`,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
expectEvents: map[string]int{
"OnModelBeforeCreate": 2,
},
@@ -101,7 +103,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
]
}`,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
expectEvents: map[string]int{
"OnModelBeforeCreate": 2,
},
@@ -137,7 +139,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
]
}`,
expectError: false,
expectCollectionsCount: 11,
expectCollectionsCount: totalCollections + 3,
expectEvents: map[string]int{
"OnModelBeforeCreate": 3,
"OnModelAfterCreate": 3,
@@ -160,7 +162,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
]
}`,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
expectEvents: map[string]int{
"OnModelBeforeCreate": 1,
},
@@ -202,7 +204,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
]
}`,
expectError: true,
expectCollectionsCount: 8,
expectCollectionsCount: totalCollections,
expectEvents: map[string]int{
"OnModelBeforeDelete": 5,
},
@@ -253,7 +255,7 @@ func TestCollectionsImportSubmit(t *testing.T) {
]
}`,
expectError: false,
expectCollectionsCount: 10,
expectCollectionsCount: totalCollections + 2,
expectEvents: map[string]int{
"OnModelBeforeUpdate": 1,
"OnModelAfterUpdate": 1,
@@ -341,8 +343,54 @@ func TestCollectionsImportSubmit(t *testing.T) {
"OnModelAfterUpdate": 2,
"OnModelBeforeCreate": 1,
"OnModelAfterCreate": 1,
"OnModelBeforeDelete": 6,
"OnModelAfterDelete": 6,
"OnModelBeforeDelete": totalCollections - 2,
"OnModelAfterDelete": totalCollections - 2,
},
},
{
name: "lazy view evaluation",
jsonData: `{
"collections": [
{
"name": "view_before",
"type": "view",
"options": {
"query": "select id, active from base_test"
}
},
{
"name": "base_test",
"schema": [
{
"id":"fz6iql2m",
"name":"active",
"type":"bool"
}
]
},
{
"name": "view_after_new",
"type": "view",
"options": {
"query": "select id, active from base_test"
}
},
{
"name": "view_after_old",
"type": "view",
"options": {
"query": "select id from demo1"
}
}
]
}`,
expectError: false,
expectCollectionsCount: totalCollections + 4,
expectEvents: map[string]int{
"OnModelBeforeUpdate": 3,
"OnModelAfterUpdate": 3,
"OnModelBeforeCreate": 4,
"OnModelAfterCreate": 4,
},
},
}
+22 -30
View File
@@ -235,6 +235,16 @@ func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error {
return nil
}
// FilesToUpload returns the parsed request files ready for upload.
func (form *RecordUpsert) FilesToUpload() map[string][]*filesystem.File {
return form.filesToUpload
}
// FilesToUpload returns the parsed request filenames ready to be deleted.
func (form *RecordUpsert) FilesToDelete() []string {
return form.filesToDelete
}
// AddFiles adds the provided file(s) to the specified file field.
//
// If the file field is a SINGLE-value file field (aka. "Max Select = 1"),
@@ -246,9 +256,9 @@ func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error {
//
// Example
//
// f1, _ := filesystem.NewFileFromPath("/path/to/file1.txt")
// f2, _ := filesystem.NewFileFromPath("/path/to/file2.txt")
// form.AddFiles("documents", f1, f2)
// f1, _ := filesystem.NewFileFromPath("/path/to/file1.txt")
// f2, _ := filesystem.NewFileFromPath("/path/to/file2.txt")
// form.AddFiles("documents", f1, f2)
func (form *RecordUpsert) AddFiles(key string, files ...*filesystem.File) error {
field := form.record.Collection().Schema.GetFieldByName(key)
if field == nil || field.Type != schema.FieldTypeFile {
@@ -298,11 +308,11 @@ func (form *RecordUpsert) AddFiles(key string, files ...*filesystem.File) error
//
// Example
//
// // mark only only 2 files for removal
// form.AddFiles("documents", "file1_aw4bdrvws6.txt", "file2_xwbs36bafv.txt")
// // mark only only 2 files for removal
// form.AddFiles("documents", "file1_aw4bdrvws6.txt", "file2_xwbs36bafv.txt")
//
// // mark all "documents" files for removal
// form.AddFiles("documents")
// // mark all "documents" files for removal
// form.AddFiles("documents")
func (form *RecordUpsert) RemoveFiles(key string, toDelete ...string) error {
field := form.record.Collection().Schema.GetFieldByName(key)
if field == nil || field.Type != schema.FieldTypeFile {
@@ -419,7 +429,6 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
if len(submittedNames) == 0 && len(oldNames) > 0 {
form.RemoveFiles(key)
} else if len(oldNames) > 0 {
toDelete := []string{}
for _, name := range oldNames {
@@ -731,21 +740,16 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc[*models.Record]
form.record.MarkAsNew()
}
// persist the record model
if saveErr := form.dao.SaveRecord(form.record); saveErr != nil {
return fmt.Errorf("failed to save the record: %w", saveErr)
}
// upload new files (if any)
if err := form.processFilesToUpload(); err != nil {
return fmt.Errorf("failed to process the uploaded files: %w", err)
}
// persist the record model
if saveErr := form.dao.SaveRecord(form.record); saveErr != nil {
// try to cleanup the successfully uploaded files
if _, err := form.deleteFilesByNamesList(form.getFilesToUploadNames()); err != nil && form.app.IsDebug() {
log.Println(err)
}
return fmt.Errorf("failed to save the record: %w", saveErr)
}
// delete old files (if any)
//
// for now fail silently to avoid reupload when `form.Submit()`
@@ -758,18 +762,6 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc[*models.Record]
}, interceptors...)
}
func (form *RecordUpsert) getFilesToUploadNames() []string {
names := []string{}
for fieldKey := range form.filesToUpload {
for _, file := range form.filesToUpload[fieldKey] {
names = append(names, file.Name)
}
}
return names
}
func (form *RecordUpsert) processFilesToUpload() error {
if len(form.filesToUpload) == 0 {
return nil // no parsed file fields
+28 -6
View File
@@ -979,11 +979,33 @@ func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
t.Fatal(err)
}
removed0 := "test_d61b33QdDU.txt" // replaced
removed1 := "300_WlbFWSGmW9.png"
removed2 := "logo_vcfJJG5TAh.svg"
form.AddFiles("file_one", f1) // should replace the existin file
form.AddFiles("file_many", f2, f3) // should append
form.RemoveFiles("file_many", "300_WlbFWSGmW9.png", "logo_vcfJJG5TAh.svg") // should remove
form.RemoveFiles("file_many", removed1, removed2) // should remove
filesToUpload := form.FilesToUpload()
if v, ok := filesToUpload["file_one"]; !ok || len(v) != 1 {
t.Fatalf("Expected filesToUpload[file_one] to have exactly 1 file, got %v", v)
}
if v, ok := filesToUpload["file_many"]; !ok || len(v) != 2 {
t.Fatalf("Expected filesToUpload[file_many] to have exactly 2 file, got %v", v)
}
filesToDelete := form.FilesToDelete()
if len(filesToDelete) != 3 {
t.Fatalf("Expected exactly 2 file to delete, got %v", filesToDelete)
}
for _, f := range []string{removed0, removed1, removed2} {
if !list.ExistInSlice(f, filesToDelete) {
t.Fatalf("Missing file %q from filesToDelete %v", f, filesToDelete)
}
}
if err := form.Submit(); err != nil {
t.Fatalf("Failed to submit the RecordUpsert form, got %v", err)
@@ -995,14 +1017,14 @@ func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
}
// ensure files deletion
if hasRecordFile(app, recordAfter, "test_d61b33QdDU.txt") {
if hasRecordFile(app, recordAfter, removed0) {
t.Fatalf("Expected the old file_one file to be deleted")
}
if hasRecordFile(app, recordAfter, "300_WlbFWSGmW9.png") {
t.Fatalf("Expected 300_WlbFWSGmW9.png to be deleted")
if hasRecordFile(app, recordAfter, removed1) {
t.Fatalf("Expected %s to be deleted", removed1)
}
if hasRecordFile(app, recordAfter, "logo_vcfJJG5TAh.svg") {
t.Fatalf("Expected logo_vcfJJG5TAh.svg to be deleted")
if hasRecordFile(app, recordAfter, removed2) {
t.Fatalf("Expected %s to be deleted", removed2)
}
fileOne := recordAfter.GetStringSlice("file_one")
+3 -1
View File
@@ -13,6 +13,7 @@ import (
// size is no more than the provided maxBytes.
//
// Example:
//
// validation.Field(&form.File, validation.By(validators.UploadedFileSize(1000)))
func UploadedFileSize(maxBytes int) validation.RuleFunc {
return func(value any) error {
@@ -36,7 +37,8 @@ func UploadedFileSize(maxBytes int) validation.RuleFunc {
// mimetype is within the provided allowed mime types.
//
// Example:
// validMimeTypes := []string{"test/plain","image/jpeg"}
//
// validMimeTypes := []string{"test/plain","image/jpeg"}
// validation.Field(&form.File, validation.By(validators.UploadedFileMimeType(validMimeTypes)))
func UploadedFileMimeType(validTypes []string) validation.RuleFunc {
return func(value any) error {
+1
View File
@@ -12,6 +12,7 @@ import (
// Compare checks whether the provided model id exists.
//
// Example:
//
// validation.Field(&form.Id, validation.By(validators.UniqueId(form.dao, tableName)))
func UniqueId(dao *daos.Dao, tableName string) validation.RuleFunc {
return func(value any) error {
+4
View File
@@ -362,6 +362,10 @@ func (validator *RecordDataValidator) checkRelationValue(field *schema.SchemaFie
options, _ := field.Options.(*schema.RelationOptions)
if options.MinSelect != nil && len(ids) < *options.MinSelect {
return validation.NewError("validation_not_enough_values", fmt.Sprintf("Select at least %d", *options.MinSelect))
}
if options.MaxSelect != nil && len(ids) > *options.MaxSelect {
return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", *options.MaxSelect))
}
+10
View File
@@ -1212,6 +1212,7 @@ func TestRecordDataValidatorValidateRelation(t *testing.T) {
Unique: true,
Type: schema.FieldTypeRelation,
Options: &schema.RelationOptions{
MinSelect: types.Pointer(2),
CollectionId: demo.Id,
},
},
@@ -1258,6 +1259,15 @@ func TestRecordDataValidatorValidateRelation(t *testing.T) {
nil,
[]string{"field2"},
},
{
"check min constraint",
map[string]any{
"field2": relId2,
"field3": []string{relId1},
},
nil,
[]string{"field3"},
},
{
"check unique constraint",
map[string]any{
+1
View File
@@ -7,6 +7,7 @@ import (
// Compare checks whether the validated value matches another string.
//
// Example:
//
// validation.Field(&form.PasswordConfirm, validation.By(validators.Compare(form.Password)))
func Compare(valueToCompare string) validation.RuleFunc {
return func(value any) error {
+35 -36
View File
@@ -4,49 +4,48 @@ go 1.18
require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/aws/aws-sdk-go v1.44.187
github.com/aws/aws-sdk-go v1.44.209
github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.3.4
github.com/domodwyer/mailyak/v3 v3.4.0
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86
github.com/dop251/goja_nodejs v0.0.0-20221009164102-3aa5028e57f6
github.com/fatih/color v1.14.1
github.com/gabriel-vasile/mimetype v1.4.1
github.com/ganigeorgiev/fexpr v0.3.0
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-jwt/jwt/v4 v4.4.3
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
github.com/mattn/go-sqlite3 v1.14.16
github.com/pocketbase/dbx v1.9.0
github.com/pocketbase/dbx v1.10.0
github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.6.1
gocloud.dev v0.28.0
golang.org/x/crypto v0.5.0
golang.org/x/net v0.5.0
golang.org/x/oauth2 v0.4.0
golang.org/x/sync v0.1.0
modernc.org/sqlite v1.20.3
golang.org/x/crypto v0.6.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.5.0
modernc.org/sqlite v1.21.0
)
require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.17.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.17.5 // 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.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.10 // 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.49 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // 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/s3shared v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.18.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -62,28 +61,28 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/image v0.3.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/term v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
golang.org/x/image v0.5.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.5.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.108.0 // indirect
google.golang.org/api v0.110.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa // indirect
google.golang.org/grpc v1.52.3 // indirect
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
+74 -73
View File
@@ -116,7 +116,7 @@ cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOt
cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
@@ -202,7 +202,7 @@ cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp
cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk=
cloud.google.com/go/iam v0.11.0 h1:kwCWfKwB6ePZoZnGLwrd3B6Ru/agoHANTUBWpVNIdnM=
cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=
cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM=
@@ -517,68 +517,68 @@ 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.44.128/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.44.151/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.187 h1:D5CsRomPnlwDHJCanL2mtaLIcbhjiWxNh5j8zvaWdJA=
github.com/aws/aws-sdk-go v1.44.187/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps=
github.com/aws/aws-sdk-go v1.44.209/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 v1.17.1/go.mod h1:JLnGeGONAyi2lWXI1p0PCIOIy333JMVK1U7Hf0aRFLw=
github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY=
github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4=
github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
github.com/aws/aws-sdk-go-v2/config v1.18.10 h1:Znce11DWswdh+5kOsIp+QaNfY9igp1QUN+fZHCKmeCI=
github.com/aws/aws-sdk-go-v2/config v1.18.10/go.mod h1:VATKco+pl+Qe1WW+RzvZTlPPe/09Gg9+vM0ZXsqb16k=
github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY=
github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0=
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.10 h1:T4Y39IhelTLg1f3xiKJssThnFxsndS8B6OnmcXtKK+8=
github.com/aws/aws-sdk-go-v2/credentials v1.13.10/go.mod h1:tqAm4JmQaShel+Qi38hmd1QglSnnxaYt50k/9yGQzzc=
github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8=
github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
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.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.49 h1:zPFhadkmXbXu3RVXTPU4HVW+g2DStMY+01cJaj//+Cw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.49/go.mod h1:N9gSChQkKpdAj7vRpfKma4ND88zoZM+v6W2lJgWrDh4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55 h1:ClZKHmu2QIRQCEQ2Y2upfu4JPO0pG69Ce5eiq3PS2V4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.55/go.mod h1:L/h5B6I7reig2QJXCGY0e0NVx4hYCcjETmsfR02hFng=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
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.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.19/go.mod h1:6Q0546uHDp421okhmmGfbxzq2hBqbXFNpi4k+Q1JnQA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.26/go.mod h1:Y2OJ+P+MC1u1VKnavT+PshiEuGPyh/7DqxoDNij4/bg=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.16/go.mod h1:XH+3h395e3WVdd6T2Z3mPxuI+x/HVtdqVOREkTiyubs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 h1:H/mF2LNWwX00lD6FlYfKpLLZgUW7oIzCBkig78x4Xok=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18/go.mod h1:T2Ku+STrYQ1zIkL1wMvj8P3wWQaaCMKNdz70MT2FLfE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21 h1:QdxdY43AiwsqG/VAqHA7bIVSm3rKr8/p9i05ydA0/RM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.21/go.mod h1:QtIEat7ksHH8nFItljyvMI0dGj8lipK2XZ4PhNihTEU=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.10/go.mod h1:9cBNUHI2aW4ho0A5T87O294iPDuuUOSIEDjnd1Lq/z0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.20/go.mod h1:Mp4XI/CkWGD79AQxZ5lIFlgvC0A+gl+4BmyG1F+SfNc=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 h1:kv5vRAl00tozRxSnI0IszPWGXsJOyA7hmEUHFYqsyvw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22/go.mod h1:Od+GU5+Yx41gryN/ZGZzAJMZ9R1yn6lgA0fD5Lo5SkQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24 h1:Qmm8klpAdkuN3/rPrIMa/hZQ1z93WMBPjOzdAsbSnlo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.24/go.mod h1:QelGeWBVRh9PbbXsfXKTFlU9FjT6W2yP+dW5jMQzOkg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.19/go.mod h1:02CP6iuYP+IVnBX5HULVdSAku/85eHB2Y9EsFhrkEwU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 h1:vY5siRXvW5TrOKm2qKEf9tliBfdLxdfy0i02LOcmqUo=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21/go.mod h1:WZvNXT1XuH8dnJM0HvOlvk+RNn7NbAPvA/ACO0QarSc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23 h1:qc+RW0WWZ2KApMnsu/EVCPqLTyIH55uc7YQq7mq4XqE=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.23/go.mod h1:FJhZWVWBCcgAF8jbep7pxQ1QUsjzTwa9tvEXGw2TDRo=
github.com/aws/aws-sdk-go-v2/service/kms v1.19.0/go.mod h1:kZodDPTQjSH/qM6/OvyTfM5mms5JHB/EKYp5dhn/vI4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.1 h1:kIgvVY7PHx4gIb0na/Q9gTWJWauTwhKdaqJjX8PkIY8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.1/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5 h1:kFfb+NMap4R7nDvBYyABa/nw7KFMtAfygD1Hyoxh4uE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.30.5/go.mod h1:Dze3kNt4T+Dgb8YCfuIFSBLmE6hadKNxqfdF0Xmqz1I=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
github.com/aws/aws-sdk-go-v2/service/sns v1.18.6/go.mod h1:2cPUjR63iE9MPMPJtSyzYmsTFCNrN/Xi9j0v9BL5OU0=
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.15/go.mod h1:DKX/7/ZiAzHO6p6AhArnGdrV4r+d461weby8KeVtvC4=
github.com/aws/aws-sdk-go-v2/service/ssm v1.33.1/go.mod h1:rEsqsZrOp9YvSGPOrcL3pR9+i/QJaWRkAYbuxMa7yCU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vboR0FeJ89OkEy1M9mWbK2ifCI=
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/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
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/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc=
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.2 h1:J/4wIaGInCEYCGhTSruxCxeoA5cy91a+JT7cHFKFSHQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.2/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s=
github.com/aws/smithy-go v1.13.4/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/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
@@ -817,8 +817,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/domodwyer/mailyak/v3 v3.3.4 h1:AG/pvcz2/ocFqZkPEG7lPAa0MhCq1warfUEKJt6Fagk=
github.com/domodwyer/mailyak/v3 v3.3.4/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/domodwyer/mailyak/v3 v3.4.0 h1:U9U9ctj+F8AmW5qT6ZJMwA/6INqsmvR7nOhPUSkbVUQ=
github.com/domodwyer/mailyak/v3 v3.4.0/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86 h1:E2wycakfddWJ26v+ZyEY91Lb/HEZyaiZhbMX+KQcdmc=
@@ -1014,8 +1014,9 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -1129,7 +1130,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.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
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.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -1572,8 +1573,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.9.0 h1:HYY0hQcbopfJFFqJqcjt39lg6VTKlIRoDbUFY6ZC4iM=
github.com/pocketbase/dbx v1.9.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/dbx v1.10.0 h1:58VIT7r6T+BnVbYVosvGBsPjQEic3/VFRYGT823vWSQ=
github.com/pocketbase/dbx v1.10.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
@@ -1640,8 +1641,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rakyll/embedmd v0.0.0-20171029212350-c8060a0752a2/go.mod h1:7jOTMgqac46PZcF54q6l2hkLEG8op93fZu61KmxWDV4=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -1931,8 +1932,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
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/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1947,8 +1948,8 @@ golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZ
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1977,8 +1978,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -2062,8 +2063,8 @@ golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfS
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
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/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2093,8 +2094,8 @@ golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
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/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2263,8 +2264,8 @@ golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.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/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -2274,8 +2275,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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
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/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2288,8 +2289,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.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/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2389,8 +2390,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
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/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2457,8 +2458,8 @@ google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91
google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
google.golang.org/api v0.108.0 h1:WVBc/faN0DkKtR43Q/7+tPny9ZoLZdIiAyG5Q9vFClg=
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2592,8 +2593,8 @@ google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZV
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa h1:qQPhfbPO23fwm/9lQr91L1u62Zo6cm+zI+slZT+uf+o=
google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -2639,8 +2640,8 @@ google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ=
google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -2778,19 +2779,19 @@ modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
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.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
+1 -1
View File
@@ -56,7 +56,7 @@ func SendAdminPasswordReset(app core.App, admin *models.Admin) error {
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
To: mail.Address{Address: admin.Email},
To: []mail.Address{{Address: admin.Email}},
Subject: "Reset admin password",
HTML: body,
}
+3 -3
View File
@@ -32,7 +32,7 @@ func SendRecordPasswordReset(app core.App, authRecord *models.Record) error {
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
To: mail.Address{Address: authRecord.Email()},
To: []mail.Address{{Address: authRecord.Email()}},
Subject: subject,
HTML: body,
}
@@ -76,7 +76,7 @@ func SendRecordVerification(app core.App, authRecord *models.Record) error {
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
To: mail.Address{Address: authRecord.Email()},
To: []mail.Address{{Address: authRecord.Email()}},
Subject: subject,
HTML: body,
}
@@ -120,7 +120,7 @@ func SendRecordChangeEmail(app core.App, record *models.Record, newEmail string)
Name: app.Settings().Meta.SenderName,
Address: app.Settings().Meta.SenderAddress,
},
To: mail.Address{Address: newEmail},
To: []mail.Address{{Address: newEmail}},
Subject: subject,
HTML: body,
}
@@ -0,0 +1,26 @@
package migrations
import (
"github.com/pocketbase/dbx"
)
// This migration replaces the "authentikAuth" setting with "oidc".
func init() {
AppMigrations.Register(func(db dbx.Builder) error {
_, err := db.NewQuery(`
UPDATE {{_params}}
SET [[value]] = replace([[value]], '"authentikAuth":', '"oidcAuth":')
WHERE [[key]] = 'settings'
`).Execute()
return err
}, func(db dbx.Builder) error {
_, err := db.NewQuery(`
UPDATE {{_params}}
SET [[value]] = replace([[value]], '"oidcAuth":', '"authentikAuth":')
WHERE [[key]] = 'settings'
`).Execute()
return err
})
}
+34 -2
View File
@@ -17,6 +17,7 @@ var (
const (
CollectionTypeBase = "base"
CollectionTypeAuth = "auth"
CollectionTypeView = "view"
)
type Collection struct {
@@ -52,11 +53,16 @@ func (m *Collection) IsBase() bool {
return m.Type == CollectionTypeBase
}
// IsBase checks if the current collection has "auth" type.
// IsAuth checks if the current collection has "auth" type.
func (m *Collection) IsAuth() bool {
return m.Type == CollectionTypeAuth
}
// IsView checks if the current collection has "view" type.
func (m *Collection) IsView() bool {
return m.Type == CollectionTypeView
}
// MarshalJSON implements the [json.Marshaler] interface.
func (m Collection) MarshalJSON() ([]byte, error) {
type alias Collection // prevent recursion
@@ -82,6 +88,14 @@ func (m *Collection) AuthOptions() CollectionAuthOptions {
return result
}
// ViewOptions decodes the current collection options and returns them
// as new [CollectionViewOptions] instance.
func (m *Collection) ViewOptions() CollectionViewOptions {
result := CollectionViewOptions{}
m.DecodeOptions(&result)
return result
}
// NormalizeOptions updates the current collection options with a
// new normalized state based on the collection type.
func (m *Collection) NormalizeOptions() error {
@@ -89,6 +103,8 @@ func (m *Collection) NormalizeOptions() error {
switch m.Type {
case CollectionTypeAuth:
typedOptions = m.AuthOptions()
case CollectionTypeView:
typedOptions = m.ViewOptions()
default:
typedOptions = m.BaseOptions()
}
@@ -143,7 +159,7 @@ func (m *Collection) SetOptions(typedOptions any) error {
// -------------------------------------------------------------------
// CollectionAuthOptions defines the "base" Collection.Options fields.
// CollectionBaseOptions defines the "base" Collection.Options fields.
type CollectionBaseOptions struct {
}
@@ -152,6 +168,8 @@ func (o CollectionBaseOptions) Validate() error {
return nil
}
// -------------------------------------------------------------------
// CollectionAuthOptions defines the "auth" Collection.Options fields.
type CollectionAuthOptions struct {
ManageRule *string `form:"manageRule" json:"manageRule"`
@@ -184,3 +202,17 @@ func (o CollectionAuthOptions) Validate() error {
),
)
}
// -------------------------------------------------------------------
// CollectionViewOptions defines the "view" Collection.Options fields.
type CollectionViewOptions struct {
Query string `form:"query" json:"query"`
}
// Validate implements [validation.Validatable] interface.
func (o CollectionViewOptions) Validate() error {
return validation.ValidateStruct(&o,
validation.Field(&o.Query, validation.Required),
)
}
+99 -11
View File
@@ -97,11 +97,11 @@ func TestCollectionMarshalJSON(t *testing.T) {
for _, s := range scenarios {
result, err := s.collection.MarshalJSON()
if err != nil {
t.Errorf("(%s) Unexpected error %v", s.name, err)
t.Errorf("[%s] Unexpected error %v", s.name, err)
continue
}
if string(result) != s.expected {
t.Errorf("(%s) Expected\n%v \ngot \n%v", s.name, s.expected, string(result))
t.Errorf("[%s] Expected\n%v \ngot \n%v", s.name, s.expected, string(result))
}
}
}
@@ -143,7 +143,7 @@ func TestCollectionBaseOptions(t *testing.T) {
}
if strEncoded := string(encoded); strEncoded != s.expected {
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
}
}
}
@@ -188,7 +188,52 @@ func TestCollectionAuthOptions(t *testing.T) {
}
if strEncoded := string(encoded); strEncoded != s.expected {
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
}
}
}
func TestCollectionViewOptions(t *testing.T) {
options := types.JsonMap{"query": "select id from demo1", "minPasswordLength": 4}
expectedSerialization := `{"query":"select id from demo1"}`
scenarios := []struct {
name string
collection models.Collection
expected string
}{
{
"no type",
models.Collection{Options: options},
expectedSerialization,
},
{
"unknown type",
models.Collection{Type: "anything", Options: options},
expectedSerialization,
},
{
"different type",
models.Collection{Type: models.CollectionTypeBase, Options: options},
expectedSerialization,
},
{
"view type",
models.Collection{Type: models.CollectionTypeView, Options: options},
expectedSerialization,
},
}
for _, s := range scenarios {
result := s.collection.ViewOptions()
encoded, err := json.Marshal(result)
if err != nil {
t.Fatal(err)
}
if strEncoded := string(encoded); strEncoded != s.expected {
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
}
}
}
@@ -218,7 +263,7 @@ func TestNormalizeOptions(t *testing.T) {
for _, s := range scenarios {
if err := s.collection.NormalizeOptions(); err != nil {
t.Errorf("(%s) Unexpected error %v", s.name, err)
t.Errorf("[%s] Unexpected error %v", s.name, err)
continue
}
@@ -228,7 +273,7 @@ func TestNormalizeOptions(t *testing.T) {
}
if strEncoded := string(encoded); strEncoded != s.expected {
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
}
}
}
@@ -286,7 +331,7 @@ func TestSetOptions(t *testing.T) {
for _, s := range scenarios {
if err := s.collection.SetOptions(s.options); err != nil {
t.Errorf("(%s) Unexpected error %v", s.name, err)
t.Errorf("[%s] Unexpected error %v", s.name, err)
continue
}
@@ -296,7 +341,7 @@ func TestSetOptions(t *testing.T) {
}
if strEncoded := string(encoded); strEncoded != s.expected {
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
t.Errorf("[%s] Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
}
}
}
@@ -378,18 +423,61 @@ func TestCollectionAuthOptionsValidate(t *testing.T) {
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Errorf("(%s) Failed to parse errors %v", s.name, result)
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
continue
}
if len(errs) != len(s.expectedErrors) {
t.Errorf("(%s) Expected error keys %v, got errors \n%v", s.name, s.expectedErrors, result)
t.Errorf("[%s] Expected error keys %v, got errors \n%v", s.name, s.expectedErrors, result)
continue
}
for key := range errs {
if !list.ExistInSlice(key, s.expectedErrors) {
t.Errorf("(%s) Unexpected error key %q in \n%v", s.name, key, errs)
t.Errorf("[%s] Unexpected error key %q in \n%v", s.name, key, errs)
}
}
}
}
func TestCollectionViewOptionsValidate(t *testing.T) {
scenarios := []struct {
name string
options models.CollectionViewOptions
expectedErrors []string
}{
{
"empty",
models.CollectionViewOptions{},
[]string{"query"},
},
{
"valid data",
models.CollectionViewOptions{
Query: "test123",
},
[]string{},
},
}
for _, s := range scenarios {
result := s.options.Validate()
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
continue
}
if len(errs) != len(s.expectedErrors) {
t.Errorf("[%s] Expected error keys %v, got errors \n%v", s.name, s.expectedErrors, result)
continue
}
for key := range errs {
if !list.ExistInSlice(key, s.expectedErrors) {
t.Errorf("[%s] Unexpected error key %q in \n%v", s.name, key, errs)
}
}
}
+13 -3
View File
@@ -59,6 +59,9 @@ func nullStringMapValue(data dbx.NullStringMap, key string) any {
// NewRecordFromNullStringMap initializes a single new Record model
// with data loaded from the provided NullStringMap.
//
// Note that this method is intended to load and Scan data from a database
// result and calls PostScan() which marks the record as "not new".
func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record {
resultMap := make(map[string]any, len(data))
@@ -89,6 +92,9 @@ func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap)
// NewRecordsFromNullStringMaps initializes a new Record model for
// each row in the provided NullStringMap slice.
//
// Note that this method is intended to load and Scan data from a database
// result and calls PostScan() for each record marking them as "not new".
func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record {
result := make([]*Record, len(rows))
@@ -469,8 +475,12 @@ func (m *Record) PublicExport() map[string]any {
// export base model fields
result[schema.FieldNameId] = m.GetId()
result[schema.FieldNameCreated] = m.GetCreated()
result[schema.FieldNameUpdated] = m.GetUpdated()
if created := m.GetCreated(); !m.Collection().IsView() || !created.IsZero() {
result[schema.FieldNameCreated] = created
}
if updated := m.GetUpdated(); !m.Collection().IsView() || !updated.IsZero() {
result[schema.FieldNameUpdated] = updated
}
// add helper collection reference fields
result[schema.FieldNameCollectionId] = m.collection.Id
@@ -556,7 +566,7 @@ func (m *Record) ReplaceModifers(data map[string]any) map[string]any {
}
// -----------------------------------------------------------
// legacy file field modifiers (kept for backward compatability)
// legacy file field modifiers (kept for backward compatibility)
// -----------------------------------------------------------
var oldNames []string
+15 -4
View File
@@ -139,7 +139,7 @@ type SchemaField struct {
func (f *SchemaField) ColDefinition() string {
switch f.Type {
case FieldTypeNumber:
return "REAL DEFAULT 0"
return "NUMERIC DEFAULT 0"
case FieldTypeBool:
return "BOOLEAN DEFAULT FALSE"
case FieldTypeJson:
@@ -386,8 +386,7 @@ func (f *SchemaField) PrepareValueWithModifier(baseValue any, modifier string, m
}
case FieldTypeFile:
// note: file for now supports only the subtract modifier
switch modifier {
case FieldValueModifierSubtract:
if modifier == FieldValueModifierSubtract {
resolvedValue = list.SubtractSlice(
list.ToUniqueStringSlice(baseValue),
list.ToUniqueStringSlice(modifierValue),
@@ -604,6 +603,12 @@ type RelationOptions struct {
// in case of delete of all linked relations.
CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"`
// MinSelect indicates the min number of allowed relation records
// that could be linked to the main model.
//
// If nil no limits are applied.
MinSelect *int `form:"minSelect" json:"minSelect"`
// MaxSelect indicates the max number of allowed relation records
// that could be linked to the main model.
//
@@ -615,9 +620,15 @@ type RelationOptions struct {
}
func (o RelationOptions) Validate() error {
minVal := 0
if o.MinSelect != nil {
minVal = *o.MinSelect
}
return validation.ValidateStruct(&o,
validation.Field(&o.CollectionId, validation.Required),
validation.Field(&o.MaxSelect, validation.NilOrNotEmpty, validation.Min(1)),
validation.Field(&o.MinSelect, validation.NilOrNotEmpty, validation.Min(1)),
validation.Field(&o.MaxSelect, validation.NilOrNotEmpty, validation.Min(minVal)),
)
}
+2 -2
View File
@@ -67,7 +67,7 @@ func TestSchemaFieldColDefinition(t *testing.T) {
},
{
schema.SchemaField{Type: schema.FieldTypeNumber, Name: "test"},
"REAL DEFAULT 0",
"NUMERIC DEFAULT 0",
},
{
schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"},
@@ -509,7 +509,7 @@ func TestSchemaFieldInitOptions(t *testing.T) {
{
schema.SchemaField{Type: schema.FieldTypeRelation},
false,
`{"system":false,"id":"","name":"","type":"relation","required":false,"unique":false,"options":{"collectionId":"","cascadeDelete":false,"maxSelect":null,"displayFields":null}}`,
`{"system":false,"id":"","name":"","type":"relation","required":false,"unique":false,"options":{"collectionId":"","cascadeDelete":false,"minSelect":null,"maxSelect":null,"displayFields":null}}`,
},
{
schema.SchemaField{Type: schema.FieldTypeUser},
+44 -28
View File
@@ -15,6 +15,10 @@ import (
"github.com/pocketbase/pocketbase/tools/security"
)
// SecretMask is the default settings secrets replacement value
// (see Settings.RedactClone()).
const SecretMask string = "******"
// Settings defines common app configuration options.
type Settings struct {
mux sync.RWMutex
@@ -47,8 +51,10 @@ type Settings struct {
StravaAuth AuthProviderConfig `form:"stravaAuth" json:"stravaAuth"`
GiteeAuth AuthProviderConfig `form:"giteeAuth" json:"giteeAuth"`
LivechatAuth AuthProviderConfig `form:"livechatAuth" json:"livechatAuth"`
AuthentikAuth AuthProviderConfig `form:"authentikAuth" json:"authentikAuth"`
GiteaAuth AuthProviderConfig `form:"giteaAuth" json:"giteaAuth"`
OIDCAuth AuthProviderConfig `form:"oidcAuth" json:"oidcAuth"`
OIDC2Auth AuthProviderConfig `form:"oidc2Auth" json:"oidc2Auth"`
OIDC3Auth AuthProviderConfig `form:"oidc3Auth" json:"oidc3Auth"`
}
// New creates and returns a new default Settings instance.
@@ -138,10 +144,16 @@ func New() *Settings {
LivechatAuth: AuthProviderConfig{
Enabled: false,
},
AuthentikAuth: AuthProviderConfig{
GiteaAuth: AuthProviderConfig{
Enabled: false,
},
GiteaAuth: AuthProviderConfig{
OIDCAuth: AuthProviderConfig{
Enabled: false,
},
OIDC2Auth: AuthProviderConfig{
Enabled: false,
},
OIDC3Auth: AuthProviderConfig{
Enabled: false,
},
}
@@ -176,8 +188,10 @@ func (s *Settings) Validate() error {
validation.Field(&s.StravaAuth),
validation.Field(&s.GiteeAuth),
validation.Field(&s.LivechatAuth),
validation.Field(&s.AuthentikAuth),
validation.Field(&s.GiteaAuth),
validation.Field(&s.OIDCAuth),
validation.Field(&s.OIDC2Auth),
validation.Field(&s.OIDC3Auth),
)
}
@@ -211,8 +225,6 @@ func (s *Settings) RedactClone() (*Settings, error) {
return nil, err
}
mask := "******"
sensitiveFields := []*string{
&clone.Smtp.Password,
&clone.S3.Secret,
@@ -235,14 +247,16 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.StravaAuth.ClientSecret,
&clone.GiteeAuth.ClientSecret,
&clone.LivechatAuth.ClientSecret,
&clone.AuthentikAuth.ClientSecret,
&clone.GiteaAuth.ClientSecret,
&clone.OIDCAuth.ClientSecret,
&clone.OIDC2Auth.ClientSecret,
&clone.OIDC3Auth.ClientSecret,
}
// mask all sensitive fields
for _, v := range sensitiveFields {
if v != nil && *v != "" {
*v = mask
*v = SecretMask
}
}
@@ -256,21 +270,23 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
defer s.mux.RUnlock()
return map[string]AuthProviderConfig{
auth.NameGoogle: s.GoogleAuth,
auth.NameFacebook: s.FacebookAuth,
auth.NameGithub: s.GithubAuth,
auth.NameGitlab: s.GitlabAuth,
auth.NameDiscord: s.DiscordAuth,
auth.NameTwitter: s.TwitterAuth,
auth.NameMicrosoft: s.MicrosoftAuth,
auth.NameSpotify: s.SpotifyAuth,
auth.NameKakao: s.KakaoAuth,
auth.NameTwitch: s.TwitchAuth,
auth.NameStrava: s.StravaAuth,
auth.NameGitee: s.GiteeAuth,
auth.NameLivechat: s.LivechatAuth,
auth.NameAuthentik: s.AuthentikAuth,
auth.NameGitea: s.GiteaAuth,
auth.NameGoogle: s.GoogleAuth,
auth.NameFacebook: s.FacebookAuth,
auth.NameGithub: s.GithubAuth,
auth.NameGitlab: s.GitlabAuth,
auth.NameDiscord: s.DiscordAuth,
auth.NameTwitter: s.TwitterAuth,
auth.NameMicrosoft: s.MicrosoftAuth,
auth.NameSpotify: s.SpotifyAuth,
auth.NameKakao: s.KakaoAuth,
auth.NameTwitch: s.TwitchAuth,
auth.NameStrava: s.StravaAuth,
auth.NameGitee: s.GiteeAuth,
auth.NameLivechat: s.LivechatAuth,
auth.NameGitea: s.GiteaAuth,
auth.NameOIDC: s.OIDCAuth,
auth.NameOIDC + "2": s.OIDC2Auth,
auth.NameOIDC + "3": s.OIDC3Auth,
}
}
@@ -481,11 +497,11 @@ func (c LogsConfig) Validate() error {
type AuthProviderConfig struct {
Enabled bool `form:"enabled" json:"enabled"`
ClientId string `form:"clientId" json:"clientId,omitempty"`
ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"`
AuthUrl string `form:"authUrl" json:"authUrl,omitempty"`
TokenUrl string `form:"tokenUrl" json:"tokenUrl,omitempty"`
UserApiUrl string `form:"userApiUrl" json:"userApiUrl,omitempty"`
ClientId string `form:"clientId" json:"clientId"`
ClientSecret string `form:"clientSecret" json:"clientSecret"`
AuthUrl string `form:"authUrl" json:"authUrl"`
TokenUrl string `form:"tokenUrl" json:"tokenUrl"`
UserApiUrl string `form:"userApiUrl" json:"userApiUrl"`
}
// Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface.
+88 -51
View File
@@ -1,6 +1,7 @@
package settings_test
import (
"bytes"
"encoding/json"
"fmt"
"strings"
@@ -54,10 +55,14 @@ func TestSettingsValidate(t *testing.T) {
s.GiteeAuth.ClientId = ""
s.LivechatAuth.Enabled = true
s.LivechatAuth.ClientId = ""
s.AuthentikAuth.Enabled = true
s.AuthentikAuth.ClientId = ""
s.GiteaAuth.Enabled = true
s.GiteaAuth.ClientId = ""
s.OIDCAuth.Enabled = true
s.OIDCAuth.ClientId = ""
s.OIDC2Auth.Enabled = true
s.OIDC2Auth.ClientId = ""
s.OIDC3Auth.Enabled = true
s.OIDC3Auth.ClientId = ""
// check if Validate() is triggering the members validate methods.
err := s.Validate()
@@ -89,8 +94,10 @@ func TestSettingsValidate(t *testing.T) {
`"stravaAuth":{`,
`"giteeAuth":{`,
`"livechatAuth":{`,
`"authentikAuth":{`,
`"giteaAuth":{`,
`"oidcAuth":{`,
`"oidc2Auth":{`,
`"oidc3Auth":{`,
}
errBytes, _ := json.Marshal(err)
@@ -145,10 +152,14 @@ func TestSettingsMerge(t *testing.T) {
s2.GiteeAuth.ClientId = "gitee_test"
s2.LivechatAuth.Enabled = true
s2.LivechatAuth.ClientId = "livechat_test"
s2.AuthentikAuth.Enabled = true
s2.AuthentikAuth.ClientId = "authentik_test"
s2.GiteaAuth.Enabled = true
s2.GiteaAuth.ClientId = "gitea_test"
s2.OIDCAuth.Enabled = true
s2.OIDCAuth.ClientId = "oidc_test"
s2.OIDC2Auth.Enabled = true
s2.OIDC2Auth.ClientId = "oidc2_test"
s2.OIDC3Auth.Enabled = true
s2.OIDC3Auth.ClientId = "oidc3_test"
if err := s1.Merge(s2); err != nil {
t.Fatal(err)
@@ -199,47 +210,69 @@ func TestSettingsClone(t *testing.T) {
}
func TestSettingsRedactClone(t *testing.T) {
testSecret := "test_secret"
s1 := settings.New()
s1.Meta.AppName = "test123" // control field
s1.Smtp.Password = "test123"
s1.Smtp.Tls = true
s1.S3.Secret = "test123"
s1.AdminAuthToken.Secret = "test123"
s1.AdminPasswordResetToken.Secret = "test123"
s1.RecordAuthToken.Secret = "test123"
s1.RecordPasswordResetToken.Secret = "test123"
s1.RecordEmailChangeToken.Secret = "test123"
s1.RecordVerificationToken.Secret = "test123"
s1.GoogleAuth.ClientSecret = "test123"
s1.FacebookAuth.ClientSecret = "test123"
s1.GithubAuth.ClientSecret = "test123"
s1.GitlabAuth.ClientSecret = "test123"
s1.DiscordAuth.ClientSecret = "test123"
s1.TwitterAuth.ClientSecret = "test123"
s1.MicrosoftAuth.ClientSecret = "test123"
s1.SpotifyAuth.ClientSecret = "test123"
s1.KakaoAuth.ClientSecret = "test123"
s1.TwitchAuth.ClientSecret = "test123"
s1.StravaAuth.ClientSecret = "test123"
s1.GiteeAuth.ClientSecret = "test123"
s1.LivechatAuth.ClientSecret = "test123"
s1.AuthentikAuth.ClientSecret = "test123"
s1.GiteaAuth.ClientSecret = "test123"
// control fields
s1.Meta.AppName = "test123"
// secrets
s1.Smtp.Password = testSecret
s1.S3.Secret = testSecret
s1.AdminAuthToken.Secret = testSecret
s1.AdminPasswordResetToken.Secret = testSecret
s1.RecordAuthToken.Secret = testSecret
s1.RecordPasswordResetToken.Secret = testSecret
s1.RecordEmailChangeToken.Secret = testSecret
s1.RecordVerificationToken.Secret = testSecret
s1.GoogleAuth.ClientSecret = testSecret
s1.FacebookAuth.ClientSecret = testSecret
s1.GithubAuth.ClientSecret = testSecret
s1.GitlabAuth.ClientSecret = testSecret
s1.DiscordAuth.ClientSecret = testSecret
s1.TwitterAuth.ClientSecret = testSecret
s1.MicrosoftAuth.ClientSecret = testSecret
s1.SpotifyAuth.ClientSecret = testSecret
s1.KakaoAuth.ClientSecret = testSecret
s1.TwitchAuth.ClientSecret = testSecret
s1.StravaAuth.ClientSecret = testSecret
s1.GiteeAuth.ClientSecret = testSecret
s1.LivechatAuth.ClientSecret = testSecret
s1.GiteaAuth.ClientSecret = testSecret
s1.OIDCAuth.ClientSecret = testSecret
s1.OIDC2Auth.ClientSecret = testSecret
s1.OIDC3Auth.ClientSecret = testSecret
s1Bytes, err := json.Marshal(s1)
if err != nil {
t.Fatal(err)
}
s2, err := s1.RedactClone()
if err != nil {
t.Fatal(err)
}
encoded, err := json.Marshal(s2)
s2Bytes, err := json.Marshal(s2)
if err != nil {
t.Fatal(err)
}
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"support@example.com","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","authMethod":"","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"},"twitchAuth":{"enabled":false,"clientSecret":"******"},"stravaAuth":{"enabled":false,"clientSecret":"******"},"giteeAuth":{"enabled":false,"clientSecret":"******"},"livechatAuth":{"enabled":false,"clientSecret":"******"},"authentikAuth":{"enabled":false,"clientSecret":"******"},"giteaAuth":{"enabled":false,"clientSecret":"******"}}`
if bytes.Equal(s1Bytes, s2Bytes) {
t.Fatalf("Expected the 2 settings to differ, got \n%s", s2Bytes)
}
if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr)
if strings.Contains(string(s2Bytes), testSecret) {
t.Fatalf("Expected %q secret to be replaced with mask, got \n%s", testSecret, s2Bytes)
}
if !strings.Contains(string(s2Bytes), settings.SecretMask) {
t.Fatalf("Expected the secrets to be replaced with the secret mask, got \n%s", s2Bytes)
}
if !strings.Contains(string(s2Bytes), `"appName":"test123"`) {
t.Fatalf("Missing control field in \n%s", s2Bytes)
}
}
@@ -250,7 +283,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.FacebookAuth.ClientId = "facebook_test"
s.GithubAuth.ClientId = "github_test"
s.GitlabAuth.ClientId = "gitlab_test"
s.GitlabAuth.Enabled = true
s.GitlabAuth.Enabled = true // control
s.DiscordAuth.ClientId = "discord_test"
s.TwitterAuth.ClientId = "twitter_test"
s.MicrosoftAuth.ClientId = "microsoft_test"
@@ -260,8 +293,10 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.StravaAuth.ClientId = "strava_test"
s.GiteeAuth.ClientId = "gitee_test"
s.LivechatAuth.ClientId = "livechat_test"
s.AuthentikAuth.ClientId = "authentik_test"
s.GiteaAuth.ClientId = "gitea_test"
s.OIDCAuth.ClientId = "oidc_test"
s.OIDC2Auth.ClientId = "oidc2_test"
s.OIDC3Auth.ClientId = "oidc3_test"
result := s.NamedAuthProviderConfigs()
@@ -272,21 +307,23 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
encodedStr := string(encoded)
expectedParts := []string{
`"discord":{"enabled":false,"clientId":"discord_test"}`,
`"facebook":{"enabled":false,"clientId":"facebook_test"}`,
`"github":{"enabled":false,"clientId":"github_test"}`,
`"gitlab":{"enabled":true,"clientId":"gitlab_test"}`,
`"google":{"enabled":false,"clientId":"google_test"}`,
`"microsoft":{"enabled":false,"clientId":"microsoft_test"}`,
`"spotify":{"enabled":false,"clientId":"spotify_test"}`,
`"twitter":{"enabled":false,"clientId":"twitter_test"}`,
`"kakao":{"enabled":false,"clientId":"kakao_test"}`,
`"twitch":{"enabled":false,"clientId":"twitch_test"}`,
`"strava":{"enabled":false,"clientId":"strava_test"}`,
`"gitee":{"enabled":false,"clientId":"gitee_test"}`,
`"livechat":{"enabled":false,"clientId":"livechat_test"}`,
`"authentik":{"enabled":false,"clientId":"authentik_test"}`,
`"gitea":{"enabled":false,"clientId":"gitea_test"}`,
`"discord":{"enabled":false,"clientId":"discord_test"`,
`"facebook":{"enabled":false,"clientId":"facebook_test"`,
`"github":{"enabled":false,"clientId":"github_test"`,
`"gitlab":{"enabled":true,"clientId":"gitlab_test"`,
`"google":{"enabled":false,"clientId":"google_test"`,
`"microsoft":{"enabled":false,"clientId":"microsoft_test"`,
`"spotify":{"enabled":false,"clientId":"spotify_test"`,
`"twitter":{"enabled":false,"clientId":"twitter_test"`,
`"kakao":{"enabled":false,"clientId":"kakao_test"`,
`"twitch":{"enabled":false,"clientId":"twitch_test"`,
`"strava":{"enabled":false,"clientId":"strava_test"`,
`"gitee":{"enabled":false,"clientId":"gitee_test"`,
`"livechat":{"enabled":false,"clientId":"livechat_test"`,
`"gitea":{"enabled":false,"clientId":"gitea_test"`,
`"oidc":{"enabled":false,"clientId":"oidc_test"`,
`"oidc2":{"enabled":false,"clientId":"oidc2_test"`,
`"oidc3":{"enabled":false,"clientId":"oidc3_test"`,
}
for _, p := range expectedParts {
if !strings.Contains(encodedStr, p) {
+15
View File
@@ -0,0 +1,15 @@
package models
import "github.com/pocketbase/pocketbase/tools/types"
type TableInfoRow struct {
// the `db:"pk"` tag has special semantic so we cannot rename
// the original field without specifying a custom mapper
PK int
Index int `db:"cid"`
Name string `db:"name"`
Type string `db:"type"`
NotNull bool `db:"notnull"`
DefaultValue types.JsonRaw `db:"dflt_value"`
}
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/process"
"github.com/dop251/goja_nodejs/require"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
@@ -28,7 +29,6 @@ type migrations struct {
options *MigrationsOptions
}
//
// MustRegisterMigrations registers the migrations loader plugin to
// the provided app instance and panics if it fails.
//
@@ -70,6 +70,7 @@ func RegisterMigrations(app core.App, options *MigrationsOptions) error {
vm := NewBaseVM()
registry.Enable(vm)
console.Enable(vm)
process.Enable(vm)
vm.Set("migrate", func(up, down func(db dbx.Builder) error) {
m.AppMigrations.Register(up, down, file)
+3 -3
View File
@@ -5,9 +5,9 @@
//
// 1. JS Migrations loader:
//
// jsvm.MustRegisterMigrations(app, &jsvm.MigrationsOptions{
// Dir: "custom_js_migrations_dir_path", // default to "pb_data/../pb_migrations"
// })
// jsvm.MustRegisterMigrations(app, &jsvm.MigrationsOptions{
// Dir: "custom_js_migrations_dir_path", // default to "pb_data/../pb_migrations"
// })
package jsvm
import (
+7 -7
View File
@@ -5,14 +5,14 @@
//
// Example usage:
//
// migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
// TemplateLang: migratecmd.TemplateLangJS, // default to migratecmd.TemplateLangGo
// Automigrate: true,
// Dir: "migrations_dir_path", // optional template migrations path; default to "pb_migrations" (for JS) and "migrations" (for Go)
// })
// migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
// TemplateLang: migratecmd.TemplateLangJS, // default to migratecmd.TemplateLangGo
// Automigrate: true,
// Dir: "migrations_dir_path", // optional template migrations path; default to "pb_migrations" (for JS) and "migrations" (for Go)
// })
//
// Note: To allow running JS migrations you'll need to enable first
// [jsvm.MustRegisterMigrations].
// Note: To allow running JS migrations you'll need to enable first
// [jsvm.MustRegisterMigrations].
package migratecmd
import (
+22 -20
View File
@@ -42,15 +42,17 @@ var _ search.FieldResolver = (*RecordFieldResolver)(nil)
// RecordFieldResolver defines a custom search resolver struct for
// managing Record model search fields.
//
// Usually used together with `search.Provider`. Example:
// resolver := resolvers.NewRecordFieldResolver(
// app.Dao(),
// myCollection,
// &models.RequestData{...},
// true,
// )
// provider := search.NewProvider(resolver)
// ...
// Usually used together with `search.Provider`.
// Example:
//
// resolver := resolvers.NewRecordFieldResolver(
// app.Dao(),
// myCollection,
// &models.RequestData{...},
// true,
// )
// provider := search.NewProvider(resolver)
// ...
type RecordFieldResolver struct {
dao *daos.Dao
baseCollection *models.Collection
@@ -125,17 +127,17 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
//
// Example of some resolvable fieldName formats:
//
// id
// someSelect.each
// project.screen.status
// @request.status
// @request.query.filter
// @request.auth.someRelation.name
// @request.data.someRelation.name
// @request.data.someField
// @request.data.someSelect:each
// @request.data.someField:isset
// @collection.product.name
// id
// someSelect.each
// project.screen.status
// @request.status
// @request.query.filter
// @request.auth.someRelation.name
// @request.data.someRelation.name
// @request.data.someField
// @request.data.someSelect:each
// @request.data.someField:isset
// @collection.product.name
func (r *RecordFieldResolver) Resolve(fieldName string) (*search.ResolverResult, error) {
return parseAndRun(fieldName, r)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":{"original-filename":"300_UhLKX91HVb.png"},"md5":"zZhZjzVvCvpcxtMAJie3GQ=="}
@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"R7TqfvF8HP3C4+FO2eZ9tg=="}
@@ -0,0 +1,9 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="25.536" y="13.4861" width="1.71467" height="16.7338" transform="rotate(45.9772 25.536 13.4861)" fill="white"/>
<path d="M26 14H36.8C37.4628 14 38 14.5373 38 15.2V36.8C38 37.4628 37.4628 38 36.8 38H15.2C14.5373 38 14 37.4628 14 36.8V26" fill="white"/>
<path d="M26 14H36.8C37.4628 14 38 14.5373 38 15.2V36.8C38 37.4628 37.4628 38 36.8 38H15.2C14.5373 38 14 37.4628 14 36.8V26" stroke="#16161a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 14V3.2C26 2.53726 25.4628 2 24.8 2H3.2C2.53726 2 2 2.53726 2 3.2V24.8C2 25.4628 2.53726 26 3.2 26H14" fill="white"/>
<path d="M26 14V3.2C26 2.53726 25.4628 2 24.8 2H3.2C2.53726 2 2 2.53726 2 3.2V24.8C2 25.4628 2.53726 26 3.2 26H14" stroke="#16161a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 20C9.44772 20 9 19.5523 9 19V8C9 7.44772 9.44772 7 10 7H13.7531C14.4801 7 15.1591 7.07311 15.7901 7.21932C16.4348 7.35225 16.9904 7.58487 17.4568 7.91718C17.9369 8.2362 18.3141 8.6682 18.5885 9.21319C18.8628 9.74489 19 10.4029 19 11.1871C19 11.9448 18.856 12.6028 18.5679 13.161C18.2936 13.7193 17.9163 14.1779 17.4362 14.5368C16.9561 14.8957 16.4005 15.1616 15.7695 15.3344C15.1385 15.5072 14.4664 15.5936 13.7531 15.5936H13.0247C12.4724 15.5936 12.0247 16.0413 12.0247 16.5936V19C12.0247 19.5523 11.577 20 11.0247 20H10ZM12.0247 12.2607C12.0247 12.813 12.4724 13.2607 13.0247 13.2607H13.5679C15.214 13.2607 16.037 12.5695 16.037 11.1871C16.037 10.5092 15.8244 10.0307 15.3992 9.75153C14.9877 9.47239 14.3772 9.33282 13.5679 9.33282H13.0247C12.4724 9.33282 12.0247 9.78054 12.0247 10.3328V12.2607Z" fill="#16161a"/>
<path d="M22 33C21.4477 33 21 32.5523 21 32V21C21 20.4477 21.4477 20 22 20H25.4877C26.1844 20 26.8265 20.0532 27.4139 20.1595C28.015 20.2526 28.5342 20.4254 28.9713 20.6779C29.4085 20.9305 29.75 21.2628 29.9959 21.6748C30.2555 22.0869 30.3852 22.6053 30.3852 23.2301C30.3852 23.5225 30.3374 23.8149 30.2418 24.1074C30.1598 24.3998 30.0232 24.6723 29.832 24.9248C29.6407 25.1774 29.4016 25.4034 29.1148 25.6028C28.837 25.7958 28.5081 25.939 28.1279 26.0323C28.1058 26.0378 28.0902 26.0575 28.0902 26.0802V26.0802C28.0902 26.1039 28.1073 26.1242 28.1306 26.1286C29.0669 26.3034 29.7774 26.6332 30.2623 27.1181C30.7541 27.6099 31 28.2945 31 29.1718C31 29.8364 30.8702 30.408 30.6107 30.8865C30.3511 31.365 29.9891 31.7638 29.5246 32.0828C29.0601 32.3885 28.5137 32.6212 27.8852 32.7807C27.2705 32.9269 26.6011 33 25.8771 33H22ZM24.0123 24.2239C24.0123 24.7762 24.46 25.2239 25.0123 25.2239H25.3443C26.082 25.2239 26.6148 25.0844 26.9426 24.8052C27.2705 24.5261 27.4344 24.1339 27.4344 23.6288C27.4344 23.1503 27.2637 22.8113 26.9221 22.612C26.5943 22.3993 26.0751 22.2929 25.3648 22.2929H25.0123C24.46 22.2929 24.0123 22.7407 24.0123 23.2929V24.2239ZM24.0123 29.7071C24.0123 30.2593 24.46 30.7071 25.0123 30.7071H25.6311C27.2432 30.7071 28.0492 30.1222 28.0492 28.9525C28.0492 28.3809 27.8511 27.9688 27.4549 27.7163C27.0724 27.4637 26.4645 27.3374 25.6311 27.3374H25.0123C24.46 27.3374 24.0123 27.7851 24.0123 28.3374V29.7071Z" fill="#16161a"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1 @@
{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/svg+xml","user.metadata":{"original-filename":"logo_vcfJJG5TAh.svg"},"md5":"9/B7afas4c3O6vbFcbpOug=="}
+1
View File
@@ -10,6 +10,7 @@ import (
// MockMultipartData creates a mocked multipart/form-data payload.
//
// Example
//
// data, mp, err := tests.MockMultipartData(
// map[string]string{"title": "new"},
// "file1",
+6 -2
View File
@@ -112,10 +112,14 @@ func NewProviderByName(name string) (Provider, error) {
return NewGiteeProvider(), nil
case NameLivechat:
return NewLivechatProvider(), nil
case NameAuthentik:
return NewAuthentikProvider(), nil
case NameGitea:
return NewGiteaProvider(), nil
case NameOIDC:
return NewOIDCProvider(), nil
case NameOIDC + "2":
return NewOIDCProvider(), nil
case NameOIDC + "3":
return NewOIDCProvider(), nil
default:
return nil, errors.New("Missing provider " + name)
}
+27 -9
View File
@@ -136,15 +136,6 @@ func TestNewProviderByName(t *testing.T) {
t.Error("Expected to be instance of *auth.Livechat")
}
// authentik
p, err = auth.NewProviderByName(auth.NameAuthentik)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.Authentik); !ok {
t.Error("Expected to be instance of *auth.Authentik")
}
// gitea
p, err = auth.NewProviderByName(auth.NameGitea)
if err != nil {
@@ -153,4 +144,31 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Gitea); !ok {
t.Error("Expected to be instance of *auth.Gitea")
}
// oidc
p, err = auth.NewProviderByName(auth.NameOIDC)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.OIDC); !ok {
t.Error("Expected to be instance of *auth.OIDC")
}
// oidc2
p, err = auth.NewProviderByName(auth.NameOIDC + "2")
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.OIDC); !ok {
t.Error("Expected to be instance of *auth.OIDC")
}
// oidc3
p, err = auth.NewProviderByName(auth.NameOIDC + "3")
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.OIDC); !ok {
t.Error("Expected to be instance of *auth.OIDC")
}
}
+11 -11
View File
@@ -6,19 +6,19 @@ import (
"golang.org/x/oauth2"
)
var _ Provider = (*Authentik)(nil)
var _ Provider = (*OIDC)(nil)
// NameAuthentik is the unique name of the Authentik provider.
const NameAuthentik string = "authentik"
// NameOIDC is the unique name of the OpenID Connect (OIDC) provider.
const NameOIDC string = "oidc"
// Authentik allows authentication via Authentik OAuth2.
type Authentik struct {
// OIDC allows authentication via OpenID Connect (OIDC) OAuth2 provider.
type OIDC struct {
*baseProvider
}
// NewAuthentikProvider creates new Authentik provider instance with some defaults.
func NewAuthentikProvider() *Authentik {
return &Authentik{&baseProvider{
// NewOIDCProvider creates new OpenID Connect (OIDC) provider instance with some defaults.
func NewOIDCProvider() *OIDC {
return &OIDC{&baseProvider{
scopes: []string{
"openid", // minimal requirement to return the id
"email",
@@ -27,10 +27,10 @@ func NewAuthentikProvider() *Authentik {
}}
}
// FetchAuthUser returns an AuthUser instance based the Authentik's user api.
// FetchAuthUser returns an AuthUser instance based the provider's user api.
//
// API reference: https://goauthentik.io/docs/providers/oauth2/
func (p *Authentik) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// API reference: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
func (p *OIDC) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserData(token)
if err != nil {
return nil, err
+12 -23
View File
@@ -13,7 +13,6 @@ import (
"sort"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
@@ -101,7 +100,7 @@ func (s *System) Attributes(fileKey string) (*blob.Attributes, error) {
// GetFile returns a file content reader for the given fileKey.
//
// NB! Make sure to call `Close()` after you are done working with it.
func (s *System) GetFile(fileKey string) (io.ReadCloser, error) {
func (s *System) GetFile(fileKey string) (*blob.Reader, error) {
br, err := s.bucket.NewReader(s.ctx, fileKey, nil)
if err != nil {
return nil, err
@@ -316,37 +315,27 @@ func (s *System) Serve(res http.ResponseWriter, req *http.Request, fileKey strin
extContentType = ct
}
// clickjacking shouldn't be a concern when serving uploaded files,
// so it safe to unset the global X-Frame-Options to allow files embedding
// (see https://github.com/pocketbase/pocketbase/issues/677)
res.Header().Del("X-Frame-Options")
res.Header().Set("Content-Disposition", disposition+"; filename="+name)
res.Header().Set("Content-Type", extContentType)
res.Header().Set("Content-Length", strconv.FormatInt(br.Size(), 10))
res.Header().Set("Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox")
// all HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT)
// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
//
// NB! time.LoadLocation may fail on non-Unix systems (see https://github.com/pocketbase/pocketbase/issues/45)
location, locationErr := time.LoadLocation("GMT")
if locationErr == nil {
res.Header().Set("Last-Modified", br.ModTime().In(location).Format("Mon, 02 Jan 06 15:04:05 MST"))
}
setHeaderIfMissing(res, "Content-Disposition", disposition+"; filename="+name)
setHeaderIfMissing(res, "Content-Type", extContentType)
setHeaderIfMissing(res, "Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox")
// set a default cache-control header
// (valid for 30 days but the cache is allowed to reuse the file for any requests
// that are made in the last day while revalidating the res in the background)
if res.Header().Get("Cache-Control") == "" {
res.Header().Set("Cache-Control", "max-age=2592000, stale-while-revalidate=86400")
}
setHeaderIfMissing(res, "Cache-Control", "max-age=2592000, stale-while-revalidate=86400")
http.ServeContent(res, req, name, br.ModTime(), br)
return nil
}
// note: expects key to be in a canonical form (eg. "accept-encoding" should be "Accept-Encoding").
func setHeaderIfMissing(res http.ResponseWriter, key string, value string) {
if _, ok := res.Header()[key]; !ok {
res.Header().Set(key, value)
}
}
var ThumbSizeRegex = regexp.MustCompile(`^(\d+)x(\d+)(t|b|f)?$`)
// CreateThumb creates a new thumb image for the file at originalKey location.
+50 -19
View File
@@ -251,9 +251,13 @@ func TestFileSystemServe(t *testing.T) {
}
defer fs.Close()
csp := "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox"
cacheControl := "max-age=2592000, stale-while-revalidate=86400"
scenarios := []struct {
path string
name string
customHeaders map[string]string
expectError bool
expectHeaders map[string]string
}{
@@ -261,6 +265,7 @@ func TestFileSystemServe(t *testing.T) {
// missing
"missing.txt",
"test_name.txt",
nil,
true,
nil,
},
@@ -268,84 +273,110 @@ func TestFileSystemServe(t *testing.T) {
// existing regular file
"test/sub1.txt",
"test_name.txt",
nil,
false,
map[string]string{
"Content-Disposition": "attachment; filename=test_name.txt",
"Content-Type": "application/octet-stream",
"Content-Length": "0",
"Content-Security-Policy": "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox",
"Content-Security-Policy": csp,
"Cache-Control": cacheControl,
},
},
{
// png inline
"image.png",
"test_name.png",
nil,
false,
map[string]string{
"Content-Disposition": "inline; filename=test_name.png",
"Content-Type": "image/png",
"Content-Length": "73",
"Content-Security-Policy": "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox",
"Content-Security-Policy": csp,
"Cache-Control": cacheControl,
},
},
{
// svg exception
"image.svg",
"test_name.svg",
nil,
false,
map[string]string{
"Content-Disposition": "attachment; filename=test_name.svg",
"Content-Type": "image/svg+xml",
"Content-Length": "0",
"Content-Security-Policy": "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox",
"Content-Security-Policy": csp,
"Cache-Control": cacheControl,
},
},
{
// css exception
"style.css",
"test_name.css",
nil,
false,
map[string]string{
"Content-Disposition": "attachment; filename=test_name.css",
"Content-Type": "text/css",
"Content-Length": "0",
"Content-Security-Policy": "default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox",
"Content-Security-Policy": csp,
"Cache-Control": cacheControl,
},
},
{
// custom header
"test/sub2.txt",
"test_name.txt",
map[string]string{
"Content-Disposition": "1",
"Content-Type": "2",
"Content-Length": "3",
"Content-Security-Policy": "4",
"Cache-Control": "5",
"X-Custom": "6",
},
false,
map[string]string{
"Content-Disposition": "1",
"Content-Type": "2",
"Content-Length": "0", // overwriten by http.ServeContent
"Content-Security-Policy": "4",
"Cache-Control": "5",
"X-Custom": "6",
},
},
}
for _, scenario := range scenarios {
for _, s := range scenarios {
res := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/", nil)
err := fs.Serve(res, req, scenario.path, scenario.name)
for k, v := range s.customHeaders {
res.Header().Set(k, v)
}
err := fs.Serve(res, req, s.path, s.name)
hasErr := err != nil
if hasErr != scenario.expectError {
t.Errorf("(%s) Expected hasError %v, got %v (%v)", scenario.path, scenario.expectError, hasErr, err)
if hasErr != s.expectError {
t.Errorf("(%s) Expected hasError %v, got %v (%v)", s.path, s.expectError, hasErr, err)
continue
}
if scenario.expectError {
if s.expectError {
continue
}
result := res.Result()
for hName, hValue := range scenario.expectHeaders {
for hName, hValue := range s.expectHeaders {
v := result.Header.Get(hName)
if v != hValue {
t.Errorf("(%s) Expected value %q for header %q, got %q", scenario.path, hValue, hName, v)
t.Errorf("(%s) Expected value %q for header %q, got %q", s.path, hValue, hName, v)
}
}
if v := result.Header.Get("X-Frame-Options"); v != "" {
t.Errorf("(%s) Expected the X-Frame-Options header to be unset, got %v", scenario.path, v)
}
if v := result.Header.Get("Cache-Control"); v == "" {
t.Errorf("(%s) Expected Cache-Control header to be set, got empty string", scenario.path)
}
}
}
+21 -3
View File
@@ -8,9 +8,9 @@ import (
// Message defines a generic email message struct.
type Message struct {
From mail.Address
To mail.Address
Bcc []string
Cc []string
To []mail.Address
Bcc []mail.Address
Cc []mail.Address
Subject string
HTML string
Text string
@@ -23,3 +23,21 @@ type Mailer interface {
// Send sends an email with the provided Message.
Send(message *Message) error
}
// addressesToStrings converts the provided address to a list of serialized RFC 5322 strings.
//
// To export only the email part of mail.Address, you can set withName to false.
func addressesToStrings(addresses []mail.Address, withName bool) []string {
result := make([]string, len(addresses))
for i, addr := range addresses {
if withName && addr.Name != "" {
result[i] = addr.String()
} else {
// keep only the email part to avoid wrapping in angle-brackets
result[i] = addr.Address
}
}
return result
}
+5 -2
View File
@@ -6,6 +6,7 @@ import (
"mime"
"net/http"
"os/exec"
"strings"
)
var _ Mailer = (*Sendmail)(nil)
@@ -19,11 +20,13 @@ type Sendmail struct {
// Send implements `mailer.Mailer` interface.
func (c *Sendmail) Send(m *Message) error {
toAddresses := addressesToStrings(m.To, false)
headers := make(http.Header)
headers.Set("Subject", mime.QEncoding.Encode("utf-8", m.Subject))
headers.Set("From", m.From.String())
headers.Set("To", m.To.String())
headers.Set("Content-Type", "text/html; charset=UTF-8")
headers.Set("To", strings.Join(toAddresses, ","))
cmdPath, err := findSendmailPath()
if err != nil {
@@ -51,7 +54,7 @@ func (c *Sendmail) Send(m *Message) error {
}
// ---
sendmail := exec.Command(cmdPath, m.To.Address)
sendmail := exec.Command(cmdPath, strings.Join(toAddresses, ","))
sendmail.Stdin = &buffer
return sendmail.Run()
+6 -3
View File
@@ -75,7 +75,6 @@ func (c *SmtpClient) Send(m *Message) error {
yak.FromName(m.From.Name)
}
yak.From(m.From.Address)
yak.To(m.To.Address)
yak.Subject(m.Subject)
yak.HTML().Set(m.HTML)
@@ -88,12 +87,16 @@ func (c *SmtpClient) Send(m *Message) error {
yak.Plain().Set(m.Text)
}
if len(m.To) > 0 {
yak.To(addressesToStrings(m.To, true)...)
}
if len(m.Bcc) > 0 {
yak.Bcc(m.Bcc...)
yak.Bcc(addressesToStrings(m.Bcc, true)...)
}
if len(m.Cc) > 0 {
yak.Cc(m.Cc...)
yak.Cc(addressesToStrings(m.Cc, true)...)
}
// add attachements (if any)
+3 -6
View File
@@ -5,7 +5,6 @@ import (
"math"
"net/url"
"strconv"
"strings"
"github.com/pocketbase/dbx"
)
@@ -198,11 +197,9 @@ func (s *Provider) Exec(items any) (*Result, error) {
if len(queryInfo.From) > 0 {
baseTable = queryInfo.From[0]
}
countQuery := modelsQuery
rawCountQuery := countQuery.Select(strings.Join([]string{baseTable, "id"}, ".")).OrderBy().Build().SQL()
wrappedCountQuery := queryInfo.Builder.NewQuery("SELECT COUNT(*) FROM (" + rawCountQuery + ")")
wrappedCountQuery.Bind(countQuery.Build().Params())
if err := wrappedCountQuery.Row(&totalCount); err != nil {
clone := modelsQuery
countQuery := clone.Select("COUNT(DISTINCT [[" + baseTable + ".id]])").OrderBy()
if err := countQuery.Row(&totalCount); err != nil {
return nil, err
}
+6 -6
View File
@@ -228,7 +228,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":10,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE NOT (`test1` IS NULL))",
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10",
},
},
@@ -241,7 +241,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":30,"totalItems":2,"totalPages":1,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE NOT (`test1` IS NULL))",
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30",
},
},
@@ -274,7 +274,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"totalPages":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2))",
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2)",
"SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (COALESCE(test2, '') != COALESCE(null, ''))) AND (test1 >= 2) ORDER BY `test1` ASC, `test2` DESC LIMIT 500",
},
},
@@ -287,7 +287,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":1,"perPage":10,"totalItems":0,"totalPages":0,"items":[]}`,
[]string{
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')))",
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', ''))",
"SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (COALESCE(test3, '') != COALESCE('', '')) ORDER BY `test1` ASC, `test3` ASC LIMIT 10",
},
},
@@ -300,7 +300,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
false,
`{"page":2,"perPage":1,"totalItems":2,"totalPages":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`,
[]string{
"SELECT COUNT(*) FROM (SELECT `test`.`id` FROM `test` WHERE NOT (`test1` IS NULL))",
"SELECT COUNT(DISTINCT [[test.id]]) FROM `test` WHERE NOT (`test1` IS NULL)",
"SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1",
},
},
@@ -345,7 +345,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) {
for _, q := range testDB.CalledQueries {
if !list.ExistInSliceWithRegex(q, s.expectQueries) {
t.Errorf("[%s] Didn't expect query \n%v in \n%v", s.name, q, testDB.CalledQueries)
t.Fatalf("[%s] Didn't expect query \n%v \nin \n%v", s.name, q, s.expectQueries)
}
}
}
+1
View File
@@ -40,6 +40,7 @@ func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) {
// into a slice of SortFields.
//
// Example:
//
// fields := search.ParseSortFromString("-name,+created")
func ParseSortFromString(str string) (fields []SortField) {
data := strings.Split(str, ",")
+212
View File
@@ -0,0 +1,212 @@
// Package tokenizer implements a rudimentary tokens parser of buffered
// io.Reader while respecting quotes and parenthesis boundaries.
//
// Example
//
// tk := tokenizer.NewFromString("a, b, (c, d)")
// result, _ := tk.ScanAll() // ["a", "b", "(c, d)"]
package tokenizer
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
)
// eof represents a marker rune for the end of the reader.
const eof = rune(0)
// DefaultSeparators is a list with the default token separator characters.
var DefaultSeparators = []rune{','}
// NewFromString creates new Tokenizer from the provided string.
func NewFromString(str string) *Tokenizer {
return New(strings.NewReader(str))
}
// NewFromBytes creates new Tokenizer from the provided bytes slice.
func NewFromBytes(b []byte) *Tokenizer {
return New(bytes.NewReader(b))
}
// New creates new Tokenizer from the provided reader with DefaultSeparators.
func New(r io.Reader) *Tokenizer {
return &Tokenizer{
r: bufio.NewReader(r),
separators: DefaultSeparators,
keepSeparator: false,
}
}
// Tokenizer defines a struct that parses a reader into tokens while
// respecting quotes and parenthesis boundaries.
type Tokenizer struct {
r *bufio.Reader
separators []rune
keepSeparator bool
}
// Separators defines the provided separatos of the current Tokenizer.
func (s *Tokenizer) Separators(separators ...rune) {
s.separators = separators
}
// KeepSeparator defines whether to keep the separator rune as part
// of the token (default to false).
func (s *Tokenizer) KeepSeparator(state bool) {
s.keepSeparator = state
}
// Scan reads and returns the next available token from the Tokenizer's buffer (trimmed).
//
// Returns [io.EOF] error when there are no more tokens to scan.
func (s *Tokenizer) Scan() (string, error) {
ch := s.read()
if ch == eof {
return "", io.EOF
}
if isWhitespaceRune(ch) {
s.readWhiteSpaces()
} else {
s.unread()
}
token, err := s.readToken()
if err != nil {
return "", err
}
// read all remaining whitespaces
s.readWhiteSpaces()
return token, err
}
// ScanAll reads the entire Tokenizer's buffer and return all found tokens.
func (s *Tokenizer) ScanAll() ([]string, error) {
tokens := []string{}
for {
token, err := s.Scan()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
tokens = append(tokens, token)
}
return tokens, nil
}
// readToken reads a single token from the buffer and returns it.
func (s *Tokenizer) readToken() (string, error) {
var buf bytes.Buffer
var parenthesis int
var quoteCh rune
var prevCh rune
for {
ch := s.read()
if ch == eof {
break
}
if !isEscapeRune(prevCh) {
if ch == '(' && quoteCh == eof {
parenthesis++
} else if ch == ')' && parenthesis > 0 && quoteCh == eof {
parenthesis--
} else if isQuoteRune(ch) {
if quoteCh == ch {
quoteCh = eof // reached closing quote
} else if quoteCh == eof {
quoteCh = ch // opening quote
}
}
}
if s.isSeperatorRune(ch) && parenthesis == 0 && quoteCh == eof {
if s.keepSeparator {
buf.WriteRune(ch)
}
break
}
prevCh = ch
buf.WriteRune(ch)
}
if parenthesis > 0 || quoteCh != eof {
return "", fmt.Errorf("unbalanced parenthesis or quoted expression: %q", buf.String())
}
return buf.String(), nil
}
// readWhiteSpaces consumes all contiguous whitespace runes.
func (s *Tokenizer) readWhiteSpaces() {
for {
ch := s.read()
if ch == eof {
break
}
if !s.isSeperatorRune(ch) {
s.unread()
break
}
}
}
// read reads the next rune from the buffered reader.
// Returns the `rune(0)` if an error or `io.EOF` occurs.
func (s *Tokenizer) read() rune {
ch, _, err := s.r.ReadRune()
if err != nil {
return eof
}
return ch
}
// unread places the previously read rune back on the reader.
func (s *Tokenizer) unread() error {
return s.r.UnreadRune()
}
// isSeperatorRune checks if a rune is a token part separator.
func (s *Tokenizer) isSeperatorRune(ch rune) bool {
for _, r := range s.separators {
if ch == r {
return true
}
}
return false
}
// isWhitespaceRune checks if a rune is a space, tab, or newline.
func isWhitespaceRune(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n'
}
// isQuoteRune checks if a rune is a quote.
func isQuoteRune(ch rune) bool {
return ch == '\'' || ch == '"' || ch == '`'
}
// isEscapeRune checks if a rune is an escape character.
func isEscapeRune(ch rune) bool {
return ch == '\\'
}
+233
View File
@@ -0,0 +1,233 @@
package tokenizer
import (
"io"
"strings"
"testing"
)
func TestFactories(t *testing.T) {
expectedContent := "test"
scenarios := []struct {
name string
tk *Tokenizer
}{
{
"New()",
New(strings.NewReader(expectedContent)),
},
{
"NewFromString()",
NewFromString(expectedContent),
},
{
"NewFromBytes()",
NewFromBytes([]byte(expectedContent)),
},
}
for _, s := range scenarios {
content, _ := s.tk.r.ReadString(0)
if content != expectedContent {
t.Fatalf("[%s] Expected reader with content %q, got %q", s.name, expectedContent, content)
}
if s.tk.keepSeparator != false {
t.Fatalf("[%s] Expected false, got true", s.name)
}
if len(s.tk.separators) != len(DefaultSeparators) {
t.Fatalf("[%s] Expected \n%v, \ngot \n%v", s.name, DefaultSeparators, s.tk.separators)
}
for _, r := range s.tk.separators {
exists := false
for _, def := range s.tk.separators {
if r == def {
exists = true
break
}
}
if !exists {
t.Fatalf("[%s] Unexpected sepator %s", s.name, string(r))
}
}
}
}
func TestScan(t *testing.T) {
tk := NewFromString("abc, 123.456, (abc)")
expectedTokens := []string{"abc", "123.456", "(abc)"}
for _, token := range expectedTokens {
result, err := tk.Scan()
if err != nil {
t.Fatalf("Expected token %q, got error %v", token, err)
}
if result != token {
t.Fatalf("Expected token %q, got error %v", token, result)
}
}
// scan the last character
token, err := tk.Scan()
if err != io.EOF {
t.Fatalf("Expected EOF error, got %v", err)
}
if token != "" || err != io.EOF {
t.Fatalf("Expected empty token, got %q", token)
}
}
func TestScanAll(t *testing.T) {
scenarios := []struct {
name string
content string
separators []rune
keepSeparator bool
expectError bool
expectTokens []string
}{
{
"empty string",
"",
DefaultSeparators,
false,
false,
nil,
},
{
"unbalanced parenthesis",
`(a,b() c`,
DefaultSeparators,
false,
true,
[]string{},
},
{
"unmatching quotes",
`'asd"`,
DefaultSeparators,
false,
true,
[]string{},
},
{
"no separators",
`a, b, c, d, e 123, "abc"`,
nil,
false,
false,
[]string{
`a, b, c, d, e 123, "abc"`,
},
},
{
"default separators",
`a, b, c, d e, "a,b, c ", (123, 456)`,
DefaultSeparators,
false,
false,
[]string{
"a",
"b",
"c",
"d e",
`"a,b, c "`,
`(123, 456)`,
},
},
{
"default separators (with preserve)",
`a, b, c, d e, "a,b, c ", (123, 456)`,
DefaultSeparators,
true,
false,
[]string{
"a,",
"b,",
"c,",
"d e,",
`"a,b, c ",`,
`(123, 456)`,
},
},
{
"custom separators",
` a , 123.456, b, c d, (
test (a,b,c) " 123 "
),"(abc d", "abc) d", "(abc) d \" " 'abc "'`,
[]rune{',', ' ', '\t', '\n'},
false,
false,
[]string{
"a",
"123.456",
"b",
"c",
"d",
"(\n\t\t\t\ttest (a,b,c) \" 123 \"\n\t\t\t)",
`"(abc d"`,
`"abc) d"`,
`"(abc) d \" "`,
`'abc "'`,
},
},
{
"custom separators (with preserve)",
` a , 123.456, b, c d, (
test (a,b,c) " 123 "
),"(abc d", "abc) d", "(abc) d \" " 'abc "'`,
[]rune{',', ' ', '\t', '\n'},
true,
false,
[]string{
"a ",
"123.456,",
"b,",
"c ",
"d,",
"(\n\t\t\t\ttest (a,b,c) \" 123 \"\n\t\t\t),",
`"(abc d",`,
`"abc) d",`,
`"(abc) d \" " `,
`'abc "'`,
},
},
}
for _, s := range scenarios {
tk := NewFromString(s.content)
tk.Separators(s.separators...)
tk.KeepSeparator(s.keepSeparator)
tokens, err := tk.ScanAll()
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
}
if len(tokens) != len(s.expectTokens) {
t.Fatalf("[%s] Expected \n%v (%d), \ngot \n%v (%d)", s.name, s.expectTokens, len(s.expectTokens), tokens, len(tokens))
}
for _, tok := range tokens {
exists := false
for _, def := range s.expectTokens {
if tok == def {
exists = true
break
}
}
if !exists {
t.Fatalf("[%s] Unexpected token %s", s.name, tok)
}
}
}
}
+1 -1
View File
@@ -8,4 +8,4 @@ PB_DOCS_URL = "https://pocketbase.io/docs/"
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.12.3"
PB_VERSION = "v0.13.3"
-2
View File
@@ -6,8 +6,6 @@ This is the PocketBase Admin dashboard UI (built with Svelte and Vite).
Although it could be used independently, it is mainly intended to be embedded
as part of a PocketBase app executable (hence the `embed.go` file).
The used admins avatars are from https://boringavatars.com/.
## Development
Download the repo and run the appropriate console commands:
@@ -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 me,f as k,g as h,h as n,m as _e,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 m,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index-3e0f12d8.js";import{S as Be}from"./SdkTabs-ed893501.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+"",_,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),_=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,_),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(_,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&m(o),i=!1,u()}}}function he(a,l){let o,s,_,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),me(s.$$.fragment),_=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),_e(s,o,null),n(o,_),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&&m(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",_,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+"",E,x,I,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 me,f as k,g as h,h as n,m as _e,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 m,d as pe,R as Me,C as Se,p as $e,r as H,u as je,N as Ae}from"./index-4bd199fb.js";import{S as Be}from"./SdkTabs-fc1a80c5.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+"",_,f,i,u;function d(){return l[4](l[5])}return{key:a,first:null,c(){o=r("button"),_=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,_),n(o,f),i||(u=je(o,"click",d),i=!0)},p(v,C){l=v,C&4&&s!==(s=l[5].code+"")&&G(_,s),C&6&&H(o,"active",l[1]===l[5].code)},d(v){v&&m(o),i=!1,u()}}}function he(a,l){let o,s,_,f;return s=new Ae({props:{content:l[5].body}}),{key:a,first:null,c(){o=r("div"),me(s.$$.fragment),_=g(),k(o,"class","tab-item"),H(o,"active",l[1]===l[5].code),this.first=o},m(i,u){h(i,o,u),_e(s,o,null),n(o,_),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&&m(o),pe(s)}}}function Oe(a){var ae,ne;let l,o,s=a[0].name+"",_,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+"",E,x,I,B,J,S,O,w=[],ee=new Map,te,T,p=[],le=new Map,$;P=new Be({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}');
@@ -1,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 Je,Q as Ke,n as Qe,t as U,a as j,o as d,d as ie,R as Ie,C as He,p as We,r as x,u as Ge}from"./index-3e0f12d8.js";import{S as Xe}from"./SdkTabs-ed893501.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,J,S,F,ce,L,M,de,K,N=r[0].name+"",Q,ue,pe,V,I,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 Je,Q as Ke,n as Qe,t as U,a as j,o as d,d as ie,R as Ie,C as He,p as We,r as x,u as Ge}from"./index-4bd199fb.js";import{S as Xe}from"./SdkTabs-fc1a80c5.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,J,S,F,ce,L,M,de,K,N=r[0].name+"",Q,ue,pe,V,I,D,W,T,G,fe,X,C,Y,he,Z,be,h,me,P,_e,ke,ve,ee,ge,te,ye,Se,$e,oe,we,le,O,se,R,q,$=[],Te=new Map,Ce,H,y=[],Re=new Map,A;g=new Xe({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${r[3]}');
@@ -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-3e0f12d8.js";import{S as Ze}from"./SdkTabs-ed893501.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-4bd199fb.js";import{S as Ze}from"./SdkTabs-fc1a80c5.js";function Fe(i,l,o){const n=i.slice();return n[5]=l[o],n}function Le(i,l,o){const n=i.slice();return n[5]=l[o],n}function xe(i,l){let o,n=l[5].code+"",m,_,d,b;function g(){return l[4](l[5])}return{key:i,first:null,c(){o=s("button"),m=v(n),_=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(k,R){r(k,o,R),a(o,m),a(o,_),d||(b=Xe(o,"click",g),d=!0)},p(k,R){l=k,R&4&&n!==(n=l[5].code+"")&&de(m,n),R&6&&J(o,"active",l[1]===l[5].code)},d(k){k&&c(o),d=!1,b()}}}function Me(i,l){let o,n,m,_;return n=new We({props:{content:l[5].body}}),{key:i,first:null,c(){o=s("div"),re(n.$$.fragment),m=p(),h(o,"class","tab-item"),J(o,"active",l[1]===l[5].code),this.first=o},m(d,b){r(d,o,b),ce(n,o,null),a(o,m),_=!0},p(d,b){l=d;const g={};b&4&&(g.content=l[5].body),n.$set(g),(!_||b&6)&&J(o,"active",l[1]===l[5].code)},i(d){_||(j(n.$$.fragment,d),_=!0)},o(d){H(n.$$.fragment,d),_=!1},d(d){d&&c(o),ue(n)}}}function et(i){var qe,Ie;let l,o,n=i[0].name+"",m,_,d,b,g,k,R,C,N,y,L,pe,x,D,he,Q,M=i[0].name+"",z,be,K,q,Y,I,G,P,X,O,Z,fe,ee,$,te,me,ae,_e,f,ve,E,ge,ke,we,le,Se,oe,Re,ye,Oe,se,$e,ne,U,ie,A,V,S=[],Ae=new Map,Ee,B,w=[],Te=new Map,T;k=new Ze({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[3]}');
@@ -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-3e0f12d8.js";import{S as Ae}from"./SdkTabs-ed893501.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,N,Et,nt,Z=n[0].name+"",it,Wt,rt,I,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,Nt,yt,It,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-4bd199fb.js";import{S as Ae}from"./SdkTabs-fc1a80c5.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,N,Et,nt,Z=n[0].name+"",it,Wt,rt,I,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,Nt,yt,It,k,Vt,H,jt,Jt,Qt,gt,Kt,Pt,zt,Gt,Xt,Rt,Zt,$t,J,Ct,L,Q,A=[],xt=new Map,te,K,P=[],ee=new Map,Y;function le(t,a){if(t[1]&&t[2])return Me;if(t[1])return Ue;if(t[2])return Te}let q=le(n),R=q&&q(n);T=new Ae({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${n[6]}');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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-3e0f12d8.js";import{S as Ae}from"./SdkTabs-ed893501.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,F,w,I,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-4bd199fb.js";import{S as Ae}from"./SdkTabs-fc1a80c5.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,F,w,I,R,L,P,N,te,K,T,le,Q,M=o[0].name+"",z,se,G,E,J,y,V,B,X,S,q,g=[],ae=new Map,oe,A,k=[],ne=new Map,O;w=new Ae({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}');
@@ -1,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-3e0f12d8.js";import{S as De}from"./SdkTabs-ed893501.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,F=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-4bd199fb.js";import{S as De}from"./SdkTabs-fc1a80c5.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,F=o[0].name+"",G,le,J,E,V,T,X,g,Y,N,A,P=[],ae=new Map,oe,D,k=[],ne=new Map,C;R=new De({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}');
@@ -1,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-3e0f12d8.js";import{S as Ve}from"./SdkTabs-ed893501.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+"",F,ee,I,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-4bd199fb.js";import{S as Ve}from"./SdkTabs-fc1a80c5.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+"",F,ee,I,P,L,B,Q,T,A,te,R,q,le,z,U=i[0].name+"",G,se,J,O,W,S,X,E,Y,g,N,$=[],oe=new Map,ie,V,k=[],ne=new Map,y;P=new Ve({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[3]}');
@@ -1,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-3e0f12d8.js";import{S as Nt}from"./SdkTabs-ed893501.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,J,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?Jt: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-4bd199fb.js";import{S as Nt}from"./SdkTabs-fc1a80c5.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,J,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?Jt:Vt}let z=M(o),P=z(o);return{c(){e=a("tr"),e.innerHTML='<td colspan="3" class="txt-hint">Auth fields</td>',l=m(),s=a("tr"),s.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span>
<span>username</span></div></td>
<td><span class="label">String</span></td>
<td>The username of the auth record.
@@ -1,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-3e0f12d8.js";import{S as He}from"./SdkTabs-ed893501.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-4bd199fb.js";import{S as He}from"./SdkTabs-fc1a80c5.js";function ke(o,l,s){const a=o.slice();return a[6]=l[s],a}function he(o,l,s){const a=o.slice();return a[6]=l[s],a}function ve(o){let l;return{c(){l=c("p"),l.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",m(l,"class","txt-hint txt-sm txt-right")},m(s,a){d(s,l,a)},d(s){s&&f(l)}}}function ye(o,l){let s,a=l[6].code+"",h,i,r,u;function $(){return l[5](l[6])}return{key:o,first:null,c(){s=c("button"),h=D(a),i=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(b,g){d(b,s,g),n(s,h),n(s,i),r||(u=Se(s,"click",$),r=!0)},p(b,g){l=b,g&20&&N(s,"active",l[2]===l[6].code)},d(b){b&&f(s),r=!1,u()}}}function De(o,l){let s,a,h,i;return a=new qe({props:{content:l[6].body}}),{key:o,first:null,c(){s=c("div"),$e(a.$$.fragment),h=k(),m(s,"class","tab-item"),N(s,"active",l[2]===l[6].code),this.first=s},m(r,u){d(r,s,u),we(a,s,null),n(s,h),i=!0},p(r,u){l=r,(!i||u&20)&&N(s,"active",l[2]===l[6].code)},i(r){i||(ee(a.$$.fragment,r),i=!0)},o(r){te(a.$$.fragment,r),i=!1},d(r){r&&f(s),ge(a)}}}function Le(o){var ue,pe;let l,s,a=o[0].name+"",h,i,r,u,$,b,g,q=o[0].name+"",z,le,F,C,K,O,Q,y,H,se,L,E,oe,G,U=o[0].name+"",J,ae,V,ne,W,T,X,B,Y,I,Z,R,A,w=[],ie=new Map,re,M,v=[],ce=new Map,P;C=new He({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o as m,w as _,h as t,N as he,c as Yt,m as Zt,x as we,O as Le,P as Me,k as Be,Q as Ie,n as Ge,t as Bt,a as It,d as te,R as ze,C as _e,p as Ue,r as xe}from"./index-3e0f12d8.js";import{S as je}from"./SdkTabs-ed893501.js";function Qe(d){let n,a,r;return{c(){n=e("span"),n.textContent="Show details",a=s(),r=e("i"),o(n,"class","txt"),o(r,"class","ri-arrow-down-s-line")},m(f,p){u(f,n,p),u(f,a,p),u(f,r,p)},d(f){f&&m(n),f&&m(a),f&&m(r)}}}function Je(d){let n,a,r;return{c(){n=e("span"),n.textContent="Hide details",a=s(),r=e("i"),o(n,"class","txt"),o(r,"class","ri-arrow-up-s-line")},m(f,p){u(f,n,p),u(f,a,p),u(f,r,p)},d(f){f&&m(n),f&&m(a),f&&m(r)}}}function Ae(d){let n,a,r,f,p,b,x,$,h,w,c,V,bt,Gt,R,zt,q,it,F,W,ee,I,G,le,at,ht,X,xt,se,rt,ct,Y,O,Ut,wt,y,Z,_t,jt,$t,z,tt,Ct,Qt,kt,L,dt,gt,ne,ft,oe,D,vt,et,yt,U,pt,ie,H,Ft,lt,Lt,st,At,nt,j,E,Jt,Tt,Kt,Pt,C,Q,M,ae,Rt,re,ut,ce,B,Ot,de,Et,Vt,St,Wt,A,mt,J,K,S,Nt,fe,T,k,pe,N,v,ot,ue,P,qt,me,Dt,be,Ht,Xt,Mt;return{c(){n=e("p"),n.innerHTML=`The syntax basically follows the format
import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o as m,w as _,h as t,N as he,c as Yt,m as Zt,x as we,O as Le,P as Me,k as Be,Q as Ie,n as Ge,t as Bt,a as It,d as te,R as Ue,C as _e,p as je,r as xe}from"./index-4bd199fb.js";import{S as Qe}from"./SdkTabs-fc1a80c5.js";function ze(d){let n,a,r;return{c(){n=e("span"),n.textContent="Show details",a=s(),r=e("i"),o(n,"class","txt"),o(r,"class","ri-arrow-down-s-line")},m(f,p){u(f,n,p),u(f,a,p),u(f,r,p)},d(f){f&&m(n),f&&m(a),f&&m(r)}}}function Je(d){let n,a,r;return{c(){n=e("span"),n.textContent="Hide details",a=s(),r=e("i"),o(n,"class","txt"),o(r,"class","ri-arrow-up-s-line")},m(f,p){u(f,n,p),u(f,a,p),u(f,r,p)},d(f){f&&m(n),f&&m(a),f&&m(r)}}}function Ae(d){let n,a,r,f,p,b,x,$,h,w,c,V,bt,Gt,R,Ut,q,it,F,W,ee,I,G,le,at,ht,X,xt,se,rt,ct,Y,O,jt,wt,y,Z,_t,Qt,$t,U,tt,Ct,zt,kt,L,dt,gt,ne,ft,oe,D,vt,et,yt,j,pt,ie,H,Ft,lt,Lt,st,At,nt,Q,E,Jt,Tt,Kt,Pt,C,z,M,ae,Rt,re,ut,ce,B,Ot,de,Et,Vt,St,Wt,A,mt,J,K,S,Nt,fe,T,k,pe,N,v,ot,ue,P,qt,me,Dt,be,Ht,Xt,Mt;return{c(){n=e("p"),n.innerHTML=`The syntax basically follows the format
<code><span class="txt-success">OPERAND</span>
<span class="txt-danger">OPERATOR</span>
<span class="txt-success">OPERAND</span></code>, where:`,a=s(),r=e("ul"),f=e("li"),f.innerHTML=`<code class="txt-success">OPERAND</code> - could be any of the above field literal, string (single
or double quoted), number, null, true, false`,p=s(),b=e("li"),x=e("code"),x.textContent="OPERATOR",$=_(` - is one of:
`),h=e("br"),w=s(),c=e("ul"),V=e("li"),bt=e("code"),bt.textContent="=",Gt=s(),R=e("span"),R.textContent="Equal",zt=s(),q=e("li"),it=e("code"),it.textContent="!=",F=s(),W=e("span"),W.textContent="NOT equal",ee=s(),I=e("li"),G=e("code"),G.textContent=">",le=s(),at=e("span"),at.textContent="Greater than",ht=s(),X=e("li"),xt=e("code"),xt.textContent=">=",se=s(),rt=e("span"),rt.textContent="Greater than or equal",ct=s(),Y=e("li"),O=e("code"),O.textContent="<",Ut=s(),wt=e("span"),wt.textContent="Less than",y=s(),Z=e("li"),_t=e("code"),_t.textContent="<=",jt=s(),$t=e("span"),$t.textContent="Less than or equal",z=s(),tt=e("li"),Ct=e("code"),Ct.textContent="~",Qt=s(),kt=e("span"),kt.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
`),h=e("br"),w=s(),c=e("ul"),V=e("li"),bt=e("code"),bt.textContent="=",Gt=s(),R=e("span"),R.textContent="Equal",Ut=s(),q=e("li"),it=e("code"),it.textContent="!=",F=s(),W=e("span"),W.textContent="NOT equal",ee=s(),I=e("li"),G=e("code"),G.textContent=">",le=s(),at=e("span"),at.textContent="Greater than",ht=s(),X=e("li"),xt=e("code"),xt.textContent=">=",se=s(),rt=e("span"),rt.textContent="Greater than or equal",ct=s(),Y=e("li"),O=e("code"),O.textContent="<",jt=s(),wt=e("span"),wt.textContent="Less than",y=s(),Z=e("li"),_t=e("code"),_t.textContent="<=",Qt=s(),$t=e("span"),$t.textContent="Less than or equal",U=s(),tt=e("li"),Ct=e("code"),Ct.textContent="~",zt=s(),kt=e("span"),kt.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
wildcard match)`,L=s(),dt=e("li"),gt=e("code"),gt.textContent="!~",ne=s(),ft=e("span"),ft.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
wildcard match)`,oe=s(),D=e("li"),vt=e("code"),vt.textContent="?=",et=s(),yt=e("em"),yt.textContent="Any/At least one of",U=s(),pt=e("span"),pt.textContent="Equal",ie=s(),H=e("li"),Ft=e("code"),Ft.textContent="?!=",lt=s(),Lt=e("em"),Lt.textContent="Any/At least one of",st=s(),At=e("span"),At.textContent="NOT equal",nt=s(),j=e("li"),E=e("code"),E.textContent="?>",Jt=s(),Tt=e("em"),Tt.textContent="Any/At least one of",Kt=s(),Pt=e("span"),Pt.textContent="Greater than",C=s(),Q=e("li"),M=e("code"),M.textContent="?>=",ae=s(),Rt=e("em"),Rt.textContent="Any/At least one of",re=s(),ut=e("span"),ut.textContent="Greater than or equal",ce=s(),B=e("li"),Ot=e("code"),Ot.textContent="?<",de=s(),Et=e("em"),Et.textContent="Any/At least one of",Vt=s(),St=e("span"),St.textContent="Less than",Wt=s(),A=e("li"),mt=e("code"),mt.textContent="?<=",J=s(),K=e("em"),K.textContent="Any/At least one of",S=s(),Nt=e("span"),Nt.textContent="Less than or equal",fe=s(),T=e("li"),k=e("code"),k.textContent="?~",pe=s(),N=e("em"),N.textContent="Any/At least one of",v=s(),ot=e("span"),ot.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
wildcard match)`,oe=s(),D=e("li"),vt=e("code"),vt.textContent="?=",et=s(),yt=e("em"),yt.textContent="Any/At least one of",j=s(),pt=e("span"),pt.textContent="Equal",ie=s(),H=e("li"),Ft=e("code"),Ft.textContent="?!=",lt=s(),Lt=e("em"),Lt.textContent="Any/At least one of",st=s(),At=e("span"),At.textContent="NOT equal",nt=s(),Q=e("li"),E=e("code"),E.textContent="?>",Jt=s(),Tt=e("em"),Tt.textContent="Any/At least one of",Kt=s(),Pt=e("span"),Pt.textContent="Greater than",C=s(),z=e("li"),M=e("code"),M.textContent="?>=",ae=s(),Rt=e("em"),Rt.textContent="Any/At least one of",re=s(),ut=e("span"),ut.textContent="Greater than or equal",ce=s(),B=e("li"),Ot=e("code"),Ot.textContent="?<",de=s(),Et=e("em"),Et.textContent="Any/At least one of",Vt=s(),St=e("span"),St.textContent="Less than",Wt=s(),A=e("li"),mt=e("code"),mt.textContent="?<=",J=s(),K=e("em"),K.textContent="Any/At least one of",S=s(),Nt=e("span"),Nt.textContent="Less than or equal",fe=s(),T=e("li"),k=e("code"),k.textContent="?~",pe=s(),N=e("em"),N.textContent="Any/At least one of",v=s(),ot=e("span"),ot.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
wildcard match)`,ue=s(),P=e("li"),qt=e("code"),qt.textContent="?!~",me=s(),Dt=e("em"),Dt.textContent="Any/At least one of",be=s(),Ht=e("span"),Ht.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
wildcard match)`,Xt=s(),Mt=e("p"),Mt.innerHTML=`To group and combine several expressions you could use brackets
<code>(...)</code>, <code>&amp;&amp;</code> (AND) and <code>||</code> (OR) tokens.`,o(x,"class","txt-danger"),o(bt,"class","filter-op svelte-1w7s5nw"),o(R,"class","txt"),o(it,"class","filter-op svelte-1w7s5nw"),o(W,"class","txt"),o(G,"class","filter-op svelte-1w7s5nw"),o(at,"class","txt"),o(xt,"class","filter-op svelte-1w7s5nw"),o(rt,"class","txt"),o(O,"class","filter-op svelte-1w7s5nw"),o(wt,"class","txt"),o(_t,"class","filter-op svelte-1w7s5nw"),o($t,"class","txt"),o(Ct,"class","filter-op svelte-1w7s5nw"),o(kt,"class","txt"),o(gt,"class","filter-op svelte-1w7s5nw"),o(ft,"class","txt"),o(vt,"class","filter-op svelte-1w7s5nw"),o(yt,"class","txt-hint"),o(pt,"class","txt"),o(Ft,"class","filter-op svelte-1w7s5nw"),o(Lt,"class","txt-hint"),o(At,"class","txt"),o(E,"class","filter-op svelte-1w7s5nw"),o(Tt,"class","txt-hint"),o(Pt,"class","txt"),o(M,"class","filter-op svelte-1w7s5nw"),o(Rt,"class","txt-hint"),o(ut,"class","txt"),o(Ot,"class","filter-op svelte-1w7s5nw"),o(Et,"class","txt-hint"),o(St,"class","txt"),o(mt,"class","filter-op svelte-1w7s5nw"),o(K,"class","txt-hint"),o(Nt,"class","txt"),o(k,"class","filter-op svelte-1w7s5nw"),o(N,"class","txt-hint"),o(ot,"class","txt"),o(qt,"class","filter-op svelte-1w7s5nw"),o(Dt,"class","txt-hint"),o(Ht,"class","txt")},m(l,i){u(l,n,i),u(l,a,i),u(l,r,i),t(r,f),t(r,p),t(r,b),t(b,x),t(b,$),t(b,h),t(b,w),t(b,c),t(c,V),t(V,bt),t(V,Gt),t(V,R),t(c,zt),t(c,q),t(q,it),t(q,F),t(q,W),t(c,ee),t(c,I),t(I,G),t(I,le),t(I,at),t(c,ht),t(c,X),t(X,xt),t(X,se),t(X,rt),t(c,ct),t(c,Y),t(Y,O),t(Y,Ut),t(Y,wt),t(c,y),t(c,Z),t(Z,_t),t(Z,jt),t(Z,$t),t(c,z),t(c,tt),t(tt,Ct),t(tt,Qt),t(tt,kt),t(c,L),t(c,dt),t(dt,gt),t(dt,ne),t(dt,ft),t(c,oe),t(c,D),t(D,vt),t(D,et),t(D,yt),t(D,U),t(D,pt),t(c,ie),t(c,H),t(H,Ft),t(H,lt),t(H,Lt),t(H,st),t(H,At),t(c,nt),t(c,j),t(j,E),t(j,Jt),t(j,Tt),t(j,Kt),t(j,Pt),t(c,C),t(c,Q),t(Q,M),t(Q,ae),t(Q,Rt),t(Q,re),t(Q,ut),t(c,ce),t(c,B),t(B,Ot),t(B,de),t(B,Et),t(B,Vt),t(B,St),t(c,Wt),t(c,A),t(A,mt),t(A,J),t(A,K),t(A,S),t(A,Nt),t(c,fe),t(c,T),t(T,k),t(T,pe),t(T,N),t(T,v),t(T,ot),t(c,ue),t(c,P),t(P,qt),t(P,me),t(P,Dt),t(P,be),t(P,Ht),u(l,Xt,i),u(l,Mt,i)},d(l){l&&m(n),l&&m(a),l&&m(r),l&&m(Xt),l&&m(Mt)}}}function Ke(d){let n,a,r,f,p;function b(w,c){return w[0]?Je:Qe}let x=b(d),$=x(d),h=d[0]&&Ae();return{c(){n=e("button"),$.c(),a=s(),h&&h.c(),r=He(),o(n,"class","btn btn-sm btn-transparent m-t-5")},m(w,c){u(w,n,c),$.m(n,null),u(w,a,c),h&&h.m(w,c),u(w,r,c),f||(p=De(n,"click",d[1]),f=!0)},p(w,[c]){x!==(x=b(w))&&($.d(1),$=x(w),$&&($.c(),$.m(n,null))),w[0]?h||(h=Ae(),h.c(),h.m(r.parentNode,r)):h&&(h.d(1),h=null)},i:Fe,o:Fe,d(w){w&&m(n),$.d(),w&&m(a),h&&h.d(w),w&&m(r),f=!1,p()}}}function Ve(d,n,a){let r=!1;function f(){a(0,r=!r)}return[r,f]}class We extends Se{constructor(n){super(),Ne(this,n,Ve,Ke,qe,{})}}function Te(d,n,a){const r=d.slice();return r[6]=n[a],r}function Pe(d,n,a){const r=d.slice();return r[6]=n[a],r}function Re(d){let n;return{c(){n=e("p"),n.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",o(n,"class","txt-hint txt-sm txt-right")},m(a,r){u(a,n,r)},d(a){a&&m(n)}}}function Oe(d,n){let a,r=n[6].code+"",f,p,b,x;function $(){return n[5](n[6])}return{key:d,first:null,c(){a=e("div"),f=_(r),p=s(),o(a,"class","tab-item"),xe(a,"active",n[2]===n[6].code),this.first=a},m(h,w){u(h,a,w),t(a,f),t(a,p),b||(x=De(a,"click",$),b=!0)},p(h,w){n=h,w&20&&xe(a,"active",n[2]===n[6].code)},d(h){h&&m(a),b=!1,x()}}}function Ee(d,n){let a,r,f,p;return r=new he({props:{content:n[6].body}}),{key:d,first:null,c(){a=e("div"),Yt(r.$$.fragment),f=s(),o(a,"class","tab-item"),xe(a,"active",n[2]===n[6].code),this.first=a},m(b,x){u(b,a,x),Zt(r,a,null),t(a,f),p=!0},p(b,x){n=b,(!p||x&20)&&xe(a,"active",n[2]===n[6].code)},i(b){p||(Bt(r.$$.fragment,b),p=!0)},o(b){It(r.$$.fragment,b),p=!1},d(b){b&&m(a),te(r)}}}function Xe(d){var me,Dt,be,Ht,Xt,Mt;let n,a,r=d[0].name+"",f,p,b,x,$,h,w,c=d[0].name+"",V,bt,Gt,R,zt,q,it,F,W,ee,I,G,le,at,ht=d[0].name+"",X,xt,se,rt,ct,Y,O,Ut,wt,y,Z,_t,jt,$t,z,tt,Ct,Qt,kt,L,dt,gt,ne,ft,oe,D,vt,et,yt,U,pt,ie,H,Ft,lt,Lt,st,At,nt,j,E,Jt,Tt,Kt,Pt,C,Q,M,ae,Rt,re,ut,ce,B,Ot,de,Et,Vt,St,Wt,A,mt,J,K,S=[],Nt=new Map,fe,T,k=[],pe=new Map,N;R=new je({props:{js:`
<code>(...)</code>, <code>&amp;&amp;</code> (AND) and <code>||</code> (OR) tokens.`,o(x,"class","txt-danger"),o(bt,"class","filter-op svelte-1w7s5nw"),o(R,"class","txt"),o(it,"class","filter-op svelte-1w7s5nw"),o(W,"class","txt"),o(G,"class","filter-op svelte-1w7s5nw"),o(at,"class","txt"),o(xt,"class","filter-op svelte-1w7s5nw"),o(rt,"class","txt"),o(O,"class","filter-op svelte-1w7s5nw"),o(wt,"class","txt"),o(_t,"class","filter-op svelte-1w7s5nw"),o($t,"class","txt"),o(Ct,"class","filter-op svelte-1w7s5nw"),o(kt,"class","txt"),o(gt,"class","filter-op svelte-1w7s5nw"),o(ft,"class","txt"),o(vt,"class","filter-op svelte-1w7s5nw"),o(yt,"class","txt-hint"),o(pt,"class","txt"),o(Ft,"class","filter-op svelte-1w7s5nw"),o(Lt,"class","txt-hint"),o(At,"class","txt"),o(E,"class","filter-op svelte-1w7s5nw"),o(Tt,"class","txt-hint"),o(Pt,"class","txt"),o(M,"class","filter-op svelte-1w7s5nw"),o(Rt,"class","txt-hint"),o(ut,"class","txt"),o(Ot,"class","filter-op svelte-1w7s5nw"),o(Et,"class","txt-hint"),o(St,"class","txt"),o(mt,"class","filter-op svelte-1w7s5nw"),o(K,"class","txt-hint"),o(Nt,"class","txt"),o(k,"class","filter-op svelte-1w7s5nw"),o(N,"class","txt-hint"),o(ot,"class","txt"),o(qt,"class","filter-op svelte-1w7s5nw"),o(Dt,"class","txt-hint"),o(Ht,"class","txt")},m(l,i){u(l,n,i),u(l,a,i),u(l,r,i),t(r,f),t(r,p),t(r,b),t(b,x),t(b,$),t(b,h),t(b,w),t(b,c),t(c,V),t(V,bt),t(V,Gt),t(V,R),t(c,Ut),t(c,q),t(q,it),t(q,F),t(q,W),t(c,ee),t(c,I),t(I,G),t(I,le),t(I,at),t(c,ht),t(c,X),t(X,xt),t(X,se),t(X,rt),t(c,ct),t(c,Y),t(Y,O),t(Y,jt),t(Y,wt),t(c,y),t(c,Z),t(Z,_t),t(Z,Qt),t(Z,$t),t(c,U),t(c,tt),t(tt,Ct),t(tt,zt),t(tt,kt),t(c,L),t(c,dt),t(dt,gt),t(dt,ne),t(dt,ft),t(c,oe),t(c,D),t(D,vt),t(D,et),t(D,yt),t(D,j),t(D,pt),t(c,ie),t(c,H),t(H,Ft),t(H,lt),t(H,Lt),t(H,st),t(H,At),t(c,nt),t(c,Q),t(Q,E),t(Q,Jt),t(Q,Tt),t(Q,Kt),t(Q,Pt),t(c,C),t(c,z),t(z,M),t(z,ae),t(z,Rt),t(z,re),t(z,ut),t(c,ce),t(c,B),t(B,Ot),t(B,de),t(B,Et),t(B,Vt),t(B,St),t(c,Wt),t(c,A),t(A,mt),t(A,J),t(A,K),t(A,S),t(A,Nt),t(c,fe),t(c,T),t(T,k),t(T,pe),t(T,N),t(T,v),t(T,ot),t(c,ue),t(c,P),t(P,qt),t(P,me),t(P,Dt),t(P,be),t(P,Ht),u(l,Xt,i),u(l,Mt,i)},d(l){l&&m(n),l&&m(a),l&&m(r),l&&m(Xt),l&&m(Mt)}}}function Ke(d){let n,a,r,f,p;function b(w,c){return w[0]?Je:ze}let x=b(d),$=x(d),h=d[0]&&Ae();return{c(){n=e("button"),$.c(),a=s(),h&&h.c(),r=He(),o(n,"class","btn btn-sm btn-transparent m-t-5")},m(w,c){u(w,n,c),$.m(n,null),u(w,a,c),h&&h.m(w,c),u(w,r,c),f||(p=De(n,"click",d[1]),f=!0)},p(w,[c]){x!==(x=b(w))&&($.d(1),$=x(w),$&&($.c(),$.m(n,null))),w[0]?h||(h=Ae(),h.c(),h.m(r.parentNode,r)):h&&(h.d(1),h=null)},i:Fe,o:Fe,d(w){w&&m(n),$.d(),w&&m(a),h&&h.d(w),w&&m(r),f=!1,p()}}}function Ve(d,n,a){let r=!1;function f(){a(0,r=!r)}return[r,f]}class We extends Se{constructor(n){super(),Ne(this,n,Ve,Ke,qe,{})}}function Te(d,n,a){const r=d.slice();return r[6]=n[a],r}function Pe(d,n,a){const r=d.slice();return r[6]=n[a],r}function Re(d){let n;return{c(){n=e("p"),n.innerHTML="Requires admin <code>Authorization:TOKEN</code> header",o(n,"class","txt-hint txt-sm txt-right")},m(a,r){u(a,n,r)},d(a){a&&m(n)}}}function Oe(d,n){let a,r=n[6].code+"",f,p,b,x;function $(){return n[5](n[6])}return{key:d,first:null,c(){a=e("div"),f=_(r),p=s(),o(a,"class","tab-item"),xe(a,"active",n[2]===n[6].code),this.first=a},m(h,w){u(h,a,w),t(a,f),t(a,p),b||(x=De(a,"click",$),b=!0)},p(h,w){n=h,w&20&&xe(a,"active",n[2]===n[6].code)},d(h){h&&m(a),b=!1,x()}}}function Ee(d,n){let a,r,f,p;return r=new he({props:{content:n[6].body}}),{key:d,first:null,c(){a=e("div"),Yt(r.$$.fragment),f=s(),o(a,"class","tab-item"),xe(a,"active",n[2]===n[6].code),this.first=a},m(b,x){u(b,a,x),Zt(r,a,null),t(a,f),p=!0},p(b,x){n=b,(!p||x&20)&&xe(a,"active",n[2]===n[6].code)},i(b){p||(Bt(r.$$.fragment,b),p=!0)},o(b){It(r.$$.fragment,b),p=!1},d(b){b&&m(a),te(r)}}}function Xe(d){var me,Dt,be,Ht,Xt,Mt;let n,a,r=d[0].name+"",f,p,b,x,$,h,w,c=d[0].name+"",V,bt,Gt,R,Ut,q,it,F,W,ee,I,G,le,at,ht=d[0].name+"",X,xt,se,rt,ct,Y,O,jt,wt,y,Z,_t,Qt,$t,U,tt,Ct,zt,kt,L,dt,gt,ne,ft,oe,D,vt,et,yt,j,pt,ie,H,Ft,lt,Lt,st,At,nt,Q,E,Jt,Tt,Kt,Pt,C,z,M,ae,Rt,re,ut,ce,B,Ot,de,Et,Vt,St,Wt,A,mt,J,K,S=[],Nt=new Map,fe,T,k=[],pe=new Map,N;R=new Qe({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${d[3]}');
@@ -21,7 +21,7 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
});
// you can also fetch all records at once via getFullList
const records = await pb.collection('${(Dt=d[0])==null?void 0:Dt.name}').getFullList(200 /* batch size */, {
const records = await pb.collection('${(Dt=d[0])==null?void 0:Dt.name}').getFullList({
sort: '-created',
});
@@ -45,7 +45,6 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
// you can also fetch all records at once via getFullList
final records = await pb.collection('${(Xt=d[0])==null?void 0:Xt.name}').getFullList(
batch: 200,
sort: '-created',
);
@@ -59,23 +58,23 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
?sort=-created,id
`}}),st=new he({props:{content:`
?filter=(id='abc' && created>'2022-01-01')
`}}),nt=new We({}),M=new he({props:{content:"?expand=relField1,relField2.subRelField"}});let ot=d[4];const ue=l=>l[6].code;for(let l=0;l<ot.length;l+=1){let i=Pe(d,ot,l),g=ue(i);Nt.set(g,S[l]=Oe(g,i))}let P=d[4];const qt=l=>l[6].code;for(let l=0;l<P.length;l+=1){let i=Te(d,P,l),g=qt(i);pe.set(g,k[l]=Ee(g,i))}return{c(){n=e("h3"),a=_("List/Search ("),f=_(r),p=_(")"),b=s(),x=e("div"),$=e("p"),h=_("Fetch a paginated "),w=e("strong"),V=_(c),bt=_(" records list, supporting sorting and filtering."),Gt=s(),Yt(R.$$.fragment),zt=s(),q=e("h6"),q.textContent="API details",it=s(),F=e("div"),W=e("strong"),W.textContent="GET",ee=s(),I=e("div"),G=e("p"),le=_("/api/collections/"),at=e("strong"),X=_(ht),xt=_("/records"),se=s(),v&&v.c(),rt=s(),ct=e("div"),ct.textContent="Query parameters",Y=s(),O=e("table"),Ut=e("thead"),Ut.innerHTML=`<tr><th>Param</th>
`}}),nt=new We({}),M=new he({props:{content:"?expand=relField1,relField2.subRelField"}});let ot=d[4];const ue=l=>l[6].code;for(let l=0;l<ot.length;l+=1){let i=Pe(d,ot,l),g=ue(i);Nt.set(g,S[l]=Oe(g,i))}let P=d[4];const qt=l=>l[6].code;for(let l=0;l<P.length;l+=1){let i=Te(d,P,l),g=qt(i);pe.set(g,k[l]=Ee(g,i))}return{c(){n=e("h3"),a=_("List/Search ("),f=_(r),p=_(")"),b=s(),x=e("div"),$=e("p"),h=_("Fetch a paginated "),w=e("strong"),V=_(c),bt=_(" records list, supporting sorting and filtering."),Gt=s(),Yt(R.$$.fragment),Ut=s(),q=e("h6"),q.textContent="API details",it=s(),F=e("div"),W=e("strong"),W.textContent="GET",ee=s(),I=e("div"),G=e("p"),le=_("/api/collections/"),at=e("strong"),X=_(ht),xt=_("/records"),se=s(),v&&v.c(),rt=s(),ct=e("div"),ct.textContent="Query parameters",Y=s(),O=e("table"),jt=e("thead"),jt.innerHTML=`<tr><th>Param</th>
<th>Type</th>
<th width="60%">Description</th></tr>`,wt=s(),y=e("tbody"),Z=e("tr"),Z.innerHTML=`<td>page</td>
<td><span class="label">Number</span></td>
<td>The page (aka. offset) of the paginated list (default to 1).</td>`,_t=s(),jt=e("tr"),jt.innerHTML=`<td>perPage</td>
<td>The page (aka. offset) of the paginated list (default to 1).</td>`,_t=s(),Qt=e("tr"),Qt.innerHTML=`<td>perPage</td>
<td><span class="label">Number</span></td>
<td>Specify the max returned records per page (default to 30).</td>`,$t=s(),z=e("tr"),tt=e("td"),tt.textContent="sort",Ct=s(),Qt=e("td"),Qt.innerHTML='<span class="label">String</span>',kt=s(),L=e("td"),dt=_("Specify the records order attribute(s). "),gt=e("br"),ne=_(`
<td>Specify the max returned records per page (default to 30).</td>`,$t=s(),U=e("tr"),tt=e("td"),tt.textContent="sort",Ct=s(),zt=e("td"),zt.innerHTML='<span class="label">String</span>',kt=s(),L=e("td"),dt=_("Specify the records order attribute(s). "),gt=e("br"),ne=_(`
Add `),ft=e("code"),ft.textContent="-",oe=_(" / "),D=e("code"),D.textContent="+",vt=_(` (default) in front of the attribute for DESC / ASC order.
Ex.:
`),Yt(et.$$.fragment),yt=s(),U=e("tr"),pt=e("td"),pt.textContent="filter",ie=s(),H=e("td"),H.innerHTML='<span class="label">String</span>',Ft=s(),lt=e("td"),Lt=_(`Filter the returned records. Ex.:
`),Yt(st.$$.fragment),At=s(),Yt(nt.$$.fragment),j=s(),E=e("tr"),Jt=e("td"),Jt.textContent="expand",Tt=s(),Kt=e("td"),Kt.innerHTML='<span class="label">String</span>',Pt=s(),C=e("td"),Q=_(`Auto expand record relations. Ex.:
`),Yt(et.$$.fragment),yt=s(),j=e("tr"),pt=e("td"),pt.textContent="filter",ie=s(),H=e("td"),H.innerHTML='<span class="label">String</span>',Ft=s(),lt=e("td"),Lt=_(`Filter the returned records. Ex.:
`),Yt(st.$$.fragment),At=s(),Yt(nt.$$.fragment),Q=s(),E=e("tr"),Jt=e("td"),Jt.textContent="expand",Tt=s(),Kt=e("td"),Kt.innerHTML='<span class="label">String</span>',Pt=s(),C=e("td"),z=_(`Auto expand record relations. Ex.:
`),Yt(M.$$.fragment),ae=_(`
Supports up to 6-levels depth nested relations expansion. `),Rt=e("br"),re=_(`
The expanded relations will be appended to each individual record under the
`),ut=e("code"),ut.textContent="expand",ce=_(" property (eg. "),B=e("code"),B.textContent='"expand": {"relField1": {...}, ...}',Ot=_(`).
`),de=e("br"),Et=_(`
Only the relations to which the request user has permissions to `),Vt=e("strong"),Vt.textContent="view",St=_(" will be expanded."),Wt=s(),A=e("div"),A.textContent="Responses",mt=s(),J=e("div"),K=e("div");for(let l=0;l<S.length;l+=1)S[l].c();fe=s(),T=e("div");for(let l=0;l<k.length;l+=1)k[l].c();o(n,"class","m-b-sm"),o(x,"class","content txt-lg m-b-sm"),o(q,"class","m-b-xs"),o(W,"class","label label-primary"),o(I,"class","content"),o(F,"class","alert alert-info"),o(ct,"class","section-title"),o(O,"class","table-compact table-border m-b-base"),o(A,"class","section-title"),o(K,"class","tabs-header compact left"),o(T,"class","tabs-content"),o(J,"class","tabs")},m(l,i){u(l,n,i),t(n,a),t(n,f),t(n,p),u(l,b,i),u(l,x,i),t(x,$),t($,h),t($,w),t(w,V),t($,bt),u(l,Gt,i),Zt(R,l,i),u(l,zt,i),u(l,q,i),u(l,it,i),u(l,F,i),t(F,W),t(F,ee),t(F,I),t(I,G),t(G,le),t(G,at),t(at,X),t(G,xt),t(F,se),v&&v.m(F,null),u(l,rt,i),u(l,ct,i),u(l,Y,i),u(l,O,i),t(O,Ut),t(O,wt),t(O,y),t(y,Z),t(y,_t),t(y,jt),t(y,$t),t(y,z),t(z,tt),t(z,Ct),t(z,Qt),t(z,kt),t(z,L),t(L,dt),t(L,gt),t(L,ne),t(L,ft),t(L,oe),t(L,D),t(L,vt),Zt(et,L,null),t(y,yt),t(y,U),t(U,pt),t(U,ie),t(U,H),t(U,Ft),t(U,lt),t(lt,Lt),Zt(st,lt,null),t(lt,At),Zt(nt,lt,null),t(y,j),t(y,E),t(E,Jt),t(E,Tt),t(E,Kt),t(E,Pt),t(E,C),t(C,Q),Zt(M,C,null),t(C,ae),t(C,Rt),t(C,re),t(C,ut),t(C,ce),t(C,B),t(C,Ot),t(C,de),t(C,Et),t(C,Vt),t(C,St),u(l,Wt,i),u(l,A,i),u(l,mt,i),u(l,J,i),t(J,K);for(let g=0;g<S.length;g+=1)S[g].m(K,null);t(J,fe),t(J,T);for(let g=0;g<k.length;g+=1)k[g].m(T,null);N=!0},p(l,[i]){var $e,Ce,ke,ge,ve,ye;(!N||i&1)&&r!==(r=l[0].name+"")&&we(f,r),(!N||i&1)&&c!==(c=l[0].name+"")&&we(V,c);const g={};i&9&&(g.js=`
Only the relations to which the request user has permissions to `),Vt=e("strong"),Vt.textContent="view",St=_(" will be expanded."),Wt=s(),A=e("div"),A.textContent="Responses",mt=s(),J=e("div"),K=e("div");for(let l=0;l<S.length;l+=1)S[l].c();fe=s(),T=e("div");for(let l=0;l<k.length;l+=1)k[l].c();o(n,"class","m-b-sm"),o(x,"class","content txt-lg m-b-sm"),o(q,"class","m-b-xs"),o(W,"class","label label-primary"),o(I,"class","content"),o(F,"class","alert alert-info"),o(ct,"class","section-title"),o(O,"class","table-compact table-border m-b-base"),o(A,"class","section-title"),o(K,"class","tabs-header compact left"),o(T,"class","tabs-content"),o(J,"class","tabs")},m(l,i){u(l,n,i),t(n,a),t(n,f),t(n,p),u(l,b,i),u(l,x,i),t(x,$),t($,h),t($,w),t(w,V),t($,bt),u(l,Gt,i),Zt(R,l,i),u(l,Ut,i),u(l,q,i),u(l,it,i),u(l,F,i),t(F,W),t(F,ee),t(F,I),t(I,G),t(G,le),t(G,at),t(at,X),t(G,xt),t(F,se),v&&v.m(F,null),u(l,rt,i),u(l,ct,i),u(l,Y,i),u(l,O,i),t(O,jt),t(O,wt),t(O,y),t(y,Z),t(y,_t),t(y,Qt),t(y,$t),t(y,U),t(U,tt),t(U,Ct),t(U,zt),t(U,kt),t(U,L),t(L,dt),t(L,gt),t(L,ne),t(L,ft),t(L,oe),t(L,D),t(L,vt),Zt(et,L,null),t(y,yt),t(y,j),t(j,pt),t(j,ie),t(j,H),t(j,Ft),t(j,lt),t(lt,Lt),Zt(st,lt,null),t(lt,At),Zt(nt,lt,null),t(y,Q),t(y,E),t(E,Jt),t(E,Tt),t(E,Kt),t(E,Pt),t(E,C),t(C,z),Zt(M,C,null),t(C,ae),t(C,Rt),t(C,re),t(C,ut),t(C,ce),t(C,B),t(C,Ot),t(C,de),t(C,Et),t(C,Vt),t(C,St),u(l,Wt,i),u(l,A,i),u(l,mt,i),u(l,J,i),t(J,K);for(let g=0;g<S.length;g+=1)S[g].m(K,null);t(J,fe),t(J,T);for(let g=0;g<k.length;g+=1)k[g].m(T,null);N=!0},p(l,[i]){var $e,Ce,ke,ge,ve,ye;(!N||i&1)&&r!==(r=l[0].name+"")&&we(f,r),(!N||i&1)&&c!==(c=l[0].name+"")&&we(V,c);const g={};i&9&&(g.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${l[3]}');
@@ -88,7 +87,7 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
});
// you can also fetch all records at once via getFullList
const records = await pb.collection('${(Ce=l[0])==null?void 0:Ce.name}').getFullList(200 /* batch size */, {
const records = await pb.collection('${(Ce=l[0])==null?void 0:Ce.name}').getFullList({
sort: '-created',
});
@@ -112,7 +111,6 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
// you can also fetch all records at once via getFullList
final records = await pb.collection('${(ve=l[0])==null?void 0:ve.name}').getFullList(
batch: 200,
sort: '-created',
);
@@ -121,7 +119,7 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
'someField="test"',
expand: 'relField1,relField2.subRelField',
);
`),R.$set(g),(!N||i&1)&&ht!==(ht=l[0].name+"")&&we(X,ht),l[1]?v||(v=Re(),v.c(),v.m(F,null)):v&&(v.d(1),v=null),i&20&&(ot=l[4],S=Le(S,i,ue,1,l,ot,Nt,K,Me,Oe,null,Pe)),i&20&&(P=l[4],Be(),k=Le(k,i,qt,1,l,P,pe,T,Ie,Ee,null,Te),Ge())},i(l){if(!N){Bt(R.$$.fragment,l),Bt(et.$$.fragment,l),Bt(st.$$.fragment,l),Bt(nt.$$.fragment,l),Bt(M.$$.fragment,l);for(let i=0;i<P.length;i+=1)Bt(k[i]);N=!0}},o(l){It(R.$$.fragment,l),It(et.$$.fragment,l),It(st.$$.fragment,l),It(nt.$$.fragment,l),It(M.$$.fragment,l);for(let i=0;i<k.length;i+=1)It(k[i]);N=!1},d(l){l&&m(n),l&&m(b),l&&m(x),l&&m(Gt),te(R,l),l&&m(zt),l&&m(q),l&&m(it),l&&m(F),v&&v.d(),l&&m(rt),l&&m(ct),l&&m(Y),l&&m(O),te(et),te(st),te(nt),te(M),l&&m(Wt),l&&m(A),l&&m(mt),l&&m(J);for(let i=0;i<S.length;i+=1)S[i].d();for(let i=0;i<k.length;i+=1)k[i].d()}}}function Ye(d,n,a){let r,f,{collection:p=new ze}=n,b=200,x=[];const $=h=>a(2,b=h.code);return d.$$set=h=>{"collection"in h&&a(0,p=h.collection)},d.$$.update=()=>{d.$$.dirty&1&&a(1,r=(p==null?void 0:p.listRule)===null),d.$$.dirty&3&&p!=null&&p.id&&(x.push({code:200,body:JSON.stringify({page:1,perPage:30,totalPages:1,totalItems:2,items:[_e.dummyCollectionRecord(p),_e.dummyCollectionRecord(p)]},null,2)}),x.push({code:400,body:`
`),R.$set(g),(!N||i&1)&&ht!==(ht=l[0].name+"")&&we(X,ht),l[1]?v||(v=Re(),v.c(),v.m(F,null)):v&&(v.d(1),v=null),i&20&&(ot=l[4],S=Le(S,i,ue,1,l,ot,Nt,K,Me,Oe,null,Pe)),i&20&&(P=l[4],Be(),k=Le(k,i,qt,1,l,P,pe,T,Ie,Ee,null,Te),Ge())},i(l){if(!N){Bt(R.$$.fragment,l),Bt(et.$$.fragment,l),Bt(st.$$.fragment,l),Bt(nt.$$.fragment,l),Bt(M.$$.fragment,l);for(let i=0;i<P.length;i+=1)Bt(k[i]);N=!0}},o(l){It(R.$$.fragment,l),It(et.$$.fragment,l),It(st.$$.fragment,l),It(nt.$$.fragment,l),It(M.$$.fragment,l);for(let i=0;i<k.length;i+=1)It(k[i]);N=!1},d(l){l&&m(n),l&&m(b),l&&m(x),l&&m(Gt),te(R,l),l&&m(Ut),l&&m(q),l&&m(it),l&&m(F),v&&v.d(),l&&m(rt),l&&m(ct),l&&m(Y),l&&m(O),te(et),te(st),te(nt),te(M),l&&m(Wt),l&&m(A),l&&m(mt),l&&m(J);for(let i=0;i<S.length;i+=1)S[i].d();for(let i=0;i<k.length;i+=1)k[i].d()}}}function Ye(d,n,a){let r,f,{collection:p=new Ue}=n,b=200,x=[];const $=h=>a(2,b=h.code);return d.$$set=h=>{"collection"in h&&a(0,p=h.collection)},d.$$.update=()=>{d.$$.dirty&1&&a(1,r=(p==null?void 0:p.listRule)===null),d.$$.dirty&3&&p!=null&&p.id&&(x.push({code:200,body:JSON.stringify({page:1,perPage:30,totalPages:1,totalItems:2,items:[_e.dummyCollectionRecord(p),_e.dummyCollectionRecord(p)]},null,2)}),x.push({code:400,body:`
{
"code": 400,
"message": "Something went wrong while processing your request. Invalid filter.",
@@ -133,4 +131,4 @@ import{S as Se,i as Ne,s as qe,e,b as s,E as He,f as o,g as u,u as De,y as Fe,o
"message": "Only admins can access this action.",
"data": {}
}
`}))},a(3,f=_e.getApiExampleUrl(Ue.baseUrl)),[p,r,b,f,x,$]}class el extends Se{constructor(n){super(),Ne(this,n,Ye,Xe,qe,{collection:0})}}export{el as default};
`}))},a(3,f=_e.getApiExampleUrl(je.baseUrl)),[p,r,b,f,x,$]}class el extends Se{constructor(n){super(),Ne(this,n,Ye,Xe,qe,{collection:0})}}export{el as default};
@@ -1,4 +1,4 @@
import{S as Be,i as qe,s as Oe,e as i,w as v,b as _,c as Se,f as b,g as r,h as s,m as Ee,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 Ie,R as ze,C as De,p as He,r as j,u as Ue,N as je}from"./index-3e0f12d8.js";import{S as Ne}from"./SdkTabs-ed893501.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"),Se(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),Ee(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),Ie(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,S,F,w,R,ae,W,A,ne,J,z=a[0].name+"",V,ie,X,ce,re,D,Y,E,Z,I,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 Se,f as b,g as r,h as s,m as Ee,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 Ie,R as ze,C as De,p as He,r as j,u as Ue,N as je}from"./index-4bd199fb.js";import{S as Ne}from"./SdkTabs-fc1a80c5.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"),Se(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),Ee(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),Ie(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,S,F,w,R,ae,W,A,ne,J,z=a[0].name+"",V,ie,X,ce,re,D,Y,E,Z,I,x,B,ee,C,q,$=[],de=new Map,ue,O,k=[],pe=new Map,T;y=new Ne({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}');
@@ -1,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-3e0f12d8.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-4bd199fb.js";function y(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&Z(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=C(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=h(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=C(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=h(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,P,v,k,F,j,m=f[3]&&y(f);return r=new J({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new J({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password
`),m&&m.c(),t=C(),A(r.$$.fragment),p=C(),A(d.$$.fragment),n=C(),i=c("button"),g=c("span"),g.textContent="Set new password",R=C(),P=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],L(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(P,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),B(r,e,null),_(e,p),B(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,P,$),_(P,v),k=!0,F||(j=[h(e,"submit",O(f[4])),Q(U.call(null,v))],F=!0)},p(a,$){a[3]?m?m.p(a,$):(m=y(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const z={};$&769&&(z.$$scope={dirty:$,ctx:a}),r.$set(z);const D={};$&770&&(D.$$scope={dirty:$,ctx:a}),d.$set(D),(!k||$&4)&&(i.disabled=a[2]),(!k||$&4)&&L(i,"btn-loading",a[2])},i(a){k||(H(r.$$.fragment,a),H(d.$$.fragment,a),k=!0)},o(a){N(r.$$.fragment,a),N(d.$$.fragment,a),k=!1},d(a){a&&w(e),m&&m.d(),T(r),T(d),a&&w(R),a&&w(P),F=!1,V(j)}}}function se(f){let e,o;return e=new K({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){A(e.$$.fragment)},m(s,l){B(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(H(e.$$.fragment,s),o=!0)},o(s){N(e.$$.fragment,s),o=!1},d(s){T(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await W.admins.confirmPasswordReset(l==null?void 0:l.token,t,r),X("Successfully set a new admin password."),Y("/")}catch(g){W.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends E{constructor(e){super(),G(this,e,le,se,I,{params:5})}}export{ae as default};
@@ -1,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-3e0f12d8.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-4bd199fb.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’ll send you a recovery link:</p>`,n=g(),H(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),L(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=E(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),(!a||$&2)&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),S(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='<i class="ri-checkbox-circle-line"></i>',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=E(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=A(B.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(N(),y(a[$],1,1,()=>{a[$]=null}),D(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new z({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){H(e.$$.fragment)},m(n,l){L(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){S(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends M{constructor(e){super(),T(this,e,W,V,j,{})}}export{Y as default};
@@ -1,4 +1,4 @@
import{S as z,i as G,s as I,F as J,c as S,m as L,t as v,a as y,d as R,C as M,E as N,g as _,k as W,n as Y,o as b,G as j,H as A,p as B,q as D,e as m,w as C,b as h,f as d,r as H,h as k,u as P,v as K,y as E,x as O,z as T}from"./index-3e0f12d8.js";function Q(r){let e,t,l,s,n,o,c,a,i,u,g,$,p=r[3]&&F(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"),l=m("h5"),s=C(`Type your password to confirm changing your email address
import{S as z,i as G,s as I,F as J,c as S,m as L,t as v,a as y,d as R,C as M,E as N,g as _,k as W,n as Y,o as b,G as j,H as A,p as B,q as D,e as m,w as C,b as h,f as d,r as H,h as k,u as P,v as K,y as E,x as O,z as T}from"./index-4bd199fb.js";function Q(r){let e,t,l,s,n,o,c,a,i,u,g,$,p=r[3]&&F(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"),l=m("h5"),s=C(`Type your password to confirm changing your email address
`),p&&p.c(),n=h(),S(o.$$.fragment),c=h(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","content txt-center m-b-base"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],H(a,"btn-loading",r[1])},m(f,w){_(f,e,w),k(e,t),k(t,l),k(l,s),p&&p.m(l,null),k(e,n),L(o,e,null),k(e,c),k(e,a),k(a,i),u=!0,g||($=P(e,"submit",K(r[4])),g=!0)},p(f,w){f[3]?p?p.p(f,w):(p=F(f),p.c(),p.m(l,null)):p&&(p.d(1),p=null);const q={};w&769&&(q.$$scope={dirty:w,ctx:f}),o.$set(q),(!u||w&2)&&(a.disabled=f[1]),(!u||w&2)&&H(a,"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,l,s,n;return{c(){e=m("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Successfully changed the user email address.</p>
<p>You can now sign in with your new email address.</p></div>`,t=h(),l=m("button"),l.textContent="Close",d(e,"class","alert alert-success"),d(l,"type","button"),d(l,"class","btn btn-transparent btn-block")},m(o,c){_(o,e,c),_(o,t,c),_(o,l,c),s||(n=P(l,"click",r[6]),s=!0)},p:E,i:E,o:E,d(o){o&&b(e),o&&b(t),o&&b(l),s=!1,n()}}}function F(r){let e,t,l;return{c(){e=C("to "),t=m("strong"),l=C(r[3]),d(t,"class","txt-nowrap")},m(s,n){_(s,e,n),_(s,t,n),k(t,l)},p(s,n){n&8&&O(l,s[3])},d(s){s&&b(e),s&&b(t)}}}function V(r){let e,t,l,s,n,o,c,a;return{c(){e=m("label"),t=C("Password"),s=h(),n=m("input"),d(e,"for",l=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),k(e,t),_(i,s,u),_(i,n,u),T(n,r[0]),n.focus(),c||(a=P(n,"input",r[7]),c=!0)},p(i,u){u&256&&l!==(l=i[8])&&d(e,"for",l),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&T(n,i[0])},d(i){i&&b(e),i&&b(s),i&&b(n),c=!1,a()}}}function X(r){let e,t,l,s;const n=[U,Q],o=[];function c(a,i){return a[2]?0:1}return e=c(r),t=o[e]=n[e](r),{c(){t.c(),l=N()},m(a,i){o[e].m(a,i),_(a,l,i),s=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(W(),y(o[u],1,1,()=>{o[u]=null}),Y(),t=o[e],t?t.p(a,i):(t=o[e]=n[e](a),t.c()),v(t,1),t.m(l.parentNode,l))},i(a){s||(v(t),s=!0)},o(a){y(t),s=!1},d(a){o[e].d(a),a&&b(l)}}}function Z(r){let e,t;return e=new J({props:{nobranding:!0,$$slots:{default:[X]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(l,s){L(e,l,s),t=!0},p(l,[s]){const n={};s&527&&(n.$$scope={dirty:s,ctx:l}),e.$set(n)},i(l){t||(v(e.$$.fragment,l),t=!0)},o(l){y(e.$$.fragment,l),t=!1},d(l){R(e,l)}}}function x(r,e,t){let l,{params:s}=e,n="",o=!1,c=!1;async function a(){if(o)return;t(1,o=!0);const g=new j("../");try{const $=A(s==null?void 0:s.token);await g.collection($.collectionId).confirmEmailChange(s==null?void 0:s.token,n),t(2,c=!0)}catch($){B.errorResponseHandler($)}t(1,o=!1)}const i=()=>window.close();function u(){n=this.value,t(0,n)}return r.$$set=g=>{"params"in g&&t(5,s=g.params)},r.$$.update=()=>{r.$$.dirty&32&&t(3,l=M.getJWTPayload(s==null?void 0:s.token).newEmail||"")},[n,o,c,l,a,s,i,u]}class te extends z{constructor(e){super(),G(this,e,x,Z,I,{params:5})}}export{te as default};
@@ -1,4 +1,4 @@
import{S as J,i as M,s as W,F as Y,c as F,m as N,t as y,a as q,d as T,C as j,E as A,g as _,k as B,n as D,o as m,G as K,H as O,p as Q,q as E,e as b,w as R,b as P,f as p,r as G,h as w,u as H,v as U,y as S,x as V,z as h}from"./index-3e0f12d8.js";function X(r){let e,l,s,n,t,o,c,a,i,u,v,k,g,C,d=r[4]&&I(r);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),a=new E({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=R(`Reset your user password
import{S as J,i as M,s as W,F as Y,c as F,m as N,t as y,a as q,d as T,C as j,E as A,g as _,k as B,n as D,o as m,G as K,H as O,p as Q,q as E,e as b,w as R,b as P,f as p,r as G,h as w,u as H,v as U,y as S,x as V,z as h}from"./index-4bd199fb.js";function X(r){let e,l,s,n,t,o,c,a,i,u,v,k,g,C,d=r[4]&&I(r);return o=new E({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),a=new E({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=R(`Reset your user password
`),d&&d.c(),t=P(),F(o.$$.fragment),c=P(),F(a.$$.fragment),i=P(),u=b("button"),v=b("span"),v.textContent="Set new password",p(l,"class","content txt-center m-b-base"),p(v,"class","txt"),p(u,"type","submit"),p(u,"class","btn btn-lg btn-block"),u.disabled=r[2],G(u,"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(a,e,null),w(e,i),w(e,u),w(u,v),k=!0,g||(C=H(e,"submit",U(r[5])),g=!0)},p(f,$){f[4]?d?d.p(f,$):(d=I(f),d.c(),d.m(s,null)):d&&(d.d(1),d=null);const L={};$&3073&&(L.$$scope={dirty:$,ctx:f}),o.$set(L);const z={};$&3074&&(z.$$scope={dirty:$,ctx:f}),a.$set(z),(!k||$&4)&&(u.disabled=f[2]),(!k||$&4)&&G(u,"btn-loading",f[2])},i(f){k||(y(o.$$.fragment,f),y(a.$$.fragment,f),k=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),k=!1},d(f){f&&m(e),d&&d.d(),T(o),T(a),g=!1,C()}}}function Z(r){let e,l,s,n,t;return{c(){e=b("div"),e.innerHTML=`<div class="icon"><i class="ri-checkbox-circle-line"></i></div>
<div class="content txt-bold"><p>Successfully changed the user password.</p>
<p>You can now sign in with your new password.</p></div>`,l=P(),s=b("button"),s.textContent="Close",p(e,"class","alert alert-success"),p(s,"type","button"),p(s,"class","btn btn-transparent 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 I(r){let e,l,s;return{c(){e=R("for "),l=b("strong"),s=R(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,a;return{c(){e=b("label"),l=R("New password"),n=P(),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,u){_(i,e,u),w(e,l),_(i,n,u),_(i,t,u),h(t,r[0]),t.focus(),c||(a=H(t,"input",r[8]),c=!0)},p(i,u){u&1024&&s!==(s=i[10])&&p(e,"for",s),u&1024&&o!==(o=i[10])&&p(t,"id",o),u&1&&t.value!==i[0]&&h(t,i[0])},d(i){i&&m(e),i&&m(n),i&&m(t),c=!1,a()}}}function ee(r){let e,l,s,n,t,o,c,a;return{c(){e=b("label"),l=R("New password confirm"),n=P(),t=b("input"),p(e,"for",s=r[10]),p(t,"type","password"),p(t,"id",o=r[10]),t.required=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,t,u),h(t,r[1]),c||(a=H(t,"input",r[9]),c=!0)},p(i,u){u&1024&&s!==(s=i[10])&&p(e,"for",s),u&1024&&o!==(o=i[10])&&p(t,"id",o),u&2&&t.value!==i[1]&&h(t,i[1])},d(i){i&&m(e),i&&m(n),i&&m(t),c=!1,a()}}}function te(r){let e,l,s,n;const t=[Z,X],o=[];function c(a,i){return a[3]?0:1}return e=c(r),l=o[e]=t[e](r),{c(){l.c(),s=A()},m(a,i){o[e].m(a,i),_(a,s,i),n=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(B(),q(o[u],1,1,()=>{o[u]=null}),D(),l=o[e],l?l.p(a,i):(l=o[e]=t[e](a),l.c()),y(l,1),l.m(s.parentNode,s))},i(a){n||(y(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(s)}}}function se(r){let e,l;return e=new Y({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||(y(e.$$.fragment,s),l=!0)},o(s){q(e.$$.fragment,s),l=!1},d(s){T(e,s)}}}function le(r,e,l){let s,{params:n}=e,t="",o="",c=!1,a=!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,a=!0)}catch(C){Q.errorResponseHandler(C)}l(2,c=!1)}const u=()=>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=j.getJWTPayload(n==null?void 0:n.token).email||"")},[t,o,c,a,s,i,n,u,v,k]}class oe extends J{constructor(e){super(),M(this,e,le,se,W,{params:6})}}export{oe as default};
@@ -1,3 +1,3 @@
import{S as v,i as y,s as w,F as C,c as g,m as x,t as $,a as H,d as L,G as P,H as T,E as M,g as r,o as a,e as u,b as _,f,u as b,y as p}from"./index-3e0f12d8.js";function S(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 C,c as g,m as x,t as $,a as H,d as L,G as P,H as T,E as M,g as r,o as a,e as u,b as _,f,u as b,y as p}from"./index-4bd199fb.js";function S(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-transparent 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-transparent 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:S}let e=s(o),n=e(o);return{c(){n.c(),t=M()},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 C({props:{nobranding:!0,$$slots:{default:[V]},$$scope:{ctx:o}}}),{c(){g(t.$$.fragment)},m(e,n){x(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 E(o,t,s){let{params:e}=t,n=!1,l=!1;i();async function i(){s(1,l=!0);const d=new P("../");try{const m=T(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 N extends v{constructor(t){super(),y(this,t,E,q,w,{params:2})}}export{N as default};
@@ -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-3e0f12d8.js";import{S as fe}from"./SdkTabs-ed893501.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-4bd199fb.js";import{S as fe}from"./SdkTabs-fc1a80c5.js";function $e(o){var B,U,W,A,H,L,T,q,M,N,j,J;let i,m,l=o[0].name+"",b,d,h,f,_,$,k,c,S,v,w,R,C,g,E,r,D;return c=new fe({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[1]}');
@@ -1,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 L,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 N,u as Oe,N as Ue}from"./index-3e0f12d8.js";import{S as je}from"./SdkTabs-ed893501.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"),N(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+"")&&L(_,a),q&6&&N(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"),N(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)&&N(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+"",F,te,I,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 L,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 N,u as Oe,N as Ue}from"./index-4bd199fb.js";import{S as je}from"./SdkTabs-fc1a80c5.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"),N(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+"")&&L(_,a),q&6&&N(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"),N(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)&&N(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+"",F,te,I,P,K,T,Q,w,H,le,O,E,se,G,U=o[0].name+"",J,ae,oe,j,V,B,X,S,Y,R,Z,C,M,g=[],ne=new Map,ie,A,k=[],ce=new Map,y;P=new je({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${o[3]}');
@@ -1,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 I,O as me,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-3e0f12d8.js";import{S as Ue}from"./SdkTabs-ed893501.js";function ue(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+"",_,u,i,p;function m(){return s[4](s[5])}return{key:a,first:null,c(){l=c("button"),_=w(o),u=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,u),i||(p=Me(l,"click",m),i=!0)},p(P,$){s=P,$&4&&o!==(o=s[5].code+"")&&I(_,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,_,u;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,_),u=!0},p(i,p){s=i;const m={};p&4&&(m.content=s[5].body),o.$set(m),(!u||p&6)&&L(l,"active",s[1]===s[5].code)},i(i){u||(Z(o.$$.fragment,i),u=!0)},o(i){x(o.$$.fragment,i),u=!1},d(i){i&&d(l),he(o)}}}function je(a){var re,de;let s,l,o=a[0].name+"",_,u,i,p,m,P,$,D=a[0].name+"",N,ee,Q,q,z,B,G,R,H,te,O,C,se,J,E=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 I,O as me,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-4bd199fb.js";import{S as Ue}from"./SdkTabs-fc1a80c5.js";function ue(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+"",_,u,i,p;function m(){return s[4](s[5])}return{key:a,first:null,c(){l=c("button"),_=w(o),u=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,u),i||(p=Me(l,"click",m),i=!0)},p(P,$){s=P,$&4&&o!==(o=s[5].code+"")&&I(_,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,_,u;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,_),u=!0},p(i,p){s=i;const m={};p&4&&(m.content=s[5].body),o.$set(m),(!u||p&6)&&L(l,"active",s[1]===s[5].code)},i(i){u||(Z(o.$$.fragment,i),u=!0)},o(i){x(o.$$.fragment,i),u=!1},d(i){i&&d(l),he(o)}}}function je(a){var re,de;let s,l,o=a[0].name+"",_,u,i,p,m,P,$,D=a[0].name+"",N,ee,Q,q,z,B,G,R,H,te,O,C,se,J,E=a[0].name+"",K,le,V,S,W,T,X,M,Y,g,A,h=[],oe=new Map,ae,U,k=[],ne=new Map,y;q=new Ue({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[3]}');

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