Compare commits

..
Author SHA1 Message Date
Gani Georgiev 83d91b3dd5 added realtime topic length validator 2024-11-03 13:14:39 +02:00
Gani Georgiev 8c71a291ff silence hooks watch errors and just print as warning 2024-11-03 12:52:52 +02:00
Gani Georgiev 106ce0f0c4 added support for specifying collection id with the factory and added collections indexes validator to prevent duplicated definitions 2024-11-03 10:44:48 +02:00
Gani Georgiev c3557d4e94 [#5789] updated the hooks watcher to account for the case when hooksDir is a symlink 2024-11-03 10:42:58 +02:00
Gani Georgiev 1025fb2406 show generic index error and added support to autoreplace the collection id if part of an index name 2024-11-02 22:34:13 +02:00
Gani Georgiev fadb4e3d67 removed JSVM Collection class aliases 2024-11-02 22:00:58 +02:00
Gani Georgiev a446290a7a normalized Collection struct methods receiver 2024-11-02 11:20:21 +02:00
Gani Georgiev bc83fddaf2 updated changelog 2024-11-02 09:34:24 +02:00
Gani Georgiev 00da008d64 updated Store.GetOrSet to lock first with RLock/RUnlock 2024-11-01 22:06:53 +02:00
Gani Georgiev d3ca24e509 added more user friendly error message in case ServeEvent.Next() is not invoked 2024-11-01 19:10:33 +02:00
Gani Georgiev 1a1e3a2741 renamed build tag 2024-11-01 19:10:08 +02:00
Gani Georgiev b11ea60436 added rate limit label format info 2024-11-01 14:05:42 +02:00
Gani Georgiev 0c054bc6ff updated impersonate popup styles 2024-10-31 12:59:15 +02:00
Gani Georgiev 697545e73f auto select the first non-system collection on collection remove 2024-10-30 09:08:31 +02:00
Gani Georgiev 39415e0f49 don't unnecessary loads the records list while refrishing the collections 2024-10-30 08:44:09 +02:00
Gani Georgiev 8b42941bd3 removed the default cgo driver registration 2024-10-29 22:46:56 +02:00
Gani Georgiev 5a94ec9918 [#5741] use random string as id for non-system collections and fields 2024-10-29 20:08:16 +02:00
Gani Georgiev 658f0c4177 added support to reference collections in the UI with both their name and id 2024-10-29 13:38:30 +02:00
Gani Georgiev 421310d2f6 updated API preview disabled checks 2024-10-27 22:53:39 +02:00
Gani Georgiev 005047099d updated API preview 2024-10-27 22:12:37 +02:00
Gani Georgiev 49db093a51 fixed auto www redirect due to missing schema 2024-10-27 21:01:44 +02:00
Gani Georgiev 8646960abc reduce the default prewarmed jsvm pool size to 15 2024-10-25 12:27:10 +03:00
Gani Georgiev 9d2637847d fixed typo 2024-10-25 10:36:40 +03:00
Gani Georgiev 747c490b0a fixed test case typo 2024-10-25 10:17:01 +03:00
Gani Georgiev cb46591e70 updated changelog 2024-10-24 22:06:29 +03:00
Gani Georgiev 4c0b2154b0 updated ui/dist 2024-10-24 22:03:54 +03:00
Gani Georgiev 8c45d4d92d lock the _mfas and _otps delete api rule, fixed flaky tests, fixed jsvm types example 2024-10-24 21:59:00 +03:00
Gani Georgiev 0b7741f1f7 added additional godoc and updated the OAuth2 form to use the same created record pointer 2024-10-24 08:37:22 +03:00
Gani Georgiev c41a4dfc07 added v0.22.22 changelog entry 2024-10-18 17:43:33 +03:00
Gani Georgiev f3c948aa13 updated go deps 2024-10-18 17:42:48 +03:00
Gani Georgiev be908ad4bf added NaN checks 2024-10-18 17:38:19 +03:00
Gani Georgiev ae86525c13 updated typo 2024-10-18 14:18:32 +03:00
Gani Georgiev 321351e0bb updated jsvm types 2024-10-18 14:02:44 +03:00
Gani Georgiev 7685e64365 updated changelog 2024-10-18 13:58:20 +03:00
Gani Georgiev 6f2fe91da5 register the panic-recover handler after the activity logger 2024-10-18 13:47:10 +03:00
Gani Georgiev dbc074ee9a updated tests 2024-10-18 12:23:18 +03:00
Gani Georgiev 5dbf975424 [#5611] removed writable_schema usage 2024-10-18 11:09:28 +03:00
Gani Georgiev ade061cc80 [#5687] fixed BindBody FormData numeric string normalization 2024-10-16 10:22:34 +03:00
Gani Georgiev d43bcccb0b updated tygoja and the generated JSVM types to fix shortened return values from the same type 2024-10-15 22:12:22 +03:00
Gani Georgiev cfff7b6d11 added OIDC host change confirm message 2024-10-15 09:03:48 +03:00
Gani Georgiev 8271452430 removed unnecessary cast 2024-10-14 23:04:10 +03:00
Gani Georgiev 4209583a88 updated ui/dist 2024-10-14 20:08:58 +03:00
Gani Georgiev 78e6a8996f [#5674] fixed realtime auth 403 error on resubscribe 2024-10-14 19:50:40 +03:00
Gani Georgiev f5c6b9652f refreshed go.sum 2024-10-14 18:20:18 +03:00
Gani Georgiev cb4e3f2d43 updated jsvm types 2024-10-14 18:17:38 +03:00
Gani Georgiev f9ee710cdd normalized builtin middlewares to return hook.Handler 2024-10-14 18:17:31 +03:00
Gani Georgiev 47d5ea3ce2 fixed comments and added default generic arg name 2024-10-14 14:33:04 +03:00
Gani Georgiev 56b756e16b [#5673] added check for empty OAuth2User.AvatarURL 2024-10-14 14:31:39 +03:00
Gani Georgiev ff3f4332ce added default hook handler arg name and router search helper 2024-10-13 13:25:04 +03:00
Gani Georgiev 3e0869a30b fixed typo 2024-10-12 11:40:54 +03:00
Gani Georgiev 11d241392a updated CHANGELOG 2024-10-12 11:27:23 +03:00
Gani Georgiev 749c77cb9d fixed the UI Set Superusers only button click not properly resetting the input state 2024-10-12 11:24:07 +03:00
Gani Georgiev 3c87df9e55 added option to retrieve the OIDC user info from the id_token payload 2024-10-12 10:16:01 +03:00
Gani Georgiev 95d5ee40b0 [#5646] fixed single select, file and relation fields ui 2024-10-11 14:48:19 +03:00
Gani Georgiev 16d88681a3 fixed linked providers image path 2024-10-11 13:14:18 +03:00
Gani Georgiev 6a9784258f [#5641] changed the relation picker default sort to use rowid instead of created 2024-10-10 16:24:19 +03:00
Gani GeorgievandJay 830e818eb7 [#5346] added monday.com OAuth2 provider
Co-authored-by: Jay <jaytpa01@gmail.com>
2024-10-10 15:42:59 +03:00
Gani Georgievands-li1 397b69041e [#4999] added Notion OAuth2 provider
Co-authored-by: s-li1 <stevenli8892@hotmail.com.au>
2024-10-10 14:50:25 +03:00
Gani Georgiev 64bbd6f841 twich - fixed comments and use the provider ctx 2024-10-10 14:46:06 +03:00
Gani Georgiev f7a6097464 fixed CopyIcon empty value check 2024-10-09 23:27:11 +03:00
164 changed files with 9743 additions and 7568 deletions

No files matched your search

-13
View File
@@ -9,19 +9,6 @@ before:
- go mod tidy
builds:
# used only for tests
- id: build_cgo
main: ./examples/base
binary: pocketbase
ldflags:
- -s -w -X github.com/pocketbase/pocketbase.Version={{ .Version }}
env:
- CGO_ENABLED=1
goos:
- linux
goarch:
- amd64
- id: build_noncgo
main: ./examples/base
binary: pocketbase
+141
View File
@@ -1,3 +1,143 @@
## v0.23.0-rc9
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Fixed auto `www.` redirect due to missing URI schema.
- Fixed collection and field renaming when reusing an old collection/field name ([#5741](https://github.com/pocketbase/pocketbase/issues/5741)).
- Update the "API preview" section to include information about the batch api.
- Exported `core.DefaultDBConnect` function that could be used as a fallback when initializing custom SQLite drivers and builds.
- ⚠️ No longer loads the `mattn/go-sqlite3` driver by default when building with `CGO_ENABLED=1` to avoid `multiple definition ...` linker errors in case different CGO SQLite drivers or builds are used.
This means that no matter of the `CGO_ENABLED` value, now out of the box PocketBase will always use only the pure Go driver ([`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite)).
This will be documented properly in the new website but if you want to continue using `mattn/go-sqlite3` (e.g. because of the icu or other builtin extension) you could register it as follow:
```go
package main
import (
"database/sql"
"log"
"github.com/mattn/go-sqlite3"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
)
func init() {
// initialize default PRAGMAs for each new connection
sql.Register("pb_sqlite3",
&sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
_, err := conn.Exec(`
PRAGMA busy_timeout = 10000;
PRAGMA journal_mode = WAL;
PRAGMA journal_size_limit = 200000000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA temp_store = MEMORY;
PRAGMA cache_size = -16000;
`, nil)
return err
},
},
)
dbx.BuilderFuncMap["pb_sqlite3"] = dbx.BuilderFuncMap["sqlite3"]
}
func main() {
app := pocketbase.NewWithConfig(pocketbase.Config{
DBConnect: func(dbPath string) (*dbx.DB, error) {
return dbx.Open("pb_sqlite3", dbPath)
},
})
// custom hooks and plugins...
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
```
Also note that if you are not planning to use the `core.DefaultDBConnect` fallback as part of your custom driver registration you can exclude the default pure Go driver from the build with the build tag `-tags no_default_driver` to reduce the binary size a little.
- ⚠️ Removed JSVM `BaseCollection()`, `AuthCollection()`, `ViewCollection()` class aliases for simplicity and to avoid confusion with the accepted constructor arguments (_you can simply use as before `new Collection({ type: "base", ... })`; this will also initialize the default type specific options_).
- Other minor improvements (added validator for duplicated index definitions, updated the impersonate popup styles, added query param support for loading a collection based on its name, etc.).
## v0.23.0-rc8
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Lock the `_otps` and `_mfas` system collections Delete API rule for superusers only.
- Reassign in the JSVM executors the global `$app` variable with the hook scoped `e.app` value to minimize the risk of a deadlock when a hook or middleware is wrapped in a transaction.
- Reuse the OAuth2 created user record pointer to ensure that all its following hooks operate on the same record instance.
- Added tags support for the `OnFileTokenRequest` hook.
- Other minor changes (added index for the `_collections` type column, added more detailed godoc for the collection fields and `core.App` methods, fixed flaky record enrich tests, etc.).
## v0.23.0-rc7
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Register the default panic-recover middleware after the activity logger so that we can log the error.
- Updated the `RequestEvent.BindBody` FormData type inferring rules to convert numeric strings into float64 only if the resulting minimal number string representation matches the initial FormData string value ([#5687](https://github.com/pocketbase/pocketbase/issues/5687)).
- Fixed the JSVM types to include properly generated function declarations when the related Go functions have shortened/combined return values.
- Reorganized the record table fields<->columns syncing to remove the `PRAGMA writable_schema` usage.
## v0.23.0-rc6
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Fixed realtime 403 API error on resubscribe ([#5674](https://github.com/pocketbase/pocketbase/issues/5674)).
- Fixed the auto OAuth2 avatar mapped field assignment when the OAuth2 provider doesn't return an avatar URL ([#5673](https://github.com/pocketbase/pocketbase/pull/5673)).
_In case the avatar retrieval fails and the mapped record field "Required" option is not set, the error is silenced and only logged with WARN level._
- Added `Router.SEARCH(path, action)` helper method for registering `SEARCH` endpoints.
- Changed all builtin middlewares to return `*hook.Handler[*core.RequestEvent]` with a default middleware id for consistency and to allow removal.
Or in other words, replace `.BindFunc(apis.Gzip())` with `.Bind(apis.Gzip())`.
- Updated the JSVM types to reflect the recent changes.
## v0.23.0-rc5
> [!CAUTION]
> **This is a prerelease intended for test and experimental purposes only!**
- Added Notion OAuth2 provider ([#4999](https://github.com/pocketbase/pocketbase/pull/4999); thanks @s-li1).
- Added monday.com OAuth2 provider ([#5346](https://github.com/pocketbase/pocketbase/pull/5346); thanks @Jaytpa01).
- Added option to retrieve the OIDC OAuth2 user info from the `id_token` payload for the cases when the provider doesn't have a dedicated user info endpoint.
- Fixed the relation record picker to sort by default by `@rowid` instead of the `created` field as the latter is optional ([#5641](https://github.com/pocketbase/pocketbase/discussions/5641)).
- Fixed the UI "Set Superusers only" button click not properly resetting the input state.
- Fixed the OAuth2 providers logo path shown in the "Authorized providers" UI.
- Fixed the single value UI for the `select`, `file` and `relation` fields ([#5646](https://github.com/pocketbase/pocketbase/discussions/5646))
## v0.23.0-rc4
> [!CAUTION]
@@ -59,6 +199,7 @@
go run . serve --dir="pb_data_temp"
```
## v0.23.0-rc3
> [!CAUTION]
+19
View File
@@ -2,6 +2,25 @@
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
---
## v0.22.22
- Added deprecation log in case Instagram OAuth2 is used (_related to [#5652](https://github.com/pocketbase/pocketbase/discussions/5652)_).
- Added `update` command warning to prevent unnecessary downloading PocketBase v0.23.0 since it will contain breaking changes.
- Added global JSVM `toString()` helper (_successor of `readerToString()`_) to stringify any value (bool, number, multi-byte array, io.Reader, etc.).
_`readerToString` is still available but it is marked as deprecated. You can also use `toString` as replacement for of `String.fromCharCode` to properly stringify multi-byte unicode characters like emojis._
```js
decodeURIComponent(escape(String.fromCharCode(...bytes))) -> toString(bytes)
```
- Updated `aws-sdk-go-v2` and removed deprecated `WithEndpointResolverWithOptions`.
- Backported some of the v0.23.0-rc form validators, fixes and tests.
- Bumped GitHub action min Go version and dependencies.
## v0.22.21
- Lock the logs database during backup to prevent `database disk image is malformed` errors in case there is a log write running in the background ([#5541](https://github.com/pocketbase/pocketbase/discussions/5541)).
+2 -1
View File
@@ -28,9 +28,10 @@ func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
// register default middlewares
pbRouter.Bind(activityLogger())
pbRouter.Bind(panicRecover())
pbRouter.Bind(rateLimit())
pbRouter.Bind(loadAuthToken())
pbRouter.Bind(securityHeaders())
pbRouter.Bind(rateLimit())
pbRouter.Bind(BodyLimit(DefaultMaxBodySize))
apiGroup := pbRouter.Group("/api")
+8 -8
View File
@@ -29,14 +29,14 @@ func bindBatchApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
type HandleFunc func(e *core.RequestEvent) error
type BatchActionHandlerFunc func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc
type BatchActionHandlerFunc func(app core.App, ir *core.InternalRequest, params map[string]string, next func(data any) error) HandleFunc
// ValidBatchActions defines a map with the supported batch InternalRequest actions.
//
// Note: when adding new routes make sure that their middlewares are inlined!
var ValidBatchActions = map[*regexp.Regexp]BatchActionHandlerFunc{
// "upsert" handler
regexp.MustCompile(`^PUT /api/collections/(?P<collection>[^\/\?]+)/records(?P<query>\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc {
regexp.MustCompile(`^PUT /api/collections/(?P<collection>[^\/\?]+)/records(?P<query>\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
var id string
if len(ir.Body) > 0 && ir.Body["id"] != "" {
id = cast.ToString(ir.Body["id"])
@@ -59,13 +59,13 @@ var ValidBatchActions = map[*regexp.Regexp]BatchActionHandlerFunc{
ir.URL = "/api/collections/" + params["collection"] + "/records" + params["query"]
return recordCreate(next)
},
regexp.MustCompile(`^POST /api/collections/(?P<collection>[^\/\?]+)/records(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc {
regexp.MustCompile(`^POST /api/collections/(?P<collection>[^\/\?]+)/records(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
return recordCreate(next)
},
regexp.MustCompile(`^PATCH /api/collections/(?P<collection>[^\/\?]+)/records/(?P<id>[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc {
regexp.MustCompile(`^PATCH /api/collections/(?P<collection>[^\/\?]+)/records/(?P<id>[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
return recordUpdate(next)
},
regexp.MustCompile(`^DELETE /api/collections/(?P<collection>[^\/\?]+)/records/(?P<id>[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func() error) HandleFunc {
regexp.MustCompile(`^DELETE /api/collections/(?P<collection>[^\/\?]+)/records/(?P<id>[^\/\?]+)(\?.*)?$`): func(app core.App, ir *core.InternalRequest, params map[string]string, next func(any) error) HandleFunc {
return recordDelete(next)
},
}
@@ -246,7 +246,7 @@ func (p *batchProcessor) process(activeApp core.App, batch []*core.InternalReque
p.baseEvent,
batch[0],
p.infoContext,
func() error {
func(_ any) error {
if len(batch) == 1 {
return nil
}
@@ -277,7 +277,7 @@ func processInternalRequest(
baseEvent *core.RequestEvent,
ir *core.InternalRequest,
infoContext string,
optNext func() error,
optNext func(data any) error,
) (*BatchRequestResult, error) {
handle, params, ok := prepareInternalAction(activeApp, ir, optNext)
if !ok {
@@ -475,7 +475,7 @@ func extractPrefixedFiles(request *http.Request, prefixes ...string) (map[string
return result, nil
}
func prepareInternalAction(activeApp core.App, ir *core.InternalRequest, optNext func() error) (HandleFunc, map[string]string, bool) {
func prepareInternalAction(activeApp core.App, ir *core.InternalRequest, optNext func(data any) error) (HandleFunc, map[string]string, bool) {
full := strings.ToUpper(ir.Method) + " " + ir.URL
for re, actionFactory := range ValidBatchActions {
+8 -2
View File
@@ -182,9 +182,15 @@ func collectionTruncate(e *core.RequestEvent) error {
}
func collectionScaffolds(e *core.RequestEvent) error {
return e.JSON(http.StatusOK, map[string]*core.Collection{
collections := map[string]*core.Collection{
core.CollectionTypeBase: core.NewBaseCollection(""),
core.CollectionTypeAuth: core.NewAuthCollection(""),
core.CollectionTypeView: core.NewViewCollection(""),
})
}
for _, c := range collections {
c.Id = "" // clear autogenerated id
}
return e.JSON(http.StatusOK, collections)
}
+5 -3
View File
@@ -503,7 +503,7 @@ func TestCollectionCreate(t *testing.T) {
"emailChangeToken":{"duration":123},
"fields":[
{"type":"text","id":"12345789","name":"test"},
{"type":"text","name":"tokenKey","required":false,"min":10}
{"type":"text","name":"tokenKey","system":true,"required":false,"min":10}
]
}`),
Headers: map[string]string{
@@ -800,7 +800,7 @@ func TestCollectionCreate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
demo1.AddIndex("exist_test", false, "created", "")
demo1.AddIndex("exist_test", false, "updated", "")
if err = app.Save(demo1); err != nil {
t.Fatal(err)
}
@@ -1268,7 +1268,7 @@ func TestCollectionUpdate(t *testing.T) {
if err != nil {
t.Fatal(err)
}
demo1.AddIndex("exist_test", false, "created", "")
demo1.AddIndex("exist_test", false, "updated", "")
if err = app.Save(demo1); err != nil {
t.Fatal(err)
}
@@ -1409,6 +1409,8 @@ func TestCollectionScaffolds(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":""`,
`"name":""`,
`"auth":{`,
`"base":{`,
`"view":{`,
+1
View File
@@ -60,6 +60,7 @@ func (api *fileApi) fileToken(e *core.RequestEvent) error {
event := new(core.FileTokenRequestEvent)
event.RequestEvent = e
event.Token = token
event.Record = e.Auth
return e.App.OnFileTokenRequest().Trigger(event, func(e *core.FileTokenRequestEvent) error {
return e.JSON(http.StatusOK, map[string]string{
+46 -2
View File
@@ -1,10 +1,12 @@
package apis
import (
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"runtime"
"slices"
"strings"
"time"
@@ -29,11 +31,14 @@ const (
DefaultWWWRedirectMiddlewarePriority = -99999
DefaultWWWRedirectMiddlewareId = "pbWWWRedirect"
DefaultActivityLoggerMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 30
DefaultActivityLoggerMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 40
DefaultActivityLoggerMiddlewareId = "pbActivityLogger"
DefaultSkipSuccessActivityLogMiddlewareId = "pbSkipSuccessActivityLog"
DefaultEnableAuthIdActivityLog = "pbEnableAuthIdActivityLog"
DefaultPanicRecoverMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 30
DefaultPanicRecoverMiddlewareId = "pbPanicRecover"
DefaultLoadAuthTokenMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 20
DefaultLoadAuthTokenMiddlewareId = "pbLoadAuthToken"
@@ -241,9 +246,15 @@ func wwwRedirect(redirectHosts []string) *hook.Handler[*core.RequestEvent] {
host := e.Request.Host
if strings.HasPrefix(host, "www.") && list.ExistInSlice(host, redirectHosts) {
// note: e.Request.URL.Scheme would be empty
schema := "http://"
if e.IsTLS() {
schema = "https://"
}
return e.Redirect(
http.StatusTemporaryRedirect,
(e.Request.URL.Scheme + "://" + host[4:] + e.Request.RequestURI),
(schema + host[4:] + e.Request.RequestURI),
)
}
@@ -252,6 +263,39 @@ func wwwRedirect(redirectHosts []string) *hook.Handler[*core.RequestEvent] {
}
}
// panicRecover returns a default panic-recover handler.
func panicRecover() *hook.Handler[*core.RequestEvent] {
return &hook.Handler[*core.RequestEvent]{
Id: DefaultPanicRecoverMiddlewareId,
Priority: DefaultPanicRecoverMiddlewarePriority,
Func: func(e *core.RequestEvent) (err error) {
// panic-recover
defer func() {
recoverResult := recover()
if recoverResult == nil {
return
}
recoverErr, ok := recoverResult.(error)
if !ok {
recoverErr = fmt.Errorf("%v", recoverResult)
} else if errors.Is(recoverErr, http.ErrAbortHandler) {
// don't recover ErrAbortHandler so the response to the client can be aborted
panic(recoverResult)
}
stack := make([]byte, 2<<10) // 2 KB
length := runtime.Stack(stack, true)
err = e.InternalServerError("", fmt.Errorf("[PANIC RECOVER] %w %s", recoverErr, stack[:length]))
}()
err = e.Next()
return err
},
}
}
// securityHeaders middleware adds common security headers to the response.
//
// This middleware is registered by default for all routes.
-3
View File
@@ -20,9 +20,6 @@ const (
// BodyLimit returns a middleware function that changes the default request body size limit.
//
// Note that in order to have effect this middleware should be registered
// before other middlewares that reads the request body.
//
// If limitBytes <= 0, no limit is applied.
//
// Otherwise, if the request body size exceeds the configured limitBytes,
+92 -87
View File
@@ -19,6 +19,7 @@ import (
"strings"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/hook"
)
const (
@@ -124,7 +125,7 @@ var DefaultCORSConfig = CORSConfig{
}
// CORSWithConfig returns a CORS middleware with config.
func CORSWithConfig(config CORSConfig) func(e *core.RequestEvent) error {
func CORSWithConfig(config CORSConfig) *hook.Handler[*core.RequestEvent] {
// Defaults
if len(config.AllowOrigins) == 0 {
config.AllowOrigins = DefaultCORSConfig.AllowOrigins
@@ -151,108 +152,112 @@ func CORSWithConfig(config CORSConfig) func(e *core.RequestEvent) error {
maxAge = strconv.Itoa(config.MaxAge)
}
return func(e *core.RequestEvent) error {
req := e.Request
res := e.Response
origin := req.Header.Get("Origin")
allowOrigin := ""
return &hook.Handler[*core.RequestEvent]{
Id: DefaultCorsMiddlewareId,
Priority: DefaultCorsMiddlewarePriority,
Func: func(e *core.RequestEvent) error {
req := e.Request
res := e.Response
origin := req.Header.Get("Origin")
allowOrigin := ""
res.Header().Add("Vary", "Origin")
res.Header().Add("Vary", "Origin")
// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
// For simplicity we just consider method type and later `Origin` header.
preflight := req.Method == http.MethodOptions
// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
// For simplicity we just consider method type and later `Origin` header.
preflight := req.Method == http.MethodOptions
// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
if origin == "" {
if !preflight {
return e.Next()
// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
if origin == "" {
if !preflight {
return e.Next()
}
return e.NoContent(http.StatusNoContent)
}
return e.NoContent(http.StatusNoContent)
}
if config.AllowOriginFunc != nil {
allowed, err := config.AllowOriginFunc(origin)
if err != nil {
return err
}
if allowed {
allowOrigin = origin
}
} else {
// Check allowed origins
for _, o := range config.AllowOrigins {
if o == "*" && config.AllowCredentials && config.UnsafeWildcardOriginWithAllowCredentials {
if config.AllowOriginFunc != nil {
allowed, err := config.AllowOriginFunc(origin)
if err != nil {
return err
}
if allowed {
allowOrigin = origin
break
}
if o == "*" || o == origin {
allowOrigin = o
break
}
if matchSubdomain(origin, o) {
allowOrigin = origin
break
}
}
checkPatterns := false
if allowOrigin == "" {
// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
if len(origin) <= (253+3+5) && strings.Contains(origin, "://") {
checkPatterns = true
}
}
if checkPatterns {
for _, re := range allowOriginPatterns {
if match, _ := regexp.MatchString(re, origin); match {
} else {
// Check allowed origins
for _, o := range config.AllowOrigins {
if o == "*" && config.AllowCredentials && config.UnsafeWildcardOriginWithAllowCredentials {
allowOrigin = origin
break
}
if o == "*" || o == origin {
allowOrigin = o
break
}
if matchSubdomain(origin, o) {
allowOrigin = origin
break
}
}
}
}
// Origin not allowed
if allowOrigin == "" {
checkPatterns := false
if allowOrigin == "" {
// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
if len(origin) <= (253+3+5) && strings.Contains(origin, "://") {
checkPatterns = true
}
}
if checkPatterns {
for _, re := range allowOriginPatterns {
if match, _ := regexp.MatchString(re, origin); match {
allowOrigin = origin
break
}
}
}
}
// Origin not allowed
if allowOrigin == "" {
if !preflight {
return e.Next()
}
return e.NoContent(http.StatusNoContent)
}
res.Header().Set("Access-Control-Allow-Origin", allowOrigin)
if config.AllowCredentials {
res.Header().Set("Access-Control-Allow-Credentials", "true")
}
// Simple request
if !preflight {
if exposeHeaders != "" {
res.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
}
return e.Next()
}
// Preflight request
res.Header().Add("Vary", "Access-Control-Request-Method")
res.Header().Add("Vary", "Access-Control-Request-Headers")
res.Header().Set("Access-Control-Allow-Methods", allowMethods)
if allowHeaders != "" {
res.Header().Set("Access-Control-Allow-Headers", allowHeaders)
} else {
h := req.Header.Get("Access-Control-Request-Headers")
if h != "" {
res.Header().Set("Access-Control-Allow-Headers", h)
}
}
if config.MaxAge != 0 {
res.Header().Set("Access-Control-Max-Age", maxAge)
}
return e.NoContent(http.StatusNoContent)
}
res.Header().Set("Access-Control-Allow-Origin", allowOrigin)
if config.AllowCredentials {
res.Header().Set("Access-Control-Allow-Credentials", "true")
}
// Simple request
if !preflight {
if exposeHeaders != "" {
res.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
}
return e.Next()
}
// Preflight request
res.Header().Add("Vary", "Access-Control-Request-Method")
res.Header().Add("Vary", "Access-Control-Request-Headers")
res.Header().Set("Access-Control-Allow-Methods", allowMethods)
if allowHeaders != "" {
res.Header().Set("Access-Control-Allow-Headers", allowHeaders)
} else {
h := req.Header.Get("Access-Control-Request-Headers")
if h != "" {
res.Header().Set("Access-Control-Allow-Headers", h)
}
}
if config.MaxAge != 0 {
res.Header().Set("Access-Control-Max-Age", maxAge)
}
return e.NoContent(http.StatusNoContent)
},
}
}
+57 -49
View File
@@ -18,6 +18,7 @@ import (
"sync"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/router"
)
@@ -25,6 +26,10 @@ const (
gzipScheme = "gzip"
)
const (
DefaultGzipMiddlewareId = "pbGzip"
)
// GzipConfig defines the config for Gzip middleware.
type GzipConfig struct {
// Gzip compression level.
@@ -45,13 +50,13 @@ type GzipConfig struct {
MinLength int
}
// Gzip returns a middleware which compresses HTTP response using gzip compression scheme.
func Gzip() func(*core.RequestEvent) error {
// Gzip returns a middleware which compresses HTTP response using Gzip compression scheme.
func Gzip() *hook.Handler[*core.RequestEvent] {
return GzipWithConfig(GzipConfig{})
}
// GzipWithConfig returns a middleware which compresses HTTP response using gzip compression scheme.
func GzipWithConfig(config GzipConfig) func(*core.RequestEvent) error {
func GzipWithConfig(config GzipConfig) *hook.Handler[*core.RequestEvent] {
if config.Level < -2 || config.Level > 9 { // these are consts: gzip.HuffmanOnly and gzip.BestCompression
panic(errors.New("invalid gzip level"))
}
@@ -79,54 +84,57 @@ func GzipWithConfig(config GzipConfig) func(*core.RequestEvent) error {
},
}
return func(e *core.RequestEvent) error {
e.Response.Header().Add("Vary", "Accept-Encoding")
if strings.Contains(e.Request.Header.Get("Accept-Encoding"), gzipScheme) {
w, ok := pool.Get().(*gzip.Writer)
if !ok {
return e.InternalServerError("", errors.New("failed to get gzip.Writer"))
return &hook.Handler[*core.RequestEvent]{
Id: DefaultGzipMiddlewareId,
Func: func(e *core.RequestEvent) error {
e.Response.Header().Add("Vary", "Accept-Encoding")
if strings.Contains(e.Request.Header.Get("Accept-Encoding"), gzipScheme) {
w, ok := pool.Get().(*gzip.Writer)
if !ok {
return e.InternalServerError("", errors.New("failed to get gzip.Writer"))
}
rw := e.Response
w.Reset(rw)
buf := bpool.Get().(*bytes.Buffer)
buf.Reset()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
defer func() {
// There are different reasons for cases when we have not yet written response to the client and now need to do so.
// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
if !grw.wroteBody {
if rw.Header().Get("Content-Encoding") == gzipScheme {
rw.Header().Del("Content-Encoding")
}
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
// We have to reset response to it's pristine state when
// nothing is written to body or error is returned.
// See issue echo#424, echo#407.
e.Response = rw
w.Reset(io.Discard)
} else if !grw.minLengthExceeded {
// Write uncompressed response
e.Response = rw
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
grw.buffer.WriteTo(rw)
w.Reset(io.Discard)
}
w.Close()
bpool.Put(buf)
pool.Put(w)
}()
e.Response = grw
}
rw := e.Response
w.Reset(rw)
buf := bpool.Get().(*bytes.Buffer)
buf.Reset()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
defer func() {
// There are different reasons for cases when we have not yet written response to the client and now need to do so.
// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
if !grw.wroteBody {
if rw.Header().Get("Content-Encoding") == gzipScheme {
rw.Header().Del("Content-Encoding")
}
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
// We have to reset response to it's pristine state when
// nothing is written to body or error is returned.
// See issue echo#424, echo#407.
e.Response = rw
w.Reset(io.Discard)
} else if !grw.minLengthExceeded {
// Write uncompressed response
e.Response = rw
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
grw.buffer.WriteTo(rw)
w.Reset(io.Discard)
}
w.Close()
bpool.Put(buf)
pool.Put(w)
}()
e.Response = grw
}
return e.Next()
return e.Next()
},
}
}
+39
View File
@@ -9,6 +9,45 @@ import (
"github.com/pocketbase/pocketbase/tests"
)
func TestPanicRecover(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
{
Name: "panic from route",
Method: http.MethodGet,
URL: "/my/test",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
e.Router.GET("/my/test", func(e *core.RequestEvent) error {
panic("123")
})
},
ExpectedStatus: 500,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "panic from middleware",
Method: http.MethodGet,
URL: "/my/test",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
e.Router.GET("/my/test", func(e *core.RequestEvent) error {
return e.String(http.StatusOK, "test")
}).BindFunc(func(e *core.RequestEvent) error {
panic(123)
})
},
ExpectedStatus: 500,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRequireGuestOnly(t *testing.T) {
t.Parallel()
+22 -19
View File
@@ -18,7 +18,6 @@ import (
"github.com/pocketbase/pocketbase/tools/routine"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/pocketbase/pocketbase/tools/subscriptions"
"github.com/spf13/cast"
"golang.org/x/sync/errgroup"
)
@@ -161,7 +160,10 @@ type realtimeSubscribeForm struct {
func (form *realtimeSubscribeForm) validate() error {
return validation.ValidateStruct(form,
validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)),
validation.Field(&form.Subscriptions, validation.Length(0, 1000)),
validation.Field(&form.Subscriptions,
validation.Length(0, 1000),
validation.Each(validation.Length(0, 2500)),
),
)
}
@@ -185,10 +187,9 @@ func realtimeSetSubscriptions(e *core.RequestEvent) error {
return e.NotFoundError("Missing or invalid client id.", err)
}
// check if the previous request was authorized
oldAuthId := extractAuthIdFromGetter(client)
newAuthId := extractAuthIdFromGetter(e)
if oldAuthId != "" && oldAuthId != newAuthId {
// for now allow only guest->auth upgrades and any other auth change is forbidden
clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record)
if clientAuth != nil && !isSameAuth(clientAuth, e.Auth) {
return e.ForbiddenError("The current and the previous request authorization don't match.", nil)
}
@@ -487,6 +488,8 @@ func realtimeBroadcastRecord(app core.App, action string, record *core.Record, d
for _, chunk := range chunks {
group.Go(func() error {
var clientAuth *core.Record
for _, client := range chunk {
// note: not executed concurrently to avoid races and to ensure
// that the access checks are applied for the current record db state
@@ -496,6 +499,8 @@ func realtimeBroadcastRecord(app core.App, action string, record *core.Record, d
continue
}
clientAuth, _ = client.Get(RealtimeClientAuthKey).(*core.Record)
for sub, options := range subs {
// create a clean record copy without expand and unknown fields
// because we don't know yet which exact fields the client subscription has permissions to access
@@ -507,8 +512,8 @@ func realtimeBroadcastRecord(app core.App, action string, record *core.Record, d
Method: "GET",
Query: options.Query,
Headers: options.Headers,
Auth: clientAuth,
}
requestInfo.Auth, _ = client.Get(RealtimeClientAuthKey).(*core.Record)
if !realtimeCanAccessRecord(app, cleanRecord, requestInfo, rule) {
continue
@@ -535,8 +540,7 @@ func realtimeBroadcastRecord(app core.App, action string, record *core.Record, d
// ignore the auth record email visibility checks
// for auth owner, superuser or manager
if collection.IsAuth() {
authId := extractAuthIdFromGetter(client)
if authId == cleanRecord.Id ||
if isSameAuth(clientAuth, cleanRecord) ||
realtimeCanAccessRecord(app, cleanRecord, requestInfo, collection.ManageRule) {
cleanRecord.IgnoreEmailVisibility(true)
}
@@ -681,17 +685,16 @@ func realtimeUnsetDryCachedRecord(app core.App, action string, record *core.Reco
return group.Wait()
}
type getter interface {
Get(string) any
}
func extractAuthIdFromGetter(val getter) string {
record, _ := val.Get(RealtimeClientAuthKey).(*core.Record)
if record != nil {
return record.Id
func isSameAuth(authA, authB *core.Record) bool {
if authA == nil {
return authB == nil
}
return ""
if authB == nil {
return false
}
return authA.Id == authB.Id && authA.Collection().Id == authB.Collection().Id
}
// realtimeCanAccessRecord checks if the subscription client has access to the specified record model.
@@ -709,7 +712,7 @@ func realtimeCanAccessRecord(
// check the subscription client-side filter (if any)
// ---
filter := cast.ToString(requestInfo.Query[search.FilterQueryParam])
filter := requestInfo.Query[search.FilterQueryParam]
if filter == "" {
return true // no further checks needed
}
+82 -2
View File
@@ -183,6 +183,54 @@ func TestRealtimeSubscribe(t *testing.T) {
resetClient()
},
},
{
Name: "existing client with invalid topic length",
Method: http.MethodPost,
URL: "/api/realtime",
Body: strings.NewReader(`{
"clientId": "` + client.Id() + `",
"subscriptions": ["abc", "` + strings.Repeat("a", 2501) + `"]
}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.SubscriptionsBroker().Register(client)
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
resetClient()
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"subscriptions":{"1":{"code":"validation_length_too_long"`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "existing client with valid topic length",
Method: http.MethodPost,
URL: "/api/realtime",
Body: strings.NewReader(`{
"clientId": "` + client.Id() + `",
"subscriptions": ["abc", "` + strings.Repeat("a", 2500) + `"]
}`),
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRealtimeSubscribeRequest": 1,
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
client.Subscribe("test0")
app.SubscriptionsBroker().Register(client)
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
if len(client.Subscriptions()) != 2 {
t.Errorf("Expected %d subscriptions, got %d", 2, len(client.Subscriptions()))
}
if client.HasSubscription("test0") {
t.Errorf("Expected old subscriptions to be replaced")
}
resetClient()
},
},
{
Name: "existing client - empty subscriptions",
Method: http.MethodPost,
@@ -233,7 +281,7 @@ func TestRealtimeSubscribe(t *testing.T) {
},
},
{
Name: "existing client - authorized superuser",
Name: "existing client - guest -> authorized superuser",
Method: http.MethodPost,
URL: "/api/realtime",
Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`),
@@ -257,7 +305,7 @@ func TestRealtimeSubscribe(t *testing.T) {
},
},
{
Name: "existing client - authorized regular record",
Name: "existing client - guest -> authorized regular auth record",
Method: http.MethodPost,
URL: "/api/realtime",
Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`),
@@ -280,6 +328,38 @@ func TestRealtimeSubscribe(t *testing.T) {
resetClient()
},
},
{
Name: "existing client - same auth",
Method: http.MethodPost,
URL: "/api/realtime",
Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRealtimeSubscribeRequest": 1,
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
// the same user as the auth token
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
client.Set(apis.RealtimeClientAuthKey, user)
app.SubscriptionsBroker().Register(client)
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
authRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record)
if authRecord == nil {
t.Errorf("Expected auth record model, got nil")
}
resetClient()
},
},
{
Name: "existing client - mismatched auth",
Method: http.MethodPost,
+6 -2
View File
@@ -9,9 +9,13 @@ import (
// the corresponding handlers.
func bindRecordAuthApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
// global oauth2 subscription redirect handler
rg.GET("/oauth2-redirect", oauth2SubscriptionRedirect)
rg.GET("/oauth2-redirect", oauth2SubscriptionRedirect).Bind(
SkipSuccessActivityLog(), // skip success log as it could contain sensitive information in the url
)
// add again as POST in case of response_mode=form_post
rg.POST("/oauth2-redirect", oauth2SubscriptionRedirect)
rg.POST("/oauth2-redirect", oauth2SubscriptionRedirect).Bind(
SkipSuccessActivityLog(), // skip success log as it could contain sensitive information in the url
)
sub := rg.Group("/collections/{collection}")
+17 -20
View File
@@ -2,9 +2,9 @@ package apis
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
"net/http"
"time"
@@ -236,7 +236,11 @@ func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *cor
oldCanAssignUsername(txApp, e.Collection, e.OAuth2User.Username) {
payload[e.Collection.OAuth2.MappedFields.Username] = e.OAuth2User.Username
}
if _, ok := payload[e.Collection.OAuth2.MappedFields.AvatarURL]; !ok && e.Collection.OAuth2.MappedFields.AvatarURL != "" {
if _, ok := payload[e.Collection.OAuth2.MappedFields.AvatarURL]; !ok &&
// no existing OAuth2 mapping
e.Collection.OAuth2.MappedFields.AvatarURL != "" &&
// non-empty OAuth2 avatar url
e.OAuth2User.AvatarURL != "" {
mappedField := e.Collection.Fields.GetByName(e.Collection.OAuth2.MappedFields.AvatarURL)
if mappedField != nil && mappedField.Type() == core.FieldTypeFile {
// download the avatar if the mapped field is a file
@@ -246,9 +250,10 @@ func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *cor
return filesystem.NewFileFromURL(ctx, e.OAuth2User.AvatarURL)
}()
if err != nil {
return err
txApp.Logger().Warn("Failed to retrieve OAuth2 avatar", slog.String("error", err.Error()))
} else {
payload[e.Collection.OAuth2.MappedFields.AvatarURL] = avatarFile
}
payload[e.Collection.OAuth2.MappedFields.AvatarURL] = avatarFile
} else {
// otherwise - assign the url string
payload[e.Collection.OAuth2.MappedFields.AvatarURL] = e.OAuth2User.AvatarURL
@@ -329,27 +334,19 @@ func sendOAuth2RecordCreateRequest(txApp core.App, e *core.RecordAuthWithOAuth2R
Body: payload,
}
response, err := processInternalRequest(txApp, e.RequestEvent, ir, core.RequestInfoContextOAuth2, nil)
var createdRecord *core.Record
response, err := processInternalRequest(txApp, e.RequestEvent, ir, core.RequestInfoContextOAuth2, func(data any) error {
createdRecord, _ = data.(*core.Record)
return nil
})
if err != nil {
return nil, err
}
if response.Status != http.StatusOK {
if response.Status != http.StatusOK || createdRecord == nil {
return nil, errors.New("failed to create OAuth2 auth record")
}
recordResponse := struct {
Id string `json:"id"`
}{}
raw, err := json.Marshal(response.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(raw, &recordResponse); err != nil {
return nil, err
}
return txApp.FindRecordById(e.Collection, recordResponse.Id)
return createdRecord, nil
}
+79
View File
@@ -1109,6 +1109,85 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordValidate": 4,
},
},
{
Name: "creating user (with mapped OAuth2 avatarURL field but empty OAuth2User.avatarURL value)",
Method: http.MethodPost,
URL: "/api/collections/users/auth-with-oauth2",
Body: strings.NewReader(`{
"provider": "test",
"code":"123",
"redirectURL": "https://example.com"
}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
usersCol, err := app.FindCollectionByNameOrId("users")
if err != nil {
t.Fatal(err)
}
// register the test provider
auth.Providers["test"] = func() auth.Provider {
return &oauth2MockProvider{
AuthUser: &auth.AuthUser{
Id: "oauth2_id",
Email: "oauth2@example.com",
AvatarURL: "",
},
Token: &oauth2.Token{AccessToken: "abc"},
}
}
// add the test provider in the collection
usersCol.MFA.Enabled = false
usersCol.OAuth2.Enabled = true
usersCol.OAuth2.Providers = []core.OAuth2ProviderConfig{{
Name: "test",
ClientId: "123",
ClientSecret: "456",
}}
usersCol.OAuth2.MappedFields = core.OAuth2KnownFields{
AvatarURL: "avatar",
}
if err := app.Save(usersCol); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"email":"oauth2@example.com"`,
`"emailVisibility":false`,
`"verified":true`,
`"avatar":""`,
},
NotExpectedContent: []string{
// hidden fields
`"tokenKey"`,
`"password"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordAuthWithOAuth2Request": 1,
"OnRecordAuthRequest": 1,
"OnRecordCreateRequest": 1,
"OnRecordEnrich": 2, // the auth response and from the create request
// ---
"OnModelCreate": 3, // record + authOrigins + externalAuths
"OnModelCreateExecute": 3,
"OnModelAfterCreateSuccess": 3,
"OnRecordCreate": 3,
"OnRecordCreateExecute": 3,
"OnRecordAfterCreateSuccess": 3,
// ---
"OnModelUpdate": 1, // created record verified state change
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
// ---
"OnModelValidate": 4,
"OnRecordValidate": 4,
},
},
{
Name: "creating user (with mapped OAuth2 fields and avatarURL->non-file field)",
Method: http.MethodPost,
+9 -9
View File
@@ -147,7 +147,7 @@ func recordView(e *core.RequestEvent) error {
})
}
func recordCreate(optFinalizer func() error) func(e *core.RequestEvent) error {
func recordCreate(optFinalizer func(data any) error) func(e *core.RequestEvent) error {
return func(e *core.RequestEvent) error {
collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
if err != nil || collection == nil {
@@ -278,7 +278,7 @@ func recordCreate(optFinalizer func() error) func(e *core.RequestEvent) error {
if optFinalizer != nil {
isOptFinalizerCalled = true
err = optFinalizer()
err = optFinalizer(e.Record)
if err != nil {
return firstApiError(err, e.InternalServerError("", err))
}
@@ -292,7 +292,7 @@ func recordCreate(optFinalizer func() error) func(e *core.RequestEvent) error {
// e.g. in case the regular hook chain was stopped and the finalizer cannot be executed as part of the last e.Next() task
if !isOptFinalizerCalled && optFinalizer != nil {
if err := optFinalizer(); err != nil {
if err := optFinalizer(event.Record); err != nil {
return firstApiError(err, e.InternalServerError("", err))
}
}
@@ -301,7 +301,7 @@ func recordCreate(optFinalizer func() error) func(e *core.RequestEvent) error {
}
}
func recordUpdate(optFinalizer func() error) func(e *core.RequestEvent) error {
func recordUpdate(optFinalizer func(data any) error) func(e *core.RequestEvent) error {
return func(e *core.RequestEvent) error {
collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
if err != nil || collection == nil {
@@ -404,7 +404,7 @@ func recordUpdate(optFinalizer func() error) func(e *core.RequestEvent) error {
if optFinalizer != nil {
isOptFinalizerCalled = true
err = optFinalizer()
err = optFinalizer(e.Record)
if err != nil {
return firstApiError(err, e.InternalServerError("", fmt.Errorf("update optFinalizer error: %w", err)))
}
@@ -418,7 +418,7 @@ func recordUpdate(optFinalizer func() error) func(e *core.RequestEvent) error {
// e.g. in case the regular hook chain was stopped and the finalizer cannot be executed as part of the last e.Next() task
if !isOptFinalizerCalled && optFinalizer != nil {
if err := optFinalizer(); err != nil {
if err := optFinalizer(event.Record); err != nil {
return firstApiError(err, e.InternalServerError("", fmt.Errorf("update optFinalizer error: %w", err)))
}
}
@@ -427,7 +427,7 @@ func recordUpdate(optFinalizer func() error) func(e *core.RequestEvent) error {
}
}
func recordDelete(optFinalizer func() error) func(e *core.RequestEvent) error {
func recordDelete(optFinalizer func(data any) error) func(e *core.RequestEvent) error {
return func(e *core.RequestEvent) error {
collection, err := e.App.FindCachedCollectionByNameOrId(e.Request.PathValue("collection"))
if err != nil || collection == nil {
@@ -494,7 +494,7 @@ func recordDelete(optFinalizer func() error) func(e *core.RequestEvent) error {
if optFinalizer != nil {
isOptFinalizerCalled = true
err = optFinalizer()
err = optFinalizer(e.Record)
if err != nil {
return firstApiError(err, e.InternalServerError("", fmt.Errorf("delete optFinalizer error: %w", err)))
}
@@ -508,7 +508,7 @@ func recordDelete(optFinalizer func() error) func(e *core.RequestEvent) error {
// e.g. in case the regular hook chain was stopped and the finalizer cannot be executed as part of the last e.Next() task
if !isOptFinalizerCalled && optFinalizer != nil {
if err := optFinalizer(); err != nil {
if err := optFinalizer(event.Record); err != nil {
return firstApiError(err, e.InternalServerError("", fmt.Errorf("delete optFinalizer error: %w", err)))
}
}
+19 -2
View File
@@ -170,7 +170,7 @@ func TestRecordCrudMFADelete(t *testing.T) {
t.Fatal(err)
}
},
ExpectedStatus: 404,
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
@@ -187,7 +187,7 @@ func TestRecordCrudMFADelete(t *testing.T) {
t.Fatal(err)
}
},
ExpectedStatus: 404,
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
@@ -204,6 +204,23 @@ func TestRecordCrudMFADelete(t *testing.T) {
t.Fatal(err)
}
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "superusers auth",
Method: http.MethodDelete,
URL: "/api/collections/" + core.CollectionNameMFAs + "/records/user1_0",
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
if err := tests.StubMFARecords(app); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
+21 -4
View File
@@ -170,12 +170,12 @@ func TestRecordCrudOTPDelete(t *testing.T) {
t.Fatal(err)
}
},
ExpectedStatus: 404,
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "non-owner",
Name: "non-owner auth",
Method: http.MethodDelete,
URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0",
Headers: map[string]string{
@@ -187,12 +187,12 @@ func TestRecordCrudOTPDelete(t *testing.T) {
t.Fatal(err)
}
},
ExpectedStatus: 404,
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "owner",
Name: "owner regular auth",
Method: http.MethodDelete,
URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0",
Headers: map[string]string{
@@ -204,6 +204,23 @@ func TestRecordCrudOTPDelete(t *testing.T) {
t.Fatal(err)
}
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "superusers auth",
Method: http.MethodDelete,
URL: "/api/collections/" + core.CollectionNameOTPs + "/records/user1_0",
Headers: map[string]string{
// superusers, test@example.com
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiY18zMzIzODY2MzM5IiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.v_bMAygr6hXPwD2DpPrFpNQ7dd68Q3pGstmYAsvNBJg",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
if err := tests.StubOTPRecords(app); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
+4 -4
View File
@@ -256,13 +256,13 @@ func EnrichRecords(e *core.RequestEvent, records []*core.Record, defaultExpands
return triggerRecordEnrichHooks(e.App, info, records, func() error {
expands := defaultExpands
if param := e.Request.URL.Query().Get(expandQueryParam); param != "" {
if param := info.Query[expandQueryParam]; param != "" {
expands = append(expands, strings.Split(param, ",")...)
}
err := defaultEnrichRecords(e.App, info, records, expands...)
if err != nil {
// only log as it is not critical
// only log because it is not critical
e.App.Logger().Warn("failed to apply default enriching", "error", err)
}
@@ -270,8 +270,6 @@ func EnrichRecords(e *core.RequestEvent, records []*core.Record, defaultExpands
})
}
var iterate func(record *core.Record) error
type iterator[T any] struct {
items []T
index int
@@ -297,6 +295,7 @@ func triggerRecordEnrichHooks(app core.App, requestInfo *core.RequestInfo, recor
event.App = app
event.RequestInfo = requestInfo
var iterate func(record *core.Record) error
iterate = func(record *core.Record) error {
if record == nil {
return nil
@@ -350,6 +349,7 @@ func defaultEnrichRecords(app core.App, requestInfo *core.RequestInfo, records [
// expandFetch is the records fetch function that is used to expand related records.
func expandFetch(app core.App, originalRequestInfo *core.RequestInfo) core.ExpandFetchFunc {
// shallow clone the provided request info to set an "expand" context
requestInfoClone := *originalRequestInfo
requestInfoPtr := &requestInfoClone
requestInfoPtr.Context = core.RequestInfoContextExpand
+18 -9
View File
@@ -23,6 +23,14 @@ func TestEnrichRecords(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
freshRecords := func(records []*core.Record) []*core.Record {
result := make([]*core.Record, len(records))
for i, r := range records {
result[i] = r.Fresh()
}
return result
}
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
@@ -77,7 +85,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[emailVisibility] guest",
auth: nil,
records: usersRecords,
records: freshRecords(usersRecords),
queryExpand: "",
defaultExpands: nil,
expected: []string{
@@ -91,7 +99,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[emailVisibility] owner",
auth: user,
records: usersRecords,
records: freshRecords(usersRecords),
queryExpand: "",
defaultExpands: nil,
expected: []string{
@@ -103,7 +111,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[emailVisibility] manager",
auth: user,
records: nologinRecords,
records: freshRecords(nologinRecords),
queryExpand: "",
defaultExpands: nil,
expected: []string{
@@ -115,7 +123,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[emailVisibility] superuser",
auth: superuser,
records: nologinRecords,
records: freshRecords(nologinRecords),
queryExpand: "",
defaultExpands: nil,
expected: []string{
@@ -127,7 +135,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[emailVisibility + expand] recursive auth rule checks (regular user)",
auth: user,
records: demo1Records,
records: freshRecords(demo1Records),
queryExpand: "",
defaultExpands: []string{"rel_many"},
expected: []string{
@@ -144,7 +152,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[emailVisibility + expand] recursive auth rule checks (superuser)",
auth: superuser,
records: demo1Records,
records: freshRecords(demo1Records),
queryExpand: "",
defaultExpands: []string{"rel_many"},
expected: []string{
@@ -152,6 +160,7 @@ func TestEnrichRecords(t *testing.T) {
`"test@example.com"`,
`"expand":{"rel_many"`,
`"id":"bgs820n361vj1qd"`,
`"id":"4q1xlclmfloku33"`,
`"id":"oap640cot4yru2s"`,
},
notExpected: []string{
@@ -163,7 +172,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[expand] guest (query)",
auth: nil,
records: usersRecords,
records: freshRecords(usersRecords),
queryExpand: "rel",
defaultExpands: nil,
expected: []string{
@@ -179,7 +188,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[expand] guest (default expands)",
auth: nil,
records: usersRecords,
records: freshRecords(usersRecords),
queryExpand: "",
defaultExpands: []string{"rel"},
expected: []string{
@@ -192,7 +201,7 @@ func TestEnrichRecords(t *testing.T) {
{
name: "[expand] @request.context=expand check",
auth: nil,
records: demo5Records,
records: freshRecords(demo5Records),
queryExpand: "rel_one",
defaultExpands: []string{"rel_many"},
expected: []string{
+9 -9
View File
@@ -83,21 +83,17 @@ func Serve(app core.App, config ServeConfig) error {
return err
}
pbRouter.Bind(&hook.Handler[*core.RequestEvent]{
Id: DefaultCorsMiddlewareId,
Func: CORSWithConfig(CORSConfig{
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}),
Priority: DefaultCorsMiddlewarePriority,
})
pbRouter.Bind(CORSWithConfig(CORSConfig{
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
pbRouter.BindFunc(installerRedirect(app, config.DashboardPath))
pbRouter.GET(config.DashboardPath, Static(ui.DistDirFS, false)).
BindFunc(dashboardRemoveInstallerParam()).
BindFunc(dashboardCacheControl()).
BindFunc(Gzip())
Bind(Gzip())
// start http server
// ---
@@ -240,6 +236,10 @@ func Serve(app core.App, config ServeConfig) error {
return serveHookErr
}
if listener == nil {
return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
}
if config.ShowStartBanner {
schema := "http"
addr := server.Addr
+71 -63
View File
@@ -596,7 +596,7 @@ type App interface {
// CanAccessRecord checks if a record is allowed to be accessed by the
// specified requestInfo and accessRule.
//
// Rule and db checks are ignored in case requestInfo.AuthRecord is a superuser.
// Rule and db checks are ignored in case requestInfo.Auth is a superuser.
//
// The returned error indicate that something unexpected happened during
// the check (eg. invalid rule or db query error).
@@ -633,17 +633,19 @@ type App interface {
// App event hooks
// ---------------------------------------------------------------
// OnBootstrap hook is triggered on initializing the main application
// OnBootstrap hook is triggered when initializing the main application
// resources (db, app settings, etc).
OnBootstrap() *hook.Hook[*BootstrapEvent]
// OnServe hook is triggered on when the app web server is started
// (after starting the tcp listener but before initializing the blocking serve task),
// OnServe hook is triggered when the app web server is started
// (after starting the TCP listener but before initializing the blocking serve task),
// allowing you to adjust its options and attach new routes or middlewares.
OnServe() *hook.Hook[*ServeEvent]
// OnTerminate hook is triggered when the app is in the process
// of being terminated (ex. on SIGTERM signal).
//
// Note that the app could be terminated abruptly without awaiting the hook completion.
OnTerminate() *hook.Hook[*TerminateEvent]
// OnBackupCreate hook is triggered on each [App.CreateBackup] call.
@@ -661,6 +663,9 @@ type App interface {
// OnModelValidate is triggered every time when a model is being validated
// (e.g. triggered by App.Validate() or App.Save()).
//
// For convenience, if you want to listen to only the Record models
// events without doing manual type assertion, you can attach to the OnRecord* proxy hooks.
//
// If the optional "tags" list (Collection id/name, Model table name, etc.) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
@@ -680,7 +685,7 @@ type App interface {
// Note that successful execution doesn't guarantee that the model
// is persisted in the database since its wrapping transaction may
// not have been committed yet.
// If you wan to listen to only the actual persisted events, you can
// If you want to listen to only the actual persisted events, you can
// bind to [OnModelAfterCreateSuccess] or [OnModelAfterCreateError] hooks.
//
// For convenience, if you want to listen to only the Record models
@@ -703,7 +708,7 @@ type App interface {
// Note that successful execution doesn't guarantee that the model
// is persisted in the database since its wrapping transaction may have been
// committed yet.
// If you wan to listen to only the actual persisted events,
// If you want to listen to only the actual persisted events,
// you can bind to [OnModelAfterCreateSuccess] or [OnModelAfterCreateError] hooks.
//
// For convenience, if you want to listen to only the Record models
@@ -718,7 +723,7 @@ type App interface {
// Model DB create persistence.
//
// Note that when a Model is persisted as part of a transaction,
// this hook is triggered AFTER the transaction has been committed.
// this hook is delayed and executed only AFTER the transaction has been committed.
// This hook is NOT triggered in case the transaction rollbacks
// (aka. when the model wasn't persisted).
//
@@ -732,10 +737,11 @@ type App interface {
// OnModelAfterCreateError is triggered after each failed
// Model DB create persistence.
// Note that when a Model is persisted as part of a transaction,
// this hook is triggered in one of the following cases:
// - immediately after App.Save() failure
// - on transaction rollback
//
// Note that the execution of this hook is either immediate or delayed
// depending on the error:
// - "immediate" on App.Save() failure
// - "delayed" on transaction rollback
//
// For convenience, if you want to listen to only the Record models
// events without doing manual type assertion, you can attach to the OnRecord* proxy hooks.
@@ -759,7 +765,7 @@ type App interface {
// Note that successful execution doesn't guarantee that the model
// is persisted in the database since its wrapping transaction may
// not have been committed yet.
// If you wan to listen to only the actual persisted events, you can
// If you want to listen to only the actual persisted events, you can
// bind to [OnModelAfterUpdateSuccess] or [OnModelAfterUpdateError] hooks.
//
// For convenience, if you want to listen to only the Record models
@@ -782,7 +788,7 @@ type App interface {
// Note that successful execution doesn't guarantee that the model
// is persisted in the database since its wrapping transaction may have been
// committed yet.
// If you wan to listen to only the actual persisted events,
// If you want to listen to only the actual persisted events,
// you can bind to [OnModelAfterUpdateSuccess] or [OnModelAfterUpdateError] hooks.
//
// For convenience, if you want to listen to only the Record models
@@ -797,7 +803,7 @@ type App interface {
// Model DB update persistence.
//
// Note that when a Model is persisted as part of a transaction,
// this hook is triggered AFTER the transaction has been committed.
// this hook is delayed and executed only AFTER the transaction has been committed.
// This hook is NOT triggered in case the transaction rollbacks
// (aka. when the model changes weren't persisted).
//
@@ -812,10 +818,10 @@ type App interface {
// OnModelAfterUpdateError is triggered after each failed
// Model DB update persistence.
//
// Note that when a Model is persisted as part of a transaction,
// this hook is triggered in one of the following cases:
// - immediately after App.Save() failure
// - on transaction rollback
// Note that the execution of this hook is either immediate or delayed
// depending on the error:
// - "immediate" on App.Save() failure
// - "delayed" on transaction rollback
//
// For convenience, if you want to listen to only the Record models
// events without doing manual type assertion, you can attach to the OnRecord* proxy hooks.
@@ -833,7 +839,7 @@ type App interface {
// Note that successful execution doesn't guarantee that the model
// is deleted from the database since its wrapping transaction may
// not have been committed yet.
// If you wan to listen to only the actual persisted deleted events, you can
// If you want to listen to only the actual persisted deleted events, you can
// bind to [OnModelAfterDeleteSuccess] or [OnModelAfterDeleteError] hooks.
//
// For convenience, if you want to listen to only the Record models
@@ -856,7 +862,7 @@ type App interface {
// Note that successful execution doesn't guarantee that the model
// is deleted from the database since its wrapping transaction may
// not have been committed yet.
// If you wan to listen to only the actual persisted deleted events, you can
// If you want to listen to only the actual persisted deleted events, you can
// bind to [OnModelAfterDeleteSuccess] or [OnModelAfterDeleteError] hooks.
//
// For convenience, if you want to listen to only the Record models
@@ -871,7 +877,7 @@ type App interface {
// Model DB delete persistence.
//
// Note that when a Model is deleted as part of a transaction,
// this hook is triggered AFTER the transaction has been committed.
// this hook is delayed and executed only AFTER the transaction has been committed.
// This hook is NOT triggered in case the transaction rollbacks
// (aka. when the model delete wasn't persisted).
//
@@ -886,10 +892,10 @@ type App interface {
// OnModelAfterDeleteError is triggered after each failed
// Model DB delete persistence.
//
// Note that when a Model is deleted as part of a transaction,
// this hook is triggered in one of the following cases:
// - immediately after App.Delete() failure
// - on transaction rollback
// Note that the execution of this hook is either immediate or delayed
// depending on the error:
// - "immediate" on App.Delete() failure
// - "delayed" on transaction rollback
//
// For convenience, if you want to listen to only the Record models
// events without doing manual type assertion, you can attach to the OnRecord* proxy hooks.
@@ -904,10 +910,9 @@ type App interface {
// ---------------------------------------------------------------
// OnRecordEnrich is triggered every time when a record is enriched
// (during realtime message seriazation, as part of the builtin Record
// responses, or when [apis.EnrichRecord] is invoked).
// (as part of the builtin Record responses, during realtime message seriazation, or when [apis.EnrichRecord] is invoked).
//
// It could be used for example to redact/hide or add computed temp
// It could be used for example to redact/hide or add computed temporary
// Record model props only for the specific request info. For example:
//
// app.OnRecordEnrich("posts").BindFunc(func(e core.*RecordEnrichEvent) {
@@ -928,7 +933,7 @@ type App interface {
// triggered and called only if their event data origin matches the tags.
OnRecordEnrich(tags ...string) *hook.TaggedHook[*RecordEnrichEvent]
// OnRecordValidate is a proxy Record model hook for [OnModelValidate].
// OnRecordValidate is a Record proxy model hook of [OnModelValidate].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -937,28 +942,28 @@ type App interface {
// ---------------------------------------------------------------
// OnRecordCreate is a proxy Record model hook for [OnModelCreate].
// OnRecordCreate is a Record proxy model hook of [OnModelCreate].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordCreate(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordCreateExecute is a proxy Record model hook for [OnModelCreateExecute].
// OnRecordCreateExecute is a Record proxy model hook of [OnModelCreateExecute].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordCreateExecute(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordAfterCreateSuccess is a proxy Record model hook for [OnModelAfterCreateSuccess].
// OnRecordAfterCreateSuccess is a Record proxy model hook of [OnModelAfterCreateSuccess].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordAfterCreateSuccess(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordAfterCreateError is a proxy Record model hook for [OnModelAfterCreateError].
// OnRecordAfterCreateError is a Record proxy model hook of [OnModelAfterCreateError].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -967,28 +972,28 @@ type App interface {
// ---------------------------------------------------------------
// OnRecordUpdate is a proxy Record model hook for [OnModelUpdate].
// OnRecordUpdate is a Record proxy model hook of [OnModelUpdate].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordUpdate(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordUpdateExecute is a proxy Record model hook for [OnModelUpdateExecute].
// OnRecordUpdateExecute is a Record proxy model hook of [OnModelUpdateExecute].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordUpdateExecute(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordAfterUpdateSuccess is a proxy Record model hook for [OnModelAfterUpdateSuccess].
// OnRecordAfterUpdateSuccess is a Record proxy model hook of [OnModelAfterUpdateSuccess].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordAfterUpdateError is a proxy Record model hook for [OnModelAfterUpdateError].
// OnRecordAfterUpdateError is a Record proxy model hook of [OnModelAfterUpdateError].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -997,28 +1002,28 @@ type App interface {
// ---------------------------------------------------------------
// OnRecordDelete is a proxy Record model hook for [OnModelDelete].
// OnRecordDelete is a Record proxy model hook of [OnModelDelete].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordDelete(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordDeleteExecute is a proxy Record model hook for [OnModelDeleteExecute].
// OnRecordDeleteExecute is a Record proxy model hook of [OnModelDeleteExecute].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordDeleteExecute(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordAfterDeleteSuccess is a proxy Record model hook for [OnModelAfterDeleteSuccess].
// OnRecordAfterDeleteSuccess is a Record proxy model hook of [OnModelAfterDeleteSuccess].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnRecordAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*RecordEvent]
// OnRecordAfterDeleteError is a proxy Record model hook for [OnModelAfterDeleteError].
// OnRecordAfterDeleteError is a Record proxy model hook of [OnModelAfterDeleteError].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -1029,7 +1034,7 @@ type App interface {
// Collection models event hooks
// ---------------------------------------------------------------
// OnCollectionValidate is a proxy Collection model hook for [OnModelValidate].
// OnCollectionValidate is a Collection proxy model hook of [OnModelValidate].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -1038,28 +1043,28 @@ type App interface {
// ---------------------------------------------------------------
// OnCollectionCreate is a proxy Collection model hook for [OnModelCreate].
// OnCollectionCreate is a Collection proxy model hook of [OnModelCreate].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionCreate(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionCreateExecute is a proxy Collection model hook for [OnModelCreateExecute].
// OnCollectionCreateExecute is a Collection proxy model hook of [OnModelCreateExecute].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionCreateExecute(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionAfterCreateSuccess is a proxy Collection model hook for [OnModelAfterCreateSuccess].
// OnCollectionAfterCreateSuccess is a Collection proxy model hook of [OnModelAfterCreateSuccess].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionAfterCreateSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionAfterCreateError is a proxy Collection model hook for [OnModelAfterCreateError].
// OnCollectionAfterCreateError is a Collection proxy model hook of [OnModelAfterCreateError].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -1068,28 +1073,28 @@ type App interface {
// ---------------------------------------------------------------
// OnCollectionUpdate is a proxy Collection model hook for [OnModelUpdate].
// OnCollectionUpdate is a Collection proxy model hook of [OnModelUpdate].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionUpdate(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionUpdateExecute is a proxy Collection model hook for [OnModelUpdateExecute].
// OnCollectionUpdateExecute is a Collection proxy model hook of [OnModelUpdateExecute].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionUpdateExecute(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionAfterUpdateSuccess is a proxy Collection model hook for [OnModelAfterUpdateSuccess].
// OnCollectionAfterUpdateSuccess is a Collection proxy model hook of [OnModelAfterUpdateSuccess].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionAfterUpdateError is a proxy Collection model hook for [OnModelAfterUpdateError].
// OnCollectionAfterUpdateError is a Collection proxy model hook of [OnModelAfterUpdateError].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -1098,28 +1103,28 @@ type App interface {
// ---------------------------------------------------------------
// OnCollectionDelete is a proxy Collection model hook for [OnModelDelete].
// OnCollectionDelete is a Collection proxy model hook of [OnModelDelete].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionDelete(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionDeleteExecute is a proxy Collection model hook for [OnModelDeleteExecute].
// OnCollectionDeleteExecute is a Collection proxy model hook of [OnModelDeleteExecute].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionDeleteExecute(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionAfterDeleteSuccess is a proxy Collection model hook for [OnModelAfterDeleteSuccess].
// OnCollectionAfterDeleteSuccess is a Collection proxy model hook of [OnModelAfterDeleteSuccess].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnCollectionAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent]
// OnCollectionAfterDeleteError is a proxy Collection model hook for [OnModelAfterDeleteError].
// OnCollectionAfterDeleteError is a Collection proxy model hook of [OnModelAfterDeleteError].
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -1131,7 +1136,7 @@ type App interface {
// ---------------------------------------------------------------
// OnMailerSend hook is triggered every time when a new email is
// being send using the App.NewMailClient() instance.
// being send using the [App.NewMailClient()] instance.
//
// It allows intercepting the email message or to use a custom mailer client.
OnMailerSend() *hook.Hook[*MailerEvent]
@@ -1187,7 +1192,7 @@ type App interface {
// OnRealtimeConnectRequest hook is triggered when establishing the SSE client connection.
//
// Any execution after [e.Next()] of a hook handler happens after the client disconnects.
// Any execution after e.Next() of a hook handler happens after the client disconnects.
OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectRequestEvent]
// OnRealtimeMessageSend hook is triggered when sending an SSE message to a client.
@@ -1216,7 +1221,7 @@ type App interface {
// OnSettingsReload hook is triggered every time when the App.Settings()
// is being replaced with a new state.
//
// Calling App.Settings() after e.Next() should return the new state.
// Calling App.Settings() after e.Next() returns the new state.
OnSettingsReload() *hook.Hook[*SettingsReloadEvent]
// ---------------------------------------------------------------
@@ -1229,8 +1234,12 @@ type App interface {
// returning it to the client.
OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*FileDownloadRequestEvent]
// OnFileBeforeTokenRequest hook is triggered on each file token API request.
OnFileTokenRequest() *hook.Hook[*FileTokenRequestEvent]
// OnFileBeforeTokenRequest hook is triggered on each auth file token API request.
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
// triggered and called only if their event data origin matches the tags.
OnFileTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenRequestEvent]
// ---------------------------------------------------------------
// Record Auth API event hooks
@@ -1250,9 +1259,8 @@ type App interface {
// OnRecordAuthWithPasswordRequest hook is triggered on each
// Record auth with password API request.
//
// RecordAuthWithPasswordRequestEvent.Record could be nil if no
// matching identity is found, allowing you to manually locate a different
// Record model (by reassigning [RecordAuthWithPasswordRequestEvent.Record]).
// [RecordAuthWithPasswordRequestEvent.Record] could be nil if no matching identity is found, allowing
// you to manually locate a different Record model (by reassigning [RecordAuthWithPasswordRequestEvent.Record]).
//
// If the optional "tags" list (Collection ids or names) is specified,
// then all event handlers registered via the created hook will be
@@ -1262,7 +1270,7 @@ type App interface {
// OnRecordAuthWithOAuth2Request hook is triggered on each Record
// OAuth2 sign-in/sign-up API request (after token exchange and before external provider linking).
//
// If the [RecordAuthWithOAuth2RequestEvent.Record] is not set, then the OAuth2
// If [RecordAuthWithOAuth2RequestEvent.Record] is not set, then the OAuth2
// request will try to create a new auth Record.
//
// To assign or link a different existing record model you can
+5 -5
View File
@@ -202,7 +202,7 @@ func NewBaseApp(config BaseAppConfig) *BaseApp {
// apply config defaults
if app.config.DBConnect == nil {
app.config.DBConnect = dbConnect
app.config.DBConnect = DefaultDBConnect
}
if app.config.DataMaxOpenConns <= 0 {
app.config.DataMaxOpenConns = DefaultDataMaxOpenConns
@@ -335,13 +335,13 @@ func (app *BaseApp) initHooks() {
app.onBatchRequest = &hook.Hook[*BatchRequestEvent]{}
}
// @todo consider caching the created instance?
//
// UnsafeWithoutHooks returns a shallow copy of the current app WITHOUT any registered hooks.
//
// NB! Note that using the returned app instance may cause data integrity errors
// since the Record validations and data normalizations (including files uploads)
// rely on the app hooks to work.
//
// @todo consider caching the created instance?
func (app *BaseApp) UnsafeWithoutHooks() App {
clone := *app
@@ -981,8 +981,8 @@ func (app *BaseApp) OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*File
return hook.NewTaggedHook(app.onFileDownloadRequest, tags...)
}
func (app *BaseApp) OnFileTokenRequest() *hook.Hook[*FileTokenRequestEvent] {
return app.onFileTokenRequest
func (app *BaseApp) OnFileTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenRequestEvent] {
return hook.NewTaggedHook(app.onFileTokenRequest, tags...)
}
// -------------------------------------------------------------------
+91 -51
View File
@@ -265,7 +265,7 @@ func (app *BaseApp) registerCollectionHooks() {
// ---
onErrorReloadCachedCollections := func(ce *CollectionErrorEvent) error {
if err := ce.App.ReloadCachedCollections(); err != nil {
ce.App.Logger().Warn("Failed to reload collections cache", "error", err)
ce.App.Logger().Warn("Failed to reload collections cache after collection change error", "error", err)
}
return ce.Next()
@@ -325,9 +325,12 @@ type baseCollection struct {
Type string `db:"type" json:"type" form:"type"`
Fields FieldsList `db:"fields" json:"fields" form:"fields"`
Indexes types.JSONArray[string] `db:"indexes" json:"indexes" form:"indexes"`
System bool `db:"system" json:"system" form:"system"`
Created types.DateTime `db:"created" json:"created"`
Updated types.DateTime `db:"updated" json:"updated"`
// System prevents the collection rename, deletion and rules change.
// It is used primarily for internal purposes for collections like "_superusers", "_externalAuths", etc.
System bool `db:"system" json:"system" form:"system"`
}
// Collection defines the table, fields and various options related to a set of records.
@@ -338,45 +341,75 @@ type Collection struct {
}
// NewCollection initializes and returns a new Collection model with the specified type and name.
func NewCollection(typ, name string) *Collection {
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewCollection(typ, name string, optId ...string) *Collection {
switch typ {
case CollectionTypeAuth:
return NewAuthCollection(name)
return NewAuthCollection(name, optId...)
case CollectionTypeView:
return NewViewCollection(name)
return NewViewCollection(name, optId...)
default:
return NewBaseCollection(name)
return NewBaseCollection(name, optId...)
}
}
// NewBaseCollection initializes and returns a new "base" Collection model.
func NewBaseCollection(name string) *Collection {
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewBaseCollection(name string, optId ...string) *Collection {
m := &Collection{}
m.Name = name
m.Type = CollectionTypeBase
if len(optId) > 0 {
m.Id = optId[0]
}
m.initDefaultId()
m.initDefaultFields()
return m
}
// NewViewCollection initializes and returns a new "view" Collection model.
func NewViewCollection(name string) *Collection {
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewViewCollection(name string, optId ...string) *Collection {
m := &Collection{}
m.Name = name
m.Type = CollectionTypeView
if len(optId) > 0 {
m.Id = optId[0]
}
m.initDefaultId()
m.initDefaultFields()
return m
}
// NewAuthCollection initializes and returns a new "auth" Collection model.
func NewAuthCollection(name string) *Collection {
//
// It also loads the minimal default configuration for the collection
// (eg. system fields, indexes, type specific options, etc.).
func NewAuthCollection(name string, optId ...string) *Collection {
m := &Collection{}
m.Name = name
m.Type = CollectionTypeAuth
if len(optId) > 0 {
m.Id = optId[0]
}
m.initDefaultId()
m.initDefaultFields()
m.setDefaultAuthOptions()
return m
}
@@ -657,8 +690,15 @@ func onCollectionDeleteExecute(e *CollectionEvent) error {
// -------------------------------------------------------------------
func (c *Collection) initDefaultId() {
if c.Id == "" && c.Name != "" {
c.Id = "_pbc_" + crc32Checksum(c.Name)
if c.Id != "" {
return // already set
}
if c.System && c.Name != "" {
// for system collections we use crc32 checksum for consistency because they cannot be renamed
c.Id = "pbc_" + crc32Checksum(c.Name)
} else {
c.Id = "pbc_" + security.RandomStringWithAlphabet(11, DefaultIdAlphabet)
}
}
@@ -783,24 +823,24 @@ func onCollectionSaveExecute(e *CollectionEvent) error {
return nil
}
func (m *Collection) initDefaultFields() {
switch m.Type {
func (c *Collection) initDefaultFields() {
switch c.Type {
case CollectionTypeBase:
m.initIdField()
c.initIdField()
case CollectionTypeAuth:
m.initIdField()
m.initPasswordField()
m.initTokenKeyField()
m.initEmailField()
m.initEmailVisibilityField()
m.initVerifiedField()
c.initIdField()
c.initPasswordField()
c.initTokenKeyField()
c.initEmailField()
c.initEmailVisibilityField()
c.initVerifiedField()
case CollectionTypeView:
// view fields are autogenerated
}
}
func (m *Collection) initIdField() {
field, _ := m.Fields.GetByName(FieldNameId).(*TextField)
func (c *Collection) initIdField() {
field, _ := c.Fields.GetByName(FieldNameId).(*TextField)
if field == nil {
// create default field
field = &TextField{
@@ -815,7 +855,7 @@ func (m *Collection) initIdField() {
}
// prepend it
m.Fields = NewFieldsList(append([]Field{field}, m.Fields...)...)
c.Fields = NewFieldsList(append([]Field{field}, c.Fields...)...)
} else {
// enforce system defaults
field.System = true
@@ -825,11 +865,11 @@ func (m *Collection) initIdField() {
}
}
func (m *Collection) initPasswordField() {
field, _ := m.Fields.GetByName(FieldNamePassword).(*PasswordField)
func (c *Collection) initPasswordField() {
field, _ := c.Fields.GetByName(FieldNamePassword).(*PasswordField)
if field == nil {
// load default field
m.Fields.Add(&PasswordField{
c.Fields.Add(&PasswordField{
Name: FieldNamePassword,
System: true,
Hidden: true,
@@ -844,11 +884,11 @@ func (m *Collection) initPasswordField() {
}
}
func (m *Collection) initTokenKeyField() {
field, _ := m.Fields.GetByName(FieldNameTokenKey).(*TextField)
func (c *Collection) initTokenKeyField() {
field, _ := c.Fields.GetByName(FieldNameTokenKey).(*TextField)
if field == nil {
// load default field
m.Fields.Add(&TextField{
c.Fields.Add(&TextField{
Name: FieldNameTokenKey,
System: true,
Hidden: true,
@@ -865,21 +905,21 @@ func (m *Collection) initTokenKeyField() {
}
// ensure that there is a unique index for the field
if !dbutils.HasSingleColumnUniqueIndex(FieldNameTokenKey, m.Indexes) {
m.Indexes = append(m.Indexes, fmt.Sprintf(
if !dbutils.HasSingleColumnUniqueIndex(FieldNameTokenKey, c.Indexes) {
c.Indexes = append(c.Indexes, fmt.Sprintf(
"CREATE UNIQUE INDEX `%s` ON `%s` (`%s`)",
m.fieldIndexName(FieldNameTokenKey),
m.Name,
c.fieldIndexName(FieldNameTokenKey),
c.Name,
FieldNameTokenKey,
))
}
}
func (m *Collection) initEmailField() {
field, _ := m.Fields.GetByName(FieldNameEmail).(*EmailField)
func (c *Collection) initEmailField() {
field, _ := c.Fields.GetByName(FieldNameEmail).(*EmailField)
if field == nil {
// load default field
m.Fields.Add(&EmailField{
c.Fields.Add(&EmailField{
Name: FieldNameEmail,
System: true,
Required: true,
@@ -891,22 +931,22 @@ func (m *Collection) initEmailField() {
}
// ensure that there is a unique index for the email field
if !dbutils.HasSingleColumnUniqueIndex(FieldNameEmail, m.Indexes) {
m.Indexes = append(m.Indexes, fmt.Sprintf(
if !dbutils.HasSingleColumnUniqueIndex(FieldNameEmail, c.Indexes) {
c.Indexes = append(c.Indexes, fmt.Sprintf(
"CREATE UNIQUE INDEX `%s` ON `%s` (`%s`) WHERE `%s` != ''",
m.fieldIndexName(FieldNameEmail),
m.Name,
c.fieldIndexName(FieldNameEmail),
c.Name,
FieldNameEmail,
FieldNameEmail,
))
}
}
func (m *Collection) initEmailVisibilityField() {
field, _ := m.Fields.GetByName(FieldNameEmailVisibility).(*BoolField)
func (c *Collection) initEmailVisibilityField() {
field, _ := c.Fields.GetByName(FieldNameEmailVisibility).(*BoolField)
if field == nil {
// load default field
m.Fields.Add(&BoolField{
c.Fields.Add(&BoolField{
Name: FieldNameEmailVisibility,
System: true,
})
@@ -916,11 +956,11 @@ func (m *Collection) initEmailVisibilityField() {
}
}
func (m *Collection) initVerifiedField() {
field, _ := m.Fields.GetByName(FieldNameVerified).(*BoolField)
func (c *Collection) initVerifiedField() {
field, _ := c.Fields.GetByName(FieldNameVerified).(*BoolField)
if field == nil {
// load default field
m.Fields.Add(&BoolField{
c.Fields.Add(&BoolField{
Name: FieldNameVerified,
System: true,
})
@@ -930,13 +970,13 @@ func (m *Collection) initVerifiedField() {
}
}
func (m *Collection) fieldIndexName(field string) string {
func (c *Collection) fieldIndexName(field string) string {
name := "idx_" + field + "_"
if m.Id != "" {
name += m.Id
} else if m.Name != "" {
name += m.Name
if c.Id != "" {
name += c.Id
} else if c.Name != "" {
name += c.Name
} else {
name += security.PseudorandomString(10)
}
+16 -8
View File
@@ -122,10 +122,13 @@ type collectionAuthOptions struct {
// and which OAuth2 providers are allowed.
OAuth2 OAuth2Config `form:"oauth2" json:"oauth2"`
// PasswordAuth defines options related to the collection password authentication.
PasswordAuth PasswordAuthConfig `form:"passwordAuth" json:"passwordAuth"`
// MFA defines options related to the Multi-factor authentication (MFA).
MFA MFAConfig `form:"mfa" json:"mfa"`
// OTP defines options related to the One-time password authentication (OTP).
OTP OTPConfig `form:"otp" json:"otp"`
// Various token configurations
@@ -136,7 +139,7 @@ type collectionAuthOptions struct {
VerificationToken TokenConfig `form:"verificationToken" json:"verificationToken"`
FileToken TokenConfig `form:"fileToken" json:"fileToken"`
// default email templates
// Default email templates
// ---
VerificationTemplate EmailTemplate `form:"verificationTemplate" json:"verificationTemplate"`
ResetPasswordTemplate EmailTemplate `form:"resetPasswordTemplate" json:"resetPasswordTemplate"`
@@ -461,13 +464,14 @@ type OAuth2ProviderConfig struct {
// (https://github.com/pocketbase/pocketbase/discussions/3799#discussioncomment-7640312)
PKCE *bool `form:"pkce" json:"pkce"`
Name string `form:"name" json:"name"`
ClientId string `form:"clientId" json:"clientId"`
ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"`
AuthURL string `form:"authURL" json:"authURL"`
TokenURL string `form:"tokenURL" json:"tokenURL"`
UserInfoURL string `form:"userInfoURL" json:"userInfoURL"`
DisplayName string `form:"displayName" json:"displayName"`
Name string `form:"name" json:"name"`
ClientId string `form:"clientId" json:"clientId"`
ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"`
AuthURL string `form:"authURL" json:"authURL"`
TokenURL string `form:"tokenURL" json:"tokenURL"`
UserInfoURL string `form:"userInfoURL" json:"userInfoURL"`
DisplayName string `form:"displayName" json:"displayName"`
Extra map[string]any `form:"extra" json:"extra"`
}
// Validate makes OAuth2ProviderConfig validatable by implementing [validation.Validatable] interface.
@@ -531,5 +535,9 @@ func (c OAuth2ProviderConfig) InitProvider() (auth.Provider, error) {
provider.SetPKCE(*c.PKCE)
}
if c.Extra != nil {
provider.SetExtra(c.Extra)
}
return provider, nil
}
@@ -1,6 +1,8 @@
package core_test
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
@@ -942,6 +944,7 @@ func TestOAuth2ProviderConfigInitProvider(t *testing.T) {
UserInfoURL: "test_UserInfoURL",
DisplayName: "test_DisplayName",
PKCE: types.Pointer(true),
Extra: map[string]any{"a": 1},
},
core.OAuth2ProviderConfig{
Name: "gitlab",
@@ -952,6 +955,7 @@ func TestOAuth2ProviderConfigInitProvider(t *testing.T) {
UserInfoURL: "test_UserInfoURL",
DisplayName: "test_DisplayName",
PKCE: types.Pointer(true),
Extra: map[string]any{"a": 1},
},
false,
},
@@ -1011,6 +1015,12 @@ func TestOAuth2ProviderConfigInitProvider(t *testing.T) {
if provider.PKCE() != *s.expectedConfig.PKCE {
t.Fatalf("Expected PKCE %v, got %v", *s.expectedConfig.PKCE, provider.PKCE())
}
rawMeta, _ := json.Marshal(provider.Extra())
expectedMeta, _ := json.Marshal(s.expectedConfig.Extra)
if !bytes.Equal(rawMeta, expectedMeta) {
t.Fatalf("Expected PKCE %v, got %v", *s.expectedConfig.PKCE, provider.PKCE())
}
})
}
}
+88 -63
View File
@@ -26,7 +26,7 @@ func TestNewCollection(t *testing.T) {
"",
"",
[]string{
`"id":""`,
`"id":"pbc_`,
`"name":""`,
`"type":"base"`,
`"system":false`,
@@ -45,7 +45,7 @@ func TestNewCollection(t *testing.T) {
"unknown",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"base"`,
`"system":false`,
@@ -64,7 +64,7 @@ func TestNewCollection(t *testing.T) {
"base",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"base"`,
`"system":false`,
@@ -83,7 +83,7 @@ func TestNewCollection(t *testing.T) {
"view",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"view"`,
`"indexes":[]`,
@@ -100,7 +100,7 @@ func TestNewCollection(t *testing.T) {
"auth",
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"auth"`,
`"fields":[{`,
@@ -148,7 +148,7 @@ func TestNewBaseCollection(t *testing.T) {
{
"",
[]string{
`"id":""`,
`"id":"pbc_`,
`"name":""`,
`"type":"base"`,
`"system":false`,
@@ -166,7 +166,7 @@ func TestNewBaseCollection(t *testing.T) {
{
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"base"`,
`"system":false`,
@@ -206,7 +206,7 @@ func TestNewViewCollection(t *testing.T) {
{
"",
[]string{
`"id":""`,
`"id":"pbc_`,
`"name":""`,
`"type":"view"`,
`"indexes":[]`,
@@ -222,7 +222,7 @@ func TestNewViewCollection(t *testing.T) {
{
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"view"`,
`"indexes":[]`,
@@ -286,7 +286,7 @@ func TestNewAuthCollection(t *testing.T) {
{
"test",
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"auth"`,
`"fields":[{`,
@@ -498,10 +498,11 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
defer app.Cleanup()
scenarios := []struct {
name string
raw string
collection func() *core.Collection
expectedCollection func() *core.Collection
name string
raw string
collection func() *core.Collection
expected []string
notExpected []string
}{
{
"base new empty",
@@ -509,12 +510,18 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
func() *core.Collection {
return &core.Collection{}
},
func() *core.Collection {
c := core.NewBaseCollection("test")
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
[]string{
`"type":"base"`,
`"id":"pbc_`,
`"name":"test"`,
`"listRule":"1=2"`,
`"fields":[`,
`"name":"id"`,
`"indexes":[]`,
},
[]string{
`"authRule":"1=3"`,
`"viewQuery":"abc"`,
},
},
{
@@ -523,12 +530,17 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
func() *core.Collection {
return &core.Collection{}
},
func() *core.Collection {
c := core.NewViewCollection("test")
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
[]string{
`"type":"view"`,
`"id":"pbc_`,
`"name":"test"`,
`"listRule":"1=2"`,
`"fields":[]`,
`"viewQuery":"abc"`,
`"indexes":[]`,
},
[]string{
`"authRule":"1=3"`,
},
},
{
@@ -537,12 +549,18 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
func() *core.Collection {
return &core.Collection{}
},
func() *core.Collection {
c := core.NewAuthCollection("test")
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
[]string{
`"type":"auth"`,
`"id":"pbc_`,
`"name":"test"`,
`"listRule":"1=2"`,
`"authRule":"1=3"`,
`"fields":[`,
`"name":"id"`,
},
[]string{
`"indexes":[]`,
`"viewQuery":"abc"`,
},
},
{
@@ -553,14 +571,16 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
c.Type = core.CollectionTypeBase
return c
},
func() *core.Collection {
c := &core.Collection{}
c.Type = core.CollectionTypeBase
c.Name = "test"
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
[]string{
`"type":"base"`,
`"id":""`,
`"name":"test"`,
`"listRule":"1=2"`,
`"fields":[]`,
},
[]string{
`"authRule":"1=3"`,
`"viewQuery":"abc"`,
},
},
{
@@ -570,14 +590,17 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
c, _ := app.FindCollectionByNameOrId("demo1")
return c
},
func() *core.Collection {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Type = core.CollectionTypeAuth
c.Name = "test"
c.ListRule = types.Pointer("1=2")
c.AuthRule = types.Pointer("1=3")
c.ViewQuery = "abc"
return c
[]string{
`"type":"auth"`,
`"name":"test"`,
`"listRule":"1=2"`,
`"authRule":"1=3"`,
`"fields":[`,
`"name":"id"`,
},
[]string{
`"name":"tokenKey"`,
`"viewQuery":"abc"`,
},
},
}
@@ -597,14 +620,16 @@ func TestCollectionUnmarshalJSON(t *testing.T) {
}
rawResultStr := string(rawResult)
rawExpected, err := json.Marshal(s.expectedCollection())
if err != nil {
t.Fatal(err)
for _, part := range s.expected {
if !strings.Contains(rawResultStr, part) {
t.Fatalf("Missing expected %q in\n%v", part, rawResultStr)
}
}
rawExpectedStr := string(rawExpected)
if rawResultStr != rawExpectedStr {
t.Fatalf("Expected collection\n%s\ngot\n%s", rawExpectedStr, rawResultStr)
for _, part := range s.notExpected {
if strings.Contains(rawResultStr, part) {
t.Fatalf("Didn't expected %q in\n%v", part, rawResultStr)
}
}
})
}
@@ -630,7 +655,7 @@ func TestCollectionSerialize(t *testing.T) {
return c
},
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"base"`,
},
@@ -658,7 +683,7 @@ func TestCollectionSerialize(t *testing.T) {
return c
},
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"view"`,
`"viewQuery":"1=1"`,
@@ -686,7 +711,7 @@ func TestCollectionSerialize(t *testing.T) {
return c
},
[]string{
`"id":"_pbc_3632233996"`,
`"id":"pbc_`,
`"name":"test"`,
`"type":"auth"`,
`"oauth2":{`,
@@ -748,19 +773,19 @@ func TestCollectionDBExport(t *testing.T) {
}{
{
"unknown",
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"unknown","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"unknown","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
{
core.CollectionTypeBase,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"base","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"base","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
{
core.CollectionTypeView,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"viewQuery":"select 1"},"system":true,"type":"view","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"viewQuery":"select 1"},"system":true,"type":"view","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
{
core.CollectionTypeAuth,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","authAlert":{"enabled":false,"emailTemplate":{"subject":"","body":""}},"oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"passwordAuth":{"enabled":false,"identityFields":null},"mfa":{"enabled":false,"duration":0,"rule":""},"otp":{"enabled":false,"duration":0,"length":0,"emailTemplate":{"subject":"","body":""}},"authToken":{"duration":0},"passwordResetToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"resetPasswordTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","authAlert":{"enabled":false,"emailTemplate":{"subject":"","body":""}},"oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"passwordAuth":{"enabled":false,"identityFields":null},"mfa":{"enabled":false,"duration":0,"rule":""},"otp":{"enabled":false,"duration":0,"length":0,"emailTemplate":{"subject":"","body":""}},"authToken":{"duration":0},"passwordResetToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"resetPasswordTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
},
}
@@ -782,8 +807,8 @@ func TestCollectionDBExport(t *testing.T) {
c.Updated = date
c.Indexes = types.JSONArray[string]{"CREATE INDEX idx1 on test_name(id)", "CREATE INDEX idx2 on test_name(id)"}
c.ViewQuery = "select 1"
c.Fields.Add(&core.BoolField{Name: "f1", System: true})
c.Fields.Add(&core.BoolField{Name: "f2", Required: true})
c.Fields.Add(&core.BoolField{Id: "f1_id", Name: "f1", System: true})
c.Fields.Add(&core.BoolField{Id: "f2_id", Name: "f2", Required: true})
c.RawOptions = types.JSONRaw(`{"viewQuery": "select 2"}`) // should be ignored
result, err := c.DBExport(app)
+54 -36
View File
@@ -158,15 +158,6 @@ func normalizeSingleVsMultipleFieldChanges(app App, newCollection *Collection, o
}
return app.RunInTransaction(func(txApp App) error {
// temporary disable the schema error checks to prevent view and trigger errors
// when "altering" (aka. deleting and recreating) the non-normalized columns
if _, err := txApp.DB().NewQuery("PRAGMA writable_schema = ON").Execute(); err != nil {
return err
}
// executed with defer to make sure that the pragma is always reverted
// in case of an error and when nested transactions are used
defer txApp.DB().NewQuery("PRAGMA writable_schema = RESET").Execute()
for _, newField := range newCollection.Fields {
// allow to continue even if there is no old field for the cases
// when a new field is added and there are already inserted data
@@ -186,17 +177,42 @@ func normalizeSingleVsMultipleFieldChanges(app App, newCollection *Collection, o
continue // no change
}
// update the column definition by:
// 1. inserting a new column with the new definition
// 2. copy normalized values from the original column to the new one
// 3. drop the original column
// 4. rename the new column to the original column
// -------------------------------------------------------
// update the field column definition
// -------------------------------------------------------
originalName := newField.GetName()
tempName := "_" + newField.GetName() + security.PseudorandomString(5)
// temporary drop all views to prevent reference errors during the columns renaming
// (this is used as an "alternative" to the writable_schema PRAGMA)
views := []struct {
Name string `db:"name"`
SQL string `db:"sql"`
}{}
err := txApp.DB().Select("name", "sql").
From("sqlite_master").
AndWhere(dbx.NewExp("sql is not null")).
AndWhere(dbx.HashExp{"type": "view"}).
All(&views)
if err != nil {
return err
}
for _, view := range views {
err = txApp.DeleteView(view.Name)
if err != nil {
return err
}
}
_, err := txApp.DB().AddColumn(newCollection.Name, tempName, newField.ColumnType(txApp)).Execute()
originalName := newField.GetName()
oldTempName := "_" + newField.GetName() + security.PseudorandomString(5)
// rename temporary the original column to something else to allow inserting a new one in its place
_, err = txApp.DB().RenameColumn(newCollection.Name, originalName, oldTempName).Execute()
if err != nil {
return err
}
// reinsert the field column with the new type
_, err = txApp.DB().AddColumn(newCollection.Name, originalName, newField.ColumnType(txApp)).Execute()
if err != nil {
return err
}
@@ -220,12 +236,12 @@ func normalizeSingleVsMultipleFieldChanges(app App, newCollection *Collection, o
END
)`,
newCollection.Name,
tempName,
originalName,
originalName,
originalName,
originalName,
originalName,
oldTempName,
oldTempName,
oldTempName,
oldTempName,
oldTempName,
))
} else {
// multiple -> single (keep only the last element)
@@ -247,35 +263,37 @@ func normalizeSingleVsMultipleFieldChanges(app App, newCollection *Collection, o
END
)`,
newCollection.Name,
tempName,
originalName,
originalName,
originalName,
originalName,
originalName,
oldTempName,
oldTempName,
oldTempName,
oldTempName,
oldTempName,
))
}
// copy the normalized values
if _, err := copyQuery.Execute(); err != nil {
_, err = copyQuery.Execute()
if err != nil {
return err
}
// drop the original column
if _, err := txApp.DB().DropColumn(newCollection.Name, originalName).Execute(); err != nil {
_, err = txApp.DB().DropColumn(newCollection.Name, oldTempName).Execute()
if err != nil {
return err
}
// rename the new column back to the original
if _, err := txApp.DB().RenameColumn(newCollection.Name, tempName, originalName).Execute(); err != nil {
return err
// restore views
for _, view := range views {
_, err = txApp.DB().NewQuery(view.SQL).Execute()
if err != nil {
return err
}
}
}
// revert the pragma and reload the schema
_, revertErr := txApp.DB().NewQuery("PRAGMA writable_schema = RESET").Execute()
return revertErr
return nil
})
}
+24 -5
View File
@@ -115,6 +115,18 @@ func TestSyncRecordTableSchema(t *testing.T) {
}
}
func getTotalViews(app core.App) (int, error) {
var total int
err := app.DB().Select("count(*)").
From("sqlite_master").
AndWhere(dbx.NewExp("sql is not null")).
AndWhere(dbx.HashExp{"type": "view"}).
Row(&total)
return total, err
}
func TestSingleVsMultipleValuesNormalization(t *testing.T) {
t.Parallel()
@@ -126,6 +138,11 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
t.Fatal(err)
}
beforeTotalViews, err := getTotalViews(app)
if err != nil {
t.Fatal(err)
}
// mock field changes
collection.Fields.GetByName("select_one").(*core.SelectField).MaxSelect = 2
collection.Fields.GetByName("select_many").(*core.SelectField).MaxSelect = 1
@@ -146,11 +163,13 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
t.Fatal(err)
}
// ensures that the writable schema was reverted to its expected default
var writableSchema bool
app.DB().NewQuery("PRAGMA writable_schema").Row(&writableSchema)
if writableSchema == true {
t.Fatalf("Expected writable_schema to be OFF, got %v", writableSchema)
// ensure that the views were reinserted
afterTotalViews, err := getTotalViews(app)
if err != nil {
t.Fatal(err)
}
if afterTotalViews != beforeTotalViews {
t.Fatalf("Expected total views %d, got %d", beforeTotalViews, afterTotalViews)
}
// check whether the columns DEFAULT definition was updated
+45 -33
View File
@@ -416,6 +416,10 @@ func (validator *collectionValidator) ensureNoSystemFieldsChange(value any) erro
return validators.ErrUnsupportedValueType
}
if validator.original.IsNew() {
return nil // not an update
}
for _, oldField := range validator.original.Fields {
if !oldField.GetSystem() {
continue
@@ -516,7 +520,8 @@ func (cv *collectionValidator) checkIndexes(value any) error {
)
}
indexNames := make(map[string]struct{}, len(indexes))
duplicatedNames := make(map[string]struct{}, len(indexes))
duplicatedDefinitions := make(map[string]struct{}, len(indexes))
for i, rawIndex := range indexes {
parsed := dbutils.ParseIndex(rawIndex)
@@ -533,15 +538,15 @@ func (cv *collectionValidator) checkIndexes(value any) error {
}
}
_, isDuplicated := indexNames[strings.ToLower(parsed.IndexName)]
if isDuplicated {
if _, isDuplicated := duplicatedNames[strings.ToLower(parsed.IndexName)]; isDuplicated {
return validation.Errors{
strconv.Itoa(i): validation.NewError(
"validation_duplicated_index_name",
"The index name must be unique.",
"The index name already exists.",
),
}
}
duplicatedNames[strings.ToLower(parsed.IndexName)] = struct{}{}
// ensure that the index name is not used in another collection
var usedTblName string
@@ -558,9 +563,24 @@ func (cv *collectionValidator) checkIndexes(value any) error {
strconv.Itoa(i): validation.NewError(
"validation_existing_index_name",
"The index name is already used in "+usedTblName+" collection.",
).SetParams(map[string]any{"usedTableName": usedTblName}),
}
}
// reset non-important identifiers
parsed.SchemaName = "validator"
parsed.IndexName = "validator"
parsedDef := parsed.Build()
if _, isDuplicated := duplicatedDefinitions[parsedDef]; isDuplicated {
return validation.Errors{
strconv.Itoa(i): validation.NewError(
"validation_duplicated_index_definition",
"The index definition already exists.",
),
}
}
duplicatedDefinitions[parsedDef] = struct{}{}
// note: we don't check the index table name because it is always
// overwritten by the SyncRecordTableSchema to allow
@@ -573,15 +593,18 @@ func (cv *collectionValidator) checkIndexes(value any) error {
// ),
// }
// }
indexNames[strings.ToLower(parsed.IndexName)] = struct{}{}
}
// ensure that indexes on system fields are not deleted or changed
// ensure that unique indexes on system fields are not changed or removed
if !cv.original.IsNew() {
OLD_INDEXES_LOOP:
for _, oldIndex := range cv.original.Indexes {
oldParsed := dbutils.ParseIndex(oldIndex)
if !oldParsed.Unique {
continue
}
oldParsedStr := oldParsed.Build()
for _, column := range oldParsed.Columns {
for _, f := range cv.original.Fields {
@@ -589,36 +612,25 @@ func (cv *collectionValidator) checkIndexes(value any) error {
continue
}
var exists bool
for i, newIndex := range cv.new.Indexes {
var hasMatch bool
for _, newIndex := range cv.new.Indexes {
newParsed := dbutils.ParseIndex(newIndex)
if !strings.EqualFold(newParsed.IndexName, oldParsed.IndexName) {
// exclude the non-important identifiers from the check
newParsed.IndexName = oldParsed.IndexName
newParsed.TableName = oldParsed.TableName
if oldParsedStr == newParsed.Build() {
hasMatch = true
continue
}
// normalize table names of both indexes
oldParsed.TableName = "validator"
newParsed.TableName = "validator"
if oldParsed.Build() != newParsed.Build() {
return validation.Errors{
strconv.Itoa(i): validation.NewError(
"validation_system_index_change",
"Indexes on system fields cannot change.",
),
}
}
exists = true
break
}
if !exists {
if !hasMatch {
return validation.NewError(
"validation_missing_system_index",
fmt.Sprintf("Missing system index %q.", oldParsed.IndexName),
).SetParams(map[string]any{"name": oldParsed.IndexName})
"validation_unique_system_field_index_change",
fmt.Sprintf("Unique index definition on system fields (%q) cannot be changed.", f.GetName()),
).SetParams(map[string]any{"fieldName": f.GetName()})
}
continue OLD_INDEXES_LOOP
@@ -630,7 +642,7 @@ func (cv *collectionValidator) checkIndexes(value any) error {
// check for required indexes
//
// note: this is in case the indexes were removed manually when creating/importing new auth collections
// and technically is not necessary since on app.Save the missing index will be reinserted by the system collection hook
// and technically it is not necessary because on app.Save() the missing indexes will be reinserted by the system collection hook
if cv.new.IsAuth() {
requiredNames := []string{FieldNameTokenKey, FieldNameEmail}
for _, name := range requiredNames {
@@ -638,7 +650,7 @@ func (cv *collectionValidator) checkIndexes(value any) error {
return validation.NewError(
"validation_missing_required_unique_index",
`Missing required unique index for field "`+name+`".`,
)
).SetParams(map[string]any{"fieldName": name})
}
}
}
+12
View File
@@ -434,6 +434,18 @@ func TestCollectionValidate(t *testing.T) {
},
expectedErrors: []string{"indexes"},
},
{
name: "duplicated index definitions",
collection: func(app core.App) (*core.Collection, error) {
c, _ := app.FindCollectionByNameOrId("demo1")
c.Indexes = []string{
"create index idx_test_demo1 on demo1 (id)",
"create index idx_test_demo2 on demo1 (id)",
}
return c, nil
},
expectedErrors: []string{"indexes"},
},
{
name: "try to add index to a view collection",
collection: func(app core.App) (*core.Collection, error) {
+9 -12
View File
@@ -140,10 +140,9 @@ func (app *BaseApp) delete(ctx context.Context, model Model, isForAuxDB bool) er
})
})
if deleteErr != nil {
hookErr := app.OnModelAfterDeleteError().Trigger(&ModelErrorEvent{
ModelEvent: *event,
Error: deleteErr,
})
errEvent := &ModelErrorEvent{ModelEvent: *event, Error: deleteErr}
errEvent.App = app // replace with the initial app in case it was changed by the hook
hookErr := app.OnModelAfterDeleteError().Trigger(errEvent)
if hookErr != nil {
return errors.Join(deleteErr, hookErr)
}
@@ -332,10 +331,9 @@ func (app *BaseApp) create(ctx context.Context, model Model, withValidations boo
if saveErr != nil {
event.Model.MarkAsNew() // reset "new" state
hookErr := app.OnModelAfterCreateError().Trigger(&ModelErrorEvent{
ModelEvent: *event,
Error: saveErr,
})
errEvent := &ModelErrorEvent{ModelEvent: *event, Error: saveErr}
errEvent.App = app // replace with the initial app in case it was changed by the hook
hookErr := app.OnModelAfterCreateError().Trigger(errEvent)
if hookErr != nil {
return errors.Join(saveErr, hookErr)
}
@@ -417,10 +415,9 @@ func (app *BaseApp) update(ctx context.Context, model Model, withValidations boo
})
})
if saveErr != nil {
hookErr := app.OnModelAfterUpdateError().Trigger(&ModelErrorEvent{
ModelEvent: *event,
Error: saveErr,
})
errEvent := &ModelErrorEvent{ModelEvent: *event, Error: saveErr}
errEvent.App = app // replace with the initial app in case it was changed by the hook
hookErr := app.OnModelAfterUpdateError().Trigger(errEvent)
if hookErr != nil {
return errors.Join(saveErr, hookErr)
}
@@ -1,4 +1,4 @@
//go:build !cgo
//go:build !no_default_driver
package core
@@ -7,7 +7,7 @@ import (
_ "modernc.org/sqlite"
)
func dbConnect(dbPath string) (*dbx.DB, error) {
func DefaultDBConnect(dbPath string) (*dbx.DB, error) {
// Note: the busy_timeout pragma must be first because
// the connection needs to be set to block on busy before WAL mode
// is set in case it hasn't been already set by another connection.
-50
View File
@@ -1,50 +0,0 @@
//go:build cgo
package core
import (
"database/sql"
"github.com/mattn/go-sqlite3"
"github.com/pocketbase/dbx"
)
func init() {
// Registers the sqlite3 driver with a ConnectHook so that we can
// initialize the default PRAGMAs.
//
// Note 1: we don't define the PRAGMA as part of the dsn string
// because not all pragmas are available.
//
// Note 2: the busy_timeout pragma must be first because
// the connection needs to be set to block on busy before WAL mode
// is set in case it hasn't been already set by another connection.
sql.Register("pb_sqlite3",
&sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
_, err := conn.Exec(`
PRAGMA busy_timeout = 10000;
PRAGMA journal_mode = WAL;
PRAGMA journal_size_limit = 200000000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA temp_store = MEMORY;
PRAGMA cache_size = -16000;
`, nil)
return err
},
},
)
dbx.BuilderFuncMap["pb_sqlite3"] = dbx.BuilderFuncMap["sqlite3"]
}
func dbConnect(dbPath string) (*dbx.DB, error) {
db, err := dbx.Open("pb_sqlite3", dbPath)
if err != nil {
return nil, err
}
return db, nil
}
+9
View File
@@ -0,0 +1,9 @@
//go:build no_default_driver
package core
import "github.com/pocketbase/dbx"
func DefaultDBConnect(dbPath string) (*dbx.DB, error) {
panic("DBConnect config option must be set when the no_default_driver tag is used!")
}
+2
View File
@@ -4,9 +4,11 @@ import (
"net/http"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/tools/hook"
)
type BatchRequestEvent struct {
hook.Event
*RequestEvent
Batch []*InternalRequest
+1
View File
@@ -323,6 +323,7 @@ func syncModelErrorEventWithCollectionErrorEvent(me *ModelErrorEvent, ce *Collec
type FileTokenRequestEvent struct {
hook.Event
*RequestEvent
baseRecordEventData
Token string
}
+19 -5
View File
@@ -25,13 +25,27 @@ var (
// AutodateField defines an "autodate" type field, aka.
// field which datetime value could be auto set on record create/update.
//
// This field is usually used for defining timestamp fields like "created" and "updated".
//
// Requires either both or at least one of the OnCreate or OnUpdate options to be set.
type AutodateField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -19,12 +19,26 @@ const FieldTypeBool = "bool"
var _ Field = (*BoolField)(nil)
// BoolField defines "bool" type field to store a single true/false value.
//
// The respective zero record field value is false.
type BoolField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -19,12 +19,26 @@ const FieldTypeDate = "date"
var _ Field = (*DateField)(nil)
// DateField defines "date" type field to store a single [types.DateTime] value.
//
// The respective zero record field value is the zero [types.DateTime].
type DateField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -25,12 +25,26 @@ var (
)
// EditorField defines "editor" type field to store HTML formatted text.
//
// The respective zero record field value is empty string.
type EditorField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+20 -6
View File
@@ -21,13 +21,27 @@ const FieldTypeEmail = "email"
var _ Field = (*EmailField)(nil)
// EmailField defines "email" type field for storing single email string address.
// EmailField defines "email" type field for storing a single email string address.
//
// The respective zero record field value is empty string.
type EmailField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+20 -6
View File
@@ -53,6 +53,8 @@ var (
//
// If MaxSelect is > 1, then the field value is expected to be a slice of record ids.
//
// The respective zero record field value is either empty string (single) or empty string slice (multiple).
//
// ---
//
// The following additional setter keys are available:
@@ -67,16 +69,28 @@ var (
// // []string{"new1_ajkvass.txt", "new2_klhfnwd.txt", "old1.txt", "old2.txt",}
// record.Set("+documents", []*filesystem.File{new1, new2})
//
// - "fieldName-" - subtract one or more files from the existing record one. For example:
// - "fieldName-" - subtract/delete one or more files from the existing record one. For example:
//
// // []string{"old2.txt",}
// record.Set("documents-", "old1.txt")
type FileField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -29,12 +29,26 @@ var (
)
// JSONField defines "json" type field for storing any serialized JSON value.
//
// The respective zero record field value is the zero [types.JSONRaw].
type JSONField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+24 -5
View File
@@ -3,6 +3,7 @@ package core
import (
"context"
"fmt"
"math"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core/validators"
@@ -24,6 +25,8 @@ var (
// NumberField defines "number" type field for storing numeric (float64) value.
//
// The respective zero record field value is 0.
//
// The following additional setter keys are available:
//
// - "fieldName+" - appends to the existing record value. For example:
@@ -31,11 +34,23 @@ var (
// - "fieldName-" - subtracts from the existing record value. For example:
// record.Set("total-", 5)
type NumberField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
@@ -118,6 +133,10 @@ func (f *NumberField) ValidateValue(ctx context.Context, app App, record *Record
return validators.ErrUnsupportedValueType
}
if math.IsInf(val, 0) || math.IsNaN(val) {
return validation.NewError("validation_not_a_number", "The submitted number is not properly formatted")
}
if val == 0 {
if f.Required {
if err := validation.Required.Validate(val); err != nil {
+20
View File
@@ -177,6 +177,26 @@ func TestNumberFieldValidateValue(t *testing.T) {
},
false,
},
{
"infinity",
&core.NumberField{Name: "test"},
func() *core.Record {
record := core.NewRecord(collection)
record.Set("test", "Inf")
return record
},
true,
},
{
"NaN",
&core.NumberField{Name: "test"},
func() *core.Record {
record := core.NewRecord(collection)
record.Set("test", "NaN")
return record
},
true,
},
}
for _, s := range scenarios {
+17 -5
View File
@@ -47,11 +47,23 @@ var (
// - "fieldName:hash" - returns the bcrypt hash string of the record field value (if any). For example:
// record.GetString("password:hash")
type PasswordField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -35,6 +35,8 @@ var (
//
// If MaxSelect is > 1, then the field value is expected to be a slice of record ids.
//
// The respective zero record field value is either empty string (single) or empty string slice (multiple).
//
// ---
//
// The following additional setter keys are available:
@@ -51,11 +53,23 @@ var (
//
// record.Set("categories-", "old1") // []string{"old2"}
type RelationField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -35,6 +35,8 @@ var (
//
// If MaxSelect is > 1, then the field value is expected to be a subset of Values slice.
//
// The respective zero record field value is either empty string (single) or empty string slice (multiple).
//
// ---
//
// The following additional setter keys are available:
@@ -51,11 +53,23 @@ var (
//
// record.Set("roles-", "old1") // []string{"old2"}
type SelectField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+19 -5
View File
@@ -29,6 +29,8 @@ var (
// TextField defines "text" type field for storing any string value.
//
// The respective zero record field value is empty string.
//
// The following additional setter keys are available:
//
// - "fieldName:autogenerate" - autogenerate field value if AutogeneratePattern is set. For example:
@@ -36,11 +38,23 @@ var (
// record.Set("slug:autogenerate", "") // [random value]
// record.Set("slug:autogenerate", "abc-") // abc-[random value]
type TextField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+20 -6
View File
@@ -21,13 +21,27 @@ const FieldTypeURL = "url"
var _ Field = (*URLField)(nil)
// URLField defines "url" type field for storing URL string value.
// URLField defines "url" type field for storing a single URL string value.
//
// The respective zero record field value is empty string.
type URLField struct {
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"`
Hidden bool `form:"hidden" json:"hidden"`
Presentable bool `form:"presentable" json:"presentable"`
// Name (required) is the unique name of the field.
Name string `form:"name" json:"name"`
// Id is the unique stable field identifier.
//
// It is automatically generated from the name when adding to a collection FieldsList.
Id string `form:"id" json:"id"`
// System prevents the renaming and removal of the field.
System bool `form:"system" json:"system"`
// Hidden hides the field from the API response.
Hidden bool `form:"hidden" json:"hidden"`
// Presentable hints the Dashboard UI to use the underlying
// field record value in the relation preview label.
Presentable bool `form:"presentable" json:"presentable"`
// ---
+29 -15
View File
@@ -108,13 +108,13 @@ func (l *FieldsList) RemoveByName(fieldName string) {
// Add adds one or more fields to the current list.
//
// If any of the new fields doesn't have an id it will try to set a
// default one based on its type and name.
// By default this method will try to REPLACE existing fields with
// the new ones by their id or by their name if the new field doesn't have an explicit id.
//
// If the list already has a field with the same id,
// then the existing field is replaced with the new one.
// If no matching existing field is found, it will APPEND the field to the end of the list.
//
// Otherwise the new field is appended after the other list fields.
// In all cases, if any of the new fields don't have an explicit id it will auto generate a default one for them
// (the id value doesn't really matter and it is mostly used as a stable identifier in case of a field rename).
func (l *FieldsList) Add(fields ...Field) {
for _, f := range fields {
l.add(f)
@@ -124,7 +124,9 @@ func (l *FieldsList) Add(fields ...Field) {
// AddMarshaledJSON parses the provided raw json data and adds the
// found fields into the current list (following the same rule as the Add method).
//
// rawJSON could be either a serialized array of field objects or a single field object.
// The rawJSON argument could be one of:
// - serialized array of field objects
// - single field object.
//
// Example:
//
@@ -160,29 +162,41 @@ func (l *FieldsList) AddMarshaledJSON(rawJSON []byte) error {
}
func (l *FieldsList) add(newField Field) {
fields := *l
var replaceByName bool
newFieldId := newField.GetId()
// set default id
if newFieldId == "" {
if newField.GetName() != "" {
replaceByName = true
if newField.GetSystem() && newField.GetName() != "" {
// for system fields we use crc32 checksum for consistency because they cannot be renamed
newFieldId = newField.Type() + crc32Checksum(newField.GetName())
} else {
newFieldId = newField.Type() + security.RandomString(5)
newFieldId = newField.Type() + security.RandomString(7)
}
newField.SetId(newFieldId)
}
fields := *l
// replace existing
for i, field := range fields {
// replace existing
if newFieldId != "" && field.GetId() == newFieldId {
(*l)[i] = newField
return
if replaceByName {
if name := newField.GetName(); name != "" && field.GetName() == name {
newField.SetId(field.GetId())
(*l)[i] = newField
return
}
} else {
if field.GetId() == newFieldId {
(*l)[i] = newField
return
}
}
}
// add new field
newField.SetId(newFieldId)
*l = append(fields, newField)
}
+17 -12
View File
@@ -414,6 +414,8 @@ func TestRecordMergeExpand(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Id = "_pbc_123"
m := core.NewRecord(collection)
m.Id = "m"
@@ -497,7 +499,7 @@ func TestRecordMergeExpand(t *testing.T) {
}
rawStr := string(raw)
expected := `{"a":{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"a1":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a1"},"a23":[{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a2"},{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"a31":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a31"},"a32":[{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a32"},{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a32New"}],"a33New":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"a33New"}},"id":"a3"}]},"id":"a"},"b":[{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"b1":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"b1"}},"id":"b"},{"collectionId":"_pbc_3632233996","collectionName":"test","id":"bNew"}],"c":[{"collectionId":"_pbc_3632233996","collectionName":"test","id":"c"}],"dNew":{"collectionId":"_pbc_3632233996","collectionName":"test","id":"dNew"}}`
expected := `{"a":{"collectionId":"_pbc_123","collectionName":"test","expand":{"a1":{"collectionId":"_pbc_123","collectionName":"test","id":"a1"},"a23":[{"collectionId":"_pbc_123","collectionName":"test","id":"a2"},{"collectionId":"_pbc_123","collectionName":"test","expand":{"a31":{"collectionId":"_pbc_123","collectionName":"test","id":"a31"},"a32":[{"collectionId":"_pbc_123","collectionName":"test","id":"a32"},{"collectionId":"_pbc_123","collectionName":"test","id":"a32New"}],"a33New":{"collectionId":"_pbc_123","collectionName":"test","id":"a33New"}},"id":"a3"}]},"id":"a"},"b":[{"collectionId":"_pbc_123","collectionName":"test","expand":{"b1":{"collectionId":"_pbc_123","collectionName":"test","id":"b1"}},"id":"b"},{"collectionId":"_pbc_123","collectionName":"test","id":"bNew"}],"c":[{"collectionId":"_pbc_123","collectionName":"test","id":"c"}],"dNew":{"collectionId":"_pbc_123","collectionName":"test","id":"dNew"}}`
if expected != rawStr {
t.Fatalf("Expected \n%v, \ngot \n%v", expected, rawStr)
@@ -508,6 +510,7 @@ func TestRecordMergeExpandNilCheck(t *testing.T) {
t.Parallel()
collection := core.NewBaseCollection("test")
collection.Id = "_pbc_123"
scenarios := []struct {
name string
@@ -517,17 +520,17 @@ func TestRecordMergeExpandNilCheck(t *testing.T) {
{
"nil expand",
nil,
`{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}`,
`{"collectionId":"_pbc_123","collectionName":"test","id":""}`,
},
{
"empty expand",
map[string]any{},
`{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}`,
`{"collectionId":"_pbc_123","collectionName":"test","id":""}`,
},
{
"non-empty expand",
map[string]any{"test": core.NewRecord(collection)},
`{"collectionId":"_pbc_3632233996","collectionName":"test","expand":{"test":{"collectionId":"_pbc_3632233996","collectionName":"test","id":""}},"id":""}`,
`{"collectionId":"_pbc_123","collectionName":"test","expand":{"test":{"collectionId":"_pbc_123","collectionName":"test","id":""}},"id":""}`,
},
}
@@ -1308,9 +1311,11 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
f5 := &core.TextField{Name: "field5", Hidden: true}
colBase := core.NewBaseCollection("test_base")
colBase.Id = "_pbc_base_123"
colBase.Fields.Add(f1, f2, f3, f4, f5)
colAuth := core.NewAuthCollection("test_auth")
colAuth.Id = "_pbc_auth_123"
colAuth.Fields.Add(f1, f2, f3, f4, f5)
scenarios := []struct {
@@ -1330,7 +1335,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
},
{
"[base] with email visibility",
@@ -1339,7 +1344,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
},
{
"[base] with custom data",
@@ -1348,7 +1353,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
nil,
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
},
{
"[base] with explicit hide and unhide fields",
@@ -1366,7 +1371,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
[]string{"field5", "@pbInternalAbc", "email", "tokenKey", "unknown"},
`{"collectionId":"_pbc_3318600878","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
},
// auth
@@ -1377,7 +1382,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
},
{
"[auth] with email visibility",
@@ -1386,7 +1391,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
false,
nil,
nil,
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
},
{
"[auth] with custom data",
@@ -1395,7 +1400,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
nil,
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","verified":true}`,
},
{
"[auth] with explicit hide and unhide fields",
@@ -1413,7 +1418,7 @@ func TestRecordPublicExportAndMarshalJSON(t *testing.T) {
true,
nil,
[]string{"field5", "@pbInternalAbc", "tokenKey", "unknown", "email"}, // emailVisibility:false has higher priority
`{"collectionId":"_pbc_4255619734","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","unknown":"test_unknown","verified":true}`,
`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","unknown":"test_unknown","verified":true}`,
},
}
+1 -1
View File
@@ -553,7 +553,7 @@ func (app *BaseApp) FindAuthRecordByEmail(collectionModelOrIdentifier any, email
// CanAccessRecord checks if a record is allowed to be accessed by the
// specified requestInfo and accessRule.
//
// Rule and db checks are ignored in case requestInfo.AuthRecord is a superuser.
// Rule and db checks are ignored in case requestInfo.Auth is a superuser.
//
// The returned error indicate that something unexpected happened during
// the check (eg. invalid rule or db query error).
+1 -1
View File
@@ -43,7 +43,7 @@ func main() {
app.RootCmd.PersistentFlags().IntVar(
&hooksPool,
"hooksPool",
20,
15,
"the total prewarm goja.Runtime instances for the JS app hooks execution",
)
+21 -22
View File
@@ -4,11 +4,11 @@ go 1.23
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/aws/aws-sdk-go-v2 v1.32.1
github.com/aws/aws-sdk-go-v2/config v1.27.42
github.com/aws/aws-sdk-go-v2/credentials v1.17.40
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.29
github.com/aws/aws-sdk-go-v2/service/s3 v1.65.1
github.com/aws/aws-sdk-go-v2 v1.32.2
github.com/aws/aws-sdk-go-v2/config v1.28.0
github.com/aws/aws-sdk-go-v2/credentials v1.17.41
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0
github.com/aws/smithy-go v1.22.0
github.com/disintegration/imaging v1.6.2
github.com/domodwyer/mailyak/v3 v3.6.2
@@ -16,16 +16,15 @@ require (
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/fatih/color v1.17.0
github.com/fsnotify/fsnotify v1.7.0
github.com/gabriel-vasile/mimetype v1.4.5
github.com/gabriel-vasile/mimetype v1.4.6
github.com/ganigeorgiev/fexpr v0.4.1
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/pocketbase/dbx v1.10.1
github.com/pocketbase/tygoja v0.0.0-20240113091827-17918475d342
github.com/pocketbase/tygoja v0.0.0-20241015175937-d6ff411a0f75
github.com/spf13/cast v1.7.0
github.com/spf13/cobra v1.8.1
gocloud.dev v0.39.0
gocloud.dev v0.40.0
golang.org/x/crypto v0.28.0
golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.23.0
@@ -36,18 +35,18 @@ require (
require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
@@ -66,7 +65,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.2 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sys v0.26.0 // indirect
@@ -74,8 +73,8 @@ require (
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.199.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/api v0.201.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
+50 -54
View File
@@ -1,8 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw=
cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM=
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8=
cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
@@ -25,42 +25,42 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.32.1 h1:8WuZ43ytA+TV6QEPT/R23mr7pWyI7bSSiEHdt9BS2Pw=
github.com/aws/aws-sdk-go-v2 v1.32.1/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI=
github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=
github.com/aws/aws-sdk-go-v2/config v1.27.42 h1:Zsy9coUPuOsCWkjTvHpl2/DB9bptXtv7WeNPxvFr87s=
github.com/aws/aws-sdk-go-v2/config v1.27.42/go.mod h1:FGASs+PuJM2EY+8rt8qyQKLPbbX/S5oY+6WzJ/KE7ko=
github.com/aws/aws-sdk-go-v2/credentials v1.17.40 h1:RjnlA7t0p/IamxAM7FUJ5uS13Vszh4sjVGvsx91tGro=
github.com/aws/aws-sdk-go-v2/credentials v1.17.40/go.mod h1:dgpdnSs1Bp/atS6vLlW83h9xZPP+uSPB/27dFSgC1BM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.16 h1:fwrer1pJeaiia0CcOfWVbZxvj9Adc7rsuaMTwPR0DIA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.16/go.mod h1:XyEwwp8XI4zMar7MTnJ0Sk7qY/9aN8Hp929XhuX5SF8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.29 h1:eyeHfJ9FAb7sd5ODTkjrfot3gS0Ln4vn/18l7zZMCik=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.29/go.mod h1:JpzRPe12SjlOmuqgi+/5RmgfbsWzDYdfxe3Abrk2kW8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.20 h1:OErdlGnt+hg3tTwGYAlKvFkKVUo/TXkoHcxDxuhYYU8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.20/go.mod h1:HsPfuL5gs+407ByRXBMgpYoyrV1sgMrzd18yMXQHJpo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.20 h1:822cE1CYSwY/EZnErlF46pyynuxvf1p+VydHRQW+XNs=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.20/go.mod h1:79/Tn7H7hYC5Gjz6fbnOV4OeBpkao7E8Tv95RO72pMM=
github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ=
github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc=
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33 h1:X+4YY5kZRI/cOoSMVMGTqFXHAMg1bvvay7IBcqHpybQ=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33/go.mod h1:DPynzu+cn92k5UQ6tZhX+wfTB4ah6QDU/NgdHqatmvk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.20 h1:HO5UCCkLmeWkJZHLvLDfylKv8ca28XLAX3HojZz2shI=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.20/go.mod h1:IO0HUM6Ouk/s7Rx3hiLtFU3mc+9OJFFygjsaxFBhAbk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.1 h1:UeW3Ul28hkKvB3beWImBvO7U62tSmapxaqk8sX9SMCU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.1/go.mod h1:TER/1DuTxSN6RFQpk3xfD9hK4A1gQ7ainfkwHV3LPtU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.1 h1:5vBMBTakOvtd8aNaicswcrr9qqCYUlasuzyoU6/0g8I=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.1/go.mod h1:WSUbDa5qdg05Q558KXx2Scb+EDvOPXT9gfET0fyrJSk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.1 h1:T6oOYbNQ+iqdtG1/mTJvMBg/YFyHR8Z8URyG3qK+Anc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.1/go.mod h1:25CEM6c1e2vyLcr3fPritPsdsoMwNAOc9//M1QAwtDk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.65.1 h1:HQR79P0F0C2YQOaS2Z+90YK9DH22z9D6Neplaj0yuy4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.65.1/go.mod h1:xYVl5BX9Ws7+ZM58b3w0kq36TR1Dgw2OMkjSr6YTWXg=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.1 h1:aAIr0WhAgvKrxZtkBqne87Gjmd7/lJVTFkR2l2yuhL8=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.1/go.mod h1:8XhxGMWUfikJuginPQl5SGZ0LSJuNX3TCEQmFWZwHTM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.1 h1:J6kIsIkgFOaU6aKjigXJoue1XEHtKIIrpSh4vKdmRTs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.1/go.mod h1:2V2JLP7tXOmUbL3Hd1ojq+774t2KUAEQ35//shoNEL0=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.1 h1:q76Ig4OaJzVJGNUSGO3wjSTBS94g+EhHIbpY9rPvkxs=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.1/go.mod h1:664dajZ7uS7JMUMUG0R5bWbtN97KECNCVdFDdQ6Ipu8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0 h1:xA6XhTF7PE89BCNHJbQi8VvPzcgMtmGC5dr8S8N7lHk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo=
github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo=
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -78,8 +78,6 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2 h1:Ux9RXuPQmTB4C1MKagNLme0krvq8ulewfor+ORO/QL4=
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3 h1:MXsAuToxwsTn5BEEYm2DheqIiC4jWGmkEJ1uy+KFhvQ=
github.com/dop251/goja v0.0.0-20241009100908-5f46f2705ca3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
@@ -98,8 +96,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -174,8 +172,6 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -185,8 +181,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA=
github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/tygoja v0.0.0-20240113091827-17918475d342 h1:OcAwewen3hs/zY8i0syt8CcMTGBJhQwQRVDLcoQVXVk=
github.com/pocketbase/tygoja v0.0.0-20240113091827-17918475d342/go.mod h1:dOJ+pCyqm/jRn5kO/TX598J0e5xGDcJAZerK5atCrKI=
github.com/pocketbase/tygoja v0.0.0-20241015175937-d6ff411a0f75 h1:XSbmekxgmbI2uPrre/nkCz7y8VsV652TPb3hAYzPb74=
github.com/pocketbase/tygoja v0.0.0-20241015175937-d6ff411a0f75/go.mod h1:hKJWPGFqavk3cdTa47Qvs8g37lnfI57OYdVVbIqW5aE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -222,16 +218,16 @@ go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
gocloud.dev v0.39.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
gocloud.dev v0.40.0 h1:f8LgP+4WDqOG/RXoUcyLpeIAGOcAbZrZbDQCUee10ng=
gocloud.dev v0.40.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw=
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
@@ -286,8 +282,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -301,20 +297,20 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs=
google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28=
google.golang.org/api v0.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0=
google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 h1:nFS3IivktIU5Mk6KQa+v6RKkHUpdQpphqGNLxqNnbEk=
google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:tEzYTYZxbmVNOu0OAFH9HzdJtLn6h4Aj89zzlBCdHms=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+8 -8
View File
@@ -51,6 +51,8 @@ func init() {
[[created]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL,
[[updated]] TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%fZ')) NOT NULL
);
CREATE INDEX IF NOT EXISTS idx__collections_type on {{_collections}} ([[type]]);
`).Execute()
if execerr != nil {
return fmt.Errorf("_collections exec error: %w", execerr)
@@ -116,13 +118,12 @@ func createParamsTable(txApp core.App) error {
}
func createMFAsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameMFAs)
col := core.NewBaseCollection(core.CollectionNameMFAs, "_pbc"+core.CollectionNameMFAs)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
col.ListRule = types.Pointer(ownerRule)
col.ViewRule = types.Pointer(ownerRule)
col.DeleteRule = types.Pointer(ownerRule)
col.Fields.Add(&core.TextField{
Name: "collectionRef",
@@ -156,13 +157,12 @@ func createMFAsCollection(txApp core.App) error {
}
func createOTPsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameOTPs)
col := core.NewBaseCollection(core.CollectionNameOTPs, "_pbc"+core.CollectionNameOTPs)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
col.ListRule = types.Pointer(ownerRule)
col.ViewRule = types.Pointer(ownerRule)
col.DeleteRule = types.Pointer(ownerRule)
col.Fields.Add(&core.TextField{
Name: "collectionRef",
@@ -198,7 +198,7 @@ func createOTPsCollection(txApp core.App) error {
}
func createAuthOriginsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameAuthOrigins)
col := core.NewBaseCollection(core.CollectionNameAuthOrigins, "_pbc"+core.CollectionNameAuthOrigins)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
@@ -238,7 +238,7 @@ func createAuthOriginsCollection(txApp core.App) error {
}
func createExternalAuthsCollection(txApp core.App) error {
col := core.NewBaseCollection(core.CollectionNameExternalAuths)
col := core.NewBaseCollection(core.CollectionNameExternalAuths, "_pbc"+core.CollectionNameExternalAuths)
col.System = true
ownerRule := "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId"
@@ -284,7 +284,7 @@ func createExternalAuthsCollection(txApp core.App) error {
}
func createSuperusersCollection(txApp core.App) error {
superusers := core.NewAuthCollection(core.CollectionNameSuperusers)
superusers := core.NewAuthCollection(core.CollectionNameSuperusers, "_pbc"+core.CollectionNameSuperusers)
superusers.System = true
superusers.Fields.Add(&core.EmailField{
Name: "email",
@@ -308,7 +308,7 @@ func createSuperusersCollection(txApp core.App) error {
}
func createUsersCollection(txApp core.App) error {
users := core.NewAuthCollection("users")
users := core.NewAuthCollection("users", "_pb_users_auth_")
users.Fields.Add(&core.TextField{
Name: "name",
Max: 255,
+35
View File
@@ -0,0 +1,35 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
)
// note: this migration will be deleted in future version
func init() {
core.SystemMigrations.Register(func(txApp core.App) error {
_, err := txApp.DB().NewQuery("CREATE INDEX IF NOT EXISTS idx__collections_type on {{_collections}} ([[type]]);").Execute()
if err != nil {
return err
}
// reset mfas and otps delete rule
collectionNames := []string{core.CollectionNameMFAs, core.CollectionNameOTPs}
for _, name := range collectionNames {
col, err := txApp.FindCollectionByNameOrId(name)
if err != nil {
return err
}
if col.DeleteRule != nil {
col.DeleteRule = nil
err = txApp.SaveNoValidate(col)
if err != nil {
return err
}
}
}
return nil
}, nil)
}
+6 -3
View File
@@ -55,6 +55,8 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
// register the hook to the loader
loader.Set(jsName, func(callback string, tags ...string) {
// overwrite the global $app with the hook scoped instance
callback = `function(e) { $app = e.app; return (` + callback + `).call(undefined, e) }`
pr := goja.MustCompile("", "{("+callback+").apply(undefined, __args)}", true)
tagsAsValues := make([]reflect.Value, len(tags))
@@ -74,6 +76,7 @@ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
}
err := executors.run(func(executor *goja.Runtime) error {
executor.Set("$app", goja.Undefined())
executor.Set("__args", handlerArgs)
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
@@ -189,6 +192,7 @@ func wrapHandlerFunc(executors *vmsPool, handler goja.Value) (func(*core.Request
wrappedHandler := func(e *core.RequestEvent) error {
return executors.run(func(executor *goja.Runtime) error {
executor.Set("$app", e.App) // overwrite the global $app with the hook scoped instance
executor.Set("__args", []any{e})
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
@@ -245,6 +249,7 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.
Priority: v.priority,
Func: func(e *core.RequestEvent) error {
return executors.run(func(executor *goja.Runtime) error {
executor.Set("$app", e.App) // overwrite the global $app with the hook scoped instance
executor.Set("__args", []any{e})
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
@@ -266,6 +271,7 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.
wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
Func: func(e *core.RequestEvent) error {
return executors.run(func(executor *goja.Runtime) error {
executor.Set("$app", e.App) // overwrite the global $app with the hook scoped instance
executor.Set("__args", []any{e})
res, err := executor.RunProgram(pr)
executor.Set("__args", goja.Undefined())
@@ -415,9 +421,6 @@ func baseBinds(vm *goja.Runtime) {
instance := &core.Collection{}
return structConstructorUnmarshal(vm, call, instance)
})
registerFactoryAsConstructor(vm, "BaseCollection", core.NewBaseCollection)
registerFactoryAsConstructor(vm, "AuthCollection", core.NewAuthCollection)
registerFactoryAsConstructor(vm, "ViewCollection", core.NewViewCollection)
vm.Set("FieldsList", func(call goja.ConstructorCall) *goja.Object {
instance := &core.FieldsList{}
+1 -37
View File
@@ -43,7 +43,7 @@ func TestBaseBindsCount(t *testing.T) {
vm := goja.New()
baseBinds(vm)
testBindsCount(vm, "this", 35, t)
testBindsCount(vm, "this", 32, t)
}
func TestBaseBindsSleep(t *testing.T) {
@@ -293,42 +293,6 @@ func TestBaseBindsCollection(t *testing.T) {
}
}
func TestBaseBindsCollectionFactories(t *testing.T) {
vm := goja.New()
baseBinds(vm)
scenarios := []struct {
js string
expectedType string
}{
{"new BaseCollection('test')", core.CollectionTypeBase},
{"new ViewCollection('test')", core.CollectionTypeView},
{"new AuthCollection('test')", core.CollectionTypeAuth},
}
for _, s := range scenarios {
t.Run(s.js, func(t *testing.T) {
v, err := vm.RunString(s.js)
if err != nil {
t.Fatal(err)
}
c, ok := v.Export().(*core.Collection)
if !ok {
t.Fatalf("Expected *core.Collection instance, got %T (%v)", c, c)
}
if c.Name != "test" {
t.Fatalf("Expected collection name %q, got %v", "test", c.Name)
}
if c.Type != s.expectedType {
t.Fatalf("Expected collection type %q, got %v", s.expectedType, c.Type)
}
})
}
}
func TestBaseBindsFieldsList(t *testing.T) {
vm := goja.New()
baseBinds(vm)
File diff suppressed because it is too large. Load diff
+35 -117
View File
@@ -217,7 +217,7 @@ declare function sleep(milliseconds: number): void;
* ` + "```" + `js
* const records = arrayOf(new Record)
*
* $app.dao().recordQuery("articles").limit(10).all(records)
* $app.recordQuery("articles").limit(10).all(records)
* ` + "```" + `
*
* @group PocketBase
@@ -279,7 +279,7 @@ declare class Context implements context.Context {
* Record model class.
*
* ` + "```" + `js
* const collection = $app.dao().findCollectionByNameOrId("article")
* const collection = $app.findCollectionByNameOrId("article")
*
* const record = new Record(collection, {
* title: "Lorem ipsum"
@@ -297,14 +297,16 @@ declare const Record: {
// note: declare as "newable" const due to conflict with the Record TS utility type
}
interface Collection extends core.Collection{} // merge
interface Collection extends core.Collection{
type: "base" | "view" | "auth"
} // merge
/**
* Collection model class.
*
* ` + "```" + `js
* const collection = new Collection({
* name: "article",
* type: "base",
* name: "article",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
@@ -327,91 +329,7 @@ interface Collection extends core.Collection{} // merge
* @group PocketBase
*/
declare class Collection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface BaseCollection extends core.Collection{} // merge
/**
* Alias for a "base" collection class.
*
* ` + "```" + `js
* const collection = new BaseCollection({
* name: "article",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* fields: [
* {
* name: "title",
* type: "text",
* required: true,
* min: 6,
* max: 100,
* },
* {
* name: "description",
* type: "text",
* },
* ]
* })
* ` + "```" + `
*
* @group PocketBase
*/
declare class BaseCollection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface AuthCollection extends core.Collection{} // merge
/**
* Alias for an "auth" collection class.
*
* ` + "```" + `js
* const collection = new AuthCollection({
* name: "clients",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* fields: [
* {
* name: "title",
* type: "text",
* required: true,
* min: 6,
* max: 100,
* },
* {
* name: "description",
* type: "text",
* },
* ]
* })
* ` + "```" + `
*
* @group PocketBase
*/
declare class AuthCollection implements core.Collection {
constructor(data?: Partial<core.Collection>)
}
interface ViewCollection extends core.Collection{} // merge
/**
* Alias for a "view" collection class.
*
* ` + "```" + `js
* const collection = new ViewCollection({
* name: "clients",
* listRule: "@request.auth.id != '' || status = 'public'",
* viewRule: "@request.auth.id != '' || status = 'public'",
* deleteRule: "@request.auth.id != ''",
* viewQuery: "SELECT id, title from posts",
* })
* ` + "```" + `
*
* @group PocketBase
*/
declare class ViewCollection implements core.Collection {
constructor(data?: Partial<core.Collection>)
constructor(data?: Partial<Collection>)
}
interface FieldsList extends core.FieldsList{} // merge
@@ -436,7 +354,7 @@ declare class Field implements core.Field {
interface NumberField extends core.NumberField{} // merge
/**
* NumberField class defines a single "number" collection field.
* {@inheritDoc core.NumberField}
*
* @group PocketBase
*/
@@ -446,7 +364,7 @@ declare class NumberField implements core.NumberField {
interface BoolField extends core.BoolField{} // merge
/**
* BoolField class defines a single "bool" collection field.
* {@inheritDoc core.BoolField}
*
* @group PocketBase
*/
@@ -456,7 +374,7 @@ declare class BoolField implements core.BoolField {
interface TextField extends core.TextField{} // merge
/**
* TextField class defines a single "text" collection field.
* {@inheritDoc core.TextField}
*
* @group PocketBase
*/
@@ -466,7 +384,7 @@ declare class TextField implements core.TextField {
interface URLField extends core.URLField{} // merge
/**
* URLField class defines a single "url" collection field.
* {@inheritDoc core.URLField}
*
* @group PocketBase
*/
@@ -476,7 +394,7 @@ declare class URLField implements core.URLField {
interface EmailField extends core.EmailField{} // merge
/**
* EmailField class defines a single "email" collection field.
* {@inheritDoc core.EmailField}
*
* @group PocketBase
*/
@@ -486,7 +404,7 @@ declare class EmailField implements core.EmailField {
interface EditorField extends core.EditorField{} // merge
/**
* EditorField class defines a single "editor" collection field.
* {@inheritDoc core.EditorField}
*
* @group PocketBase
*/
@@ -496,7 +414,7 @@ declare class EditorField implements core.EditorField {
interface PasswordField extends core.PasswordField{} // merge
/**
* PasswordField class defines a single "password" collection field.
* {@inheritDoc core.PasswordField}
*
* @group PocketBase
*/
@@ -506,7 +424,7 @@ declare class PasswordField implements core.PasswordField {
interface DateField extends core.DateField{} // merge
/**
* DateField class defines a single "date" collection field.
* {@inheritDoc core.DateField}
*
* @group PocketBase
*/
@@ -516,7 +434,7 @@ declare class DateField implements core.DateField {
interface AutodateField extends core.AutodateField{} // merge
/**
* AutodateField class defines a single "autodate" collection field.
* {@inheritDoc core.AutodateField}
*
* @group PocketBase
*/
@@ -526,7 +444,7 @@ declare class AutodateField implements core.AutodateField {
interface JSONField extends core.JSONField{} // merge
/**
* JSONField class defines a single "json" collection field.
* {@inheritDoc core.JSONField}
*
* @group PocketBase
*/
@@ -536,7 +454,7 @@ declare class JSONField implements core.JSONField {
interface RelationField extends core.RelationField{} // merge
/**
* RelationField class defines a single "relation" collection field.
* {@inheritDoc core.RelationField}
*
* @group PocketBase
*/
@@ -546,7 +464,7 @@ declare class RelationField implements core.RelationField {
interface SelectField extends core.SelectField{} // merge
/**
* SelectField class defines a single "select" collection field.
* {@inheritDoc core.SelectField}
*
* @group PocketBase
*/
@@ -556,7 +474,7 @@ declare class SelectField implements core.SelectField {
interface FileField extends core.FileField{} // merge
/**
* FileField class defines a single "file" collection field.
* {@inheritDoc core.FileField}
*
* @group PocketBase
*/
@@ -922,7 +840,7 @@ declare namespace $os {
* const cmd = $os.cmd('ls', '-sl')
*
* // execute the command and return its standard output as string
* const output = String.fromCharCode(...cmd.output());
* const output = toString(cmd.output());
* ` + "```" + `
*/
export let cmd: exec.command
@@ -988,73 +906,73 @@ declare class TestS3FilesystemForm implements forms.TestS3Filesystem {
// apisBinds
// -------------------------------------------------------------------
interface ApiError extends apis.ApiError{} // merge
interface ApiError extends router.ApiError{} // merge
/**
* @inheritDoc
*
* @group PocketBase
*/
declare class ApiError implements apis.ApiError {
declare class ApiError implements router.ApiError {
constructor(status?: number, message?: string, data?: any)
}
interface NotFoundError extends apis.ApiError{} // merge
interface NotFoundError extends router.ApiError{} // merge
/**
* NotFounderor returns 404 ApiError.
*
* @group PocketBase
*/
declare class NotFoundError implements apis.ApiError {
declare class NotFoundError implements router.ApiError {
constructor(message?: string, data?: any)
}
interface BadRequestError extends apis.ApiError{} // merge
interface BadRequestError extends router.ApiError{} // merge
/**
* BadRequestError returns 400 ApiError.
*
* @group PocketBase
*/
declare class BadRequestError implements apis.ApiError {
declare class BadRequestError implements router.ApiError {
constructor(message?: string, data?: any)
}
interface ForbiddenError extends apis.ApiError{} // merge
interface ForbiddenError extends router.ApiError{} // merge
/**
* ForbiddenError returns 403 ApiError.
*
* @group PocketBase
*/
declare class ForbiddenError implements apis.ApiError {
declare class ForbiddenError implements router.ApiError {
constructor(message?: string, data?: any)
}
interface UnauthorizedError extends apis.ApiError{} // merge
interface UnauthorizedError extends router.ApiError{} // merge
/**
* UnauthorizedError returns 401 ApiError.
*
* @group PocketBase
*/
declare class UnauthorizedError implements apis.ApiError {
declare class UnauthorizedError implements router.ApiError {
constructor(message?: string, data?: any)
}
interface TooManyRequestsError extends apis.ApiError{} // merge
interface TooManyRequestsError extends router.ApiError{} // merge
/**
* TooManyRequestsError returns 429 ApiError.
*
* @group PocketBase
*/
declare class TooManyRequestsError implements apis.ApiError {
declare class TooManyRequestsError implements router.ApiError {
constructor(message?: string, data?: any)
}
interface InternalServerError extends apis.ApiError{} // merge
interface InternalServerError extends router.ApiError{} // merge
/**
* InternalServerError returns 429 ApiError.
*
* @group PocketBase
*/
declare class InternalServerError implements apis.ApiError {
declare class InternalServerError implements router.ApiError {
constructor(message?: string, data?: any)
}
+15 -5
View File
@@ -224,7 +224,7 @@ func (p *plugin) registerHooks() error {
// initialize the hooks dir watcher
if p.config.HooksWatch {
if err := p.watchHooks(); err != nil {
return err
color.Yellow("Unable to init hooks watcher: %v", err)
}
}
@@ -343,13 +343,23 @@ func (p *plugin) normalizeServeExceptions(e *core.RequestEvent) error {
//
// This method does nothing if the hooks directory is missing.
func (p *plugin) watchHooks() error {
if _, err := os.Stat(p.config.HooksDir); err != nil {
watchDir := p.config.HooksDir
hooksDirInfo, err := os.Lstat(p.config.HooksDir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil // no hooks dir to watch
}
return err
}
if hooksDirInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
watchDir, err = filepath.EvalSymlinks(p.config.HooksDir)
if err != nil {
return fmt.Errorf("failed to resolve hooksDir symink: %w", err)
}
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
@@ -408,9 +418,9 @@ func (p *plugin) watchHooks() error {
// add directories to watch
//
// @todo replace once recursive watcher is added (https://github.com/fsnotify/fsnotify/issues/18)
dirsErr := filepath.Walk(p.config.HooksDir, func(path string, info fs.FileInfo, err error) error {
// ignore hidden directories and node_modules
if !info.IsDir() || info.Name() == "node_modules" || strings.HasPrefix(info.Name(), ".") {
dirsErr := filepath.WalkDir(watchDir, func(path string, entry fs.DirEntry, err error) error {
// ignore hidden directories, node_modules, symlinks, sockets, etc.
if !entry.IsDir() || entry.Name() == "node_modules" || strings.HasPrefix(entry.Name(), ".") {
return nil
}
+77 -54
View File
@@ -3,12 +3,14 @@ package migratecmd_test
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/types"
)
@@ -49,7 +51,7 @@ migrate((app) => {
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"id": "text@TEST_RANDOM",
"max": 15,
"min": 15,
"name": "id",
@@ -63,7 +65,7 @@ migrate((app) => {
{
"cost": 0,
"hidden": true,
"id": "password901924565",
"id": "password@TEST_RANDOM",
"max": 0,
"min": 8,
"name": "password",
@@ -76,7 +78,7 @@ migrate((app) => {
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"hidden": true,
"id": "text2504183744",
"id": "text@TEST_RANDOM",
"max": 60,
"min": 30,
"name": "tokenKey",
@@ -90,7 +92,7 @@ migrate((app) => {
{
"exceptDomains": null,
"hidden": false,
"id": "email3885137012",
"id": "email@TEST_RANDOM",
"name": "email",
"onlyDomains": null,
"presentable": false,
@@ -100,7 +102,7 @@ migrate((app) => {
},
{
"hidden": false,
"id": "bool1547992806",
"id": "bool@TEST_RANDOM",
"name": "emailVisibility",
"presentable": false,
"required": false,
@@ -109,7 +111,7 @@ migrate((app) => {
},
{
"hidden": false,
"id": "bool256245529",
"id": "bool@TEST_RANDOM",
"name": "verified",
"presentable": false,
"required": false,
@@ -120,11 +122,11 @@ migrate((app) => {
"fileToken": {
"duration": 180
},
"id": "_pbc_2865679067",
"id": "@TEST_RANDOM",
"indexes": [
"create index test on new_name (id)",
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey__pbc_2865679067` + "`" + ` ON ` + "`" + `new_name` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email__pbc_2865679067` + "`" + ` ON ` + "`" + `new_name` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `new_name` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `new_name` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
],
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
"manageRule": "1 != 2",
@@ -180,7 +182,7 @@ migrate((app) => {
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId("_pbc_2865679067");
const collection = app.findCollectionByNameOrId("@TEST_RANDOM");
return app.delete(collection);
})
@@ -225,7 +227,7 @@ func init() {
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"id": "text@TEST_RANDOM",
"max": 15,
"min": 15,
"name": "id",
@@ -239,7 +241,7 @@ func init() {
{
"cost": 0,
"hidden": true,
"id": "password901924565",
"id": "password@TEST_RANDOM",
"max": 0,
"min": 8,
"name": "password",
@@ -252,7 +254,7 @@ func init() {
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"hidden": true,
"id": "text2504183744",
"id": "text@TEST_RANDOM",
"max": 60,
"min": 30,
"name": "tokenKey",
@@ -266,7 +268,7 @@ func init() {
{
"exceptDomains": null,
"hidden": false,
"id": "email3885137012",
"id": "email@TEST_RANDOM",
"name": "email",
"onlyDomains": null,
"presentable": false,
@@ -276,7 +278,7 @@ func init() {
},
{
"hidden": false,
"id": "bool1547992806",
"id": "bool@TEST_RANDOM",
"name": "emailVisibility",
"presentable": false,
"required": false,
@@ -285,7 +287,7 @@ func init() {
},
{
"hidden": false,
"id": "bool256245529",
"id": "bool@TEST_RANDOM",
"name": "verified",
"presentable": false,
"required": false,
@@ -296,11 +298,11 @@ func init() {
"fileToken": {
"duration": 180
},
"id": "_pbc_2865679067",
"id": "@TEST_RANDOM",
"indexes": [
"create index test on new_name (id)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey__pbc_2865679067` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `new_name` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email__pbc_2865679067` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `new_name` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `new_name` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `new_name` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
],
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
"manageRule": "1 != 2",
@@ -361,7 +363,7 @@ func init() {
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("_pbc_2865679067")
collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
if err != nil {
return err
}
@@ -434,9 +436,16 @@ func init() {
if err != nil {
t.Fatalf("Failed to read the generated migration file: %v", err)
}
contentStr := strings.TrimSpace(string(content))
if v := strings.TrimSpace(string(content)); v != strings.TrimSpace(s.expectedTemplate) {
t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, v)
// replace @TEST_RANDOM placeholder with a regex pattern
expectedTemplate := strings.ReplaceAll(
"^"+regexp.QuoteMeta(strings.TrimSpace(s.expectedTemplate))+"$",
"@TEST_RANDOM",
`\w+`,
)
if !list.ExistInSliceWithRegex(contentStr, []string{expectedTemplate}) {
t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, contentStr)
}
})
}
@@ -454,7 +463,7 @@ func TestAutomigrateCollectionDelete(t *testing.T) {
`
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("_pbc_4032078523");
const collection = app.findCollectionByNameOrId("@TEST_RANDOM");
return app.delete(collection);
}, (app) => {
@@ -483,7 +492,7 @@ migrate((app) => {
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"id": "text@TEST_RANDOM",
"max": 15,
"min": 15,
"name": "id",
@@ -497,7 +506,7 @@ migrate((app) => {
{
"cost": 0,
"hidden": true,
"id": "password901924565",
"id": "password@TEST_RANDOM",
"max": 0,
"min": 8,
"name": "password",
@@ -510,7 +519,7 @@ migrate((app) => {
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"hidden": true,
"id": "text2504183744",
"id": "text@TEST_RANDOM",
"max": 60,
"min": 30,
"name": "tokenKey",
@@ -534,7 +543,7 @@ migrate((app) => {
},
{
"hidden": false,
"id": "bool1547992806",
"id": "bool@TEST_RANDOM",
"name": "emailVisibility",
"presentable": false,
"required": false,
@@ -554,11 +563,11 @@ migrate((app) => {
"fileToken": {
"duration": 180
},
"id": "_pbc_4032078523",
"id": "@TEST_RANDOM",
"indexes": [
"create index test on test123 (id)",
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey__pbc_4032078523` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email__pbc_4032078523` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
],
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
"manageRule": "1 != 2",
@@ -630,7 +639,7 @@ import (
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("_pbc_4032078523")
collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
if err != nil {
return err
}
@@ -662,7 +671,7 @@ func init() {
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"id": "text@TEST_RANDOM",
"max": 15,
"min": 15,
"name": "id",
@@ -676,7 +685,7 @@ func init() {
{
"cost": 0,
"hidden": true,
"id": "password901924565",
"id": "password@TEST_RANDOM",
"max": 0,
"min": 8,
"name": "password",
@@ -689,7 +698,7 @@ func init() {
{
"autogeneratePattern": "[a-zA-Z0-9]{50}",
"hidden": true,
"id": "text2504183744",
"id": "text@TEST_RANDOM",
"max": 60,
"min": 30,
"name": "tokenKey",
@@ -713,7 +722,7 @@ func init() {
},
{
"hidden": false,
"id": "bool1547992806",
"id": "bool@TEST_RANDOM",
"name": "emailVisibility",
"presentable": false,
"required": false,
@@ -733,11 +742,11 @@ func init() {
"fileToken": {
"duration": 180
},
"id": "_pbc_4032078523",
"id": "@TEST_RANDOM",
"indexes": [
"create index test on test123 (id)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey__pbc_4032078523` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email__pbc_4032078523` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
],
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
"manageRule": "1 != 2",
@@ -859,9 +868,16 @@ func init() {
if err != nil {
t.Fatalf("Failed to read the generated migration file: %v", err)
}
contentStr := strings.TrimSpace(string(content))
if v := strings.TrimSpace(string(content)); v != strings.TrimSpace(s.expectedTemplate) {
t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, v)
// replace @TEST_RANDOM placeholder with a regex pattern
expectedTemplate := strings.ReplaceAll(
"^"+regexp.QuoteMeta(strings.TrimSpace(s.expectedTemplate))+"$",
"@TEST_RANDOM",
`\w+`,
)
if !list.ExistInSliceWithRegex(contentStr, []string{expectedTemplate}) {
t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, contentStr)
}
})
}
@@ -879,7 +895,7 @@ func TestAutomigrateCollectionUpdate(t *testing.T) {
`
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("_pbc_4032078523")
const collection = app.findCollectionByNameOrId("@TEST_RANDOM")
// update collection data
unmarshal({
@@ -890,8 +906,8 @@ migrate((app) => {
},
"indexes": [
"create index test1 on test123_update (f1_name)",
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey__pbc_4032078523` + "`" + ` ON ` + "`" + `test123_update` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email__pbc_4032078523` + "`" + ` ON ` + "`" + `test123_update` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123_update` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123_update` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
],
"listRule": "@request.auth.id != ''",
"name": "test123_update",
@@ -936,7 +952,7 @@ migrate((app) => {
return app.save(collection)
}, (app) => {
const collection = app.findCollectionByNameOrId("_pbc_4032078523")
const collection = app.findCollectionByNameOrId("@TEST_RANDOM")
// update collection data
unmarshal({
@@ -947,8 +963,8 @@ migrate((app) => {
},
"indexes": [
"create index test1 on test123 (f1_name)",
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey__pbc_4032078523` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email__pbc_4032078523` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
"CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
"CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
],
"listRule": "@request.auth.id != '' && 1 != 2",
"name": "test123",
@@ -1005,7 +1021,7 @@ import (
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("_pbc_4032078523")
collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
if err != nil {
return err
}
@@ -1019,8 +1035,8 @@ func init() {
},
"indexes": [
"create index test1 on test123_update (f1_name)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey__pbc_4032078523` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123_update` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email__pbc_4032078523` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123_update` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123_update` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123_update` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
],
"listRule": "@request.auth.id != ''",
"name": "test123_update",
@@ -1071,7 +1087,7 @@ func init() {
return app.Save(collection)
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("_pbc_4032078523")
collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
if err != nil {
return err
}
@@ -1085,8 +1101,8 @@ func init() {
},
"indexes": [
"create index test1 on test123 (f1_name)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey__pbc_4032078523` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email__pbc_4032078523` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
],
"listRule": "@request.auth.id != '' && 1 != 2",
"name": "test123",
@@ -1240,9 +1256,16 @@ func init() {
if err != nil {
t.Fatalf("Failed to read the generated migration file: %v", err)
}
contentStr := strings.TrimSpace(string(content))
if v := strings.TrimSpace(string(content)); v != strings.TrimSpace(s.expectedTemplate) {
t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, v)
// replace @TEST_RANDOM placeholder with a regex pattern
expectedTemplate := strings.ReplaceAll(
"^"+regexp.QuoteMeta(strings.TrimSpace(s.expectedTemplate))+"$",
"@TEST_RANDOM",
`\w+`,
)
if !list.ExistInSliceWithRegex(contentStr, []string{expectedTemplate}) {
t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, contentStr)
}
})
}
+7 -7
View File
@@ -60,13 +60,13 @@ type Config struct {
}
// New creates a new PocketBase instance with the default configuration.
// Use [NewWithConfig()] if you want to provide a custom configuration.
// Use [NewWithConfig] if you want to provide a custom configuration.
//
// Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when [Start()] is executed.
// If you want to initialize the application before calling [Start()],
// then you'll have to manually call [Bootstrap()].
// Everything will be initialized when [PocketBase.Start()] is executed.
// If you want to initialize the application before calling [PocketBase.Start()],
// then you'll have to manually call [PocketBase.Bootstrap()].
func New() *PocketBase {
_, isUsingGoRun := inspectRuntime()
@@ -79,9 +79,9 @@ func New() *PocketBase {
//
// Note that the application will not be initialized/bootstrapped yet,
// aka. DB connections, migrations, app settings, etc. will not be accessible.
// Everything will be initialized when [Start()] is executed.
// If you want to initialize the application before calling [Start()],
// then you'll have to manually call [Bootstrap()].
// Everything will be initialized when [PocketBase.Start()] is executed.
// If you want to initialize the application before calling [PocketBase..Start()],
// then you'll have to manually call [PocketBase.Bootstrap()].
func NewWithConfig(config Config) *PocketBase {
// initialize a default data directory based on the executable baseDir
if config.DefaultDataDir == "" {
Binary file not shown.
Binary file not shown.
+12 -100
View File
@@ -2,14 +2,8 @@ package auth
import (
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
@@ -129,8 +123,6 @@ func (p *Apple) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
return json.Marshal(claims)
}
// -------------------------------------------------------------------
func (p *Apple) parseAndVerifyIdToken(idToken string) (jwt.MapClaims, error) {
if idToken == "" {
return nil, errors.New("empty id_token")
@@ -146,6 +138,11 @@ func (p *Apple) parseAndVerifyIdToken(idToken string) (jwt.MapClaims, error) {
// validate common claims per https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user#3383769
// ---
err = claims.Valid() // exp, iat, etc.
if err != nil {
return nil, err
}
if !claims.VerifyIssuer("https://appleid.apple.com", true) {
return nil, errors.New("iss must be https://appleid.apple.com")
}
@@ -154,102 +151,17 @@ func (p *Apple) parseAndVerifyIdToken(idToken string) (jwt.MapClaims, error) {
return nil, errors.New("aud must be the developer's client_id")
}
// fetch the public key set
// validate id_token signature
//
// note: this step could be technically considered optional because we trust
// the token which is a result of direct TLS communication with the provider
// (see also https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation)
// ---
kid, _ := t.Header["kid"].(string)
if kid == "" {
return nil, errors.New("missing kid header value")
}
key, err := p.fetchJWK(kid)
err = validateIdTokenSignature(p.ctx, idToken, p.jwksURL, kid)
if err != nil {
return nil, err
}
// decode the key params per RFC 7518 (https://tools.ietf.org/html/rfc7518#section-6.3)
// and construct a valid publicKey from them
// ---
exponent, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.E, "="))
if err != nil {
return nil, err
}
modulus, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.N, "="))
if err != nil {
return nil, err
}
publicKey := &rsa.PublicKey{
// https://tools.ietf.org/html/rfc7517#appendix-A.1
E: int(big.NewInt(0).SetBytes(exponent).Uint64()),
N: big.NewInt(0).SetBytes(modulus),
}
// verify the id_token
// ---
parser := jwt.NewParser(jwt.WithValidMethods([]string{key.Alg}))
parsedToken, err := parser.Parse(idToken, func(t *jwt.Token) (any, error) {
return publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
return claims, nil
}
return nil, errors.New("the parsed id_token is invalid")
}
type jwk struct {
Kty string
Kid string
Use string
Alg string
N string
E string
}
func (p *Apple) fetchJWK(kid string) (*jwk, error) {
req, err := http.NewRequestWithContext(p.ctx, "GET", p.jwksURL, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
rawBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
// http.Client.Get doesn't treat non 2xx responses as error
if res.StatusCode >= 400 {
return nil, fmt.Errorf(
"failed to verify the provided id_token (%d):\n%s",
res.StatusCode,
string(rawBody),
)
}
jwks := struct {
Keys []*jwk
}{}
if err := json.Unmarshal(rawBody, &jwks); err != nil {
return nil, err
}
for _, key := range jwks.Keys {
if key.Kid == kid {
return key, nil
}
}
return nil, fmt.Errorf("jwk with kid %q was not found", kid)
return claims, nil
}
+8 -1
View File
@@ -92,6 +92,13 @@ type Provider interface {
// SetUserInfoURL sets the provider's UserInfoURL.
SetUserInfoURL(url string)
// Extra returns a shallow copy of any custom config data
// that the provider may be need.
Extra() map[string]any
// SetExtra updates the provider's custom config data.
SetExtra(data map[string]any)
// Client returns an http client using the provided token.
Client(token *oauth2.Token) *http.Client
@@ -144,7 +151,7 @@ func (au AuthUser) MarshalJSON() ([]byte, error) {
type alias AuthUser // prevent recursion
au2 := alias(au)
au2.AvatarURL = au.AvatarURL // ensure that the legacy field is populated
au2.AvatarUrl = au.AvatarURL // ensure that the legacy field is populated
return json.Marshal(au2)
}
+19 -1
View File
@@ -7,7 +7,7 @@ import (
)
func TestProvidersCount(t *testing.T) {
expected := 25
expected := 27
if total := len(auth.Providers); total != expected {
t.Fatalf("Expected %d providers, got %d", expected, total)
@@ -251,4 +251,22 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Planningcenter); !ok {
t.Error("Expected to be instance of *auth.Planningcenter")
}
// notion
p, err = auth.NewProviderByName(auth.NameNotion)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.Notion); !ok {
t.Error("Expected to be instance of *auth.Notion")
}
// monday
p, err = auth.NewProviderByName(auth.NameMonday)
if err != nil {
t.Errorf("Expected nil, got error %v", err)
}
if _, ok := p.(*auth.Monday); !ok {
t.Error("Expected to be instance of *auth.Monday")
}
}
+12
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"maps"
"net/http"
"golang.org/x/oauth2"
@@ -21,6 +22,7 @@ type BaseProvider struct {
userInfoURL string
scopes []string
pkce bool
extra map[string]any
}
// Context implements Provider.Context() interface method.
@@ -123,6 +125,16 @@ func (p *BaseProvider) SetUserInfoURL(url string) {
p.userInfoURL = url
}
// Extra implements Provider.Extra() interface method.
func (p *BaseProvider) Extra() map[string]any {
return maps.Clone(p.extra)
}
// SetExtra implements Provider.SetExtra() interface method.
func (p *BaseProvider) SetExtra(data map[string]any) {
p.extra = data
}
// BuildAuthURL implements Provider.BuildAuthURL() interface method.
func (p *BaseProvider) BuildAuthURL(state string, opts ...oauth2.AuthCodeOption) string {
return p.oauth2Config().AuthCodeURL(state, opts...)
+37
View File
@@ -1,7 +1,9 @@
package auth
import (
"bytes"
"context"
"encoding/json"
"testing"
"golang.org/x/oauth2"
@@ -167,6 +169,41 @@ func TestUserInfoURL(t *testing.T) {
}
}
func TestExtra(t *testing.T) {
b := BaseProvider{}
before := b.Extra()
if before != nil {
t.Fatalf("Expected extra to be empty, got %v", before)
}
extra := map[string]any{"a": 1, "b": 2}
b.SetExtra(extra)
after := b.Extra()
rawExtra, err := json.Marshal(extra)
if err != nil {
t.Fatal(err)
}
rawAfter, err := json.Marshal(after)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(rawExtra, rawAfter) {
t.Fatalf("Expected extra to be\n%s\ngot\n%s", rawExtra, rawAfter)
}
// ensure that it was shallow copied
after["b"] = 3
if d := b.Extra(); d["b"] != 2 {
t.Fatalf("Expected extra to remain unchanged, got\n%v", d)
}
}
func TestBuildAuthURL(t *testing.T) {
b := BaseProvider{
authURL: "authURL_test",
+108
View File
@@ -0,0 +1,108 @@
package auth
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/oauth2"
)
func init() {
Providers[NameMonday] = wrapFactory(NewMondayProvider)
}
var _ Provider = (*Monday)(nil)
// NameMonday is the unique name of the Monday provider.
const NameMonday = "monday"
// Monday is an auth provider for monday.com.
type Monday struct {
BaseProvider
}
// NewMondayProvider creates a new Monday provider instance with some defaults.
func NewMondayProvider() *Monday {
return &Monday{BaseProvider{
ctx: context.Background(),
displayName: "monday.com",
pkce: true,
scopes: []string{"me:read"},
authURL: "https://auth.monday.com/oauth2/authorize",
tokenURL: "https://auth.monday.com/oauth2/token",
userInfoURL: "https://api.monday.com/v2",
}}
}
// FetchAuthUser returns an AuthUser instance based on the Monday's user api.
//
// API reference: https://developer.monday.com/api-reference/reference/me
func (p *Monday) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserData(token)
if err != nil {
return nil, err
}
rawUser := map[string]any{}
if err := json.Unmarshal(data, &rawUser); err != nil {
return nil, err
}
extracted := struct {
Data struct {
Me struct {
Id string `json:"id"`
Enabled bool `json:"enabled"`
Name string `json:"name"`
Email string `json:"email"`
IsVerified bool `json:"is_verified"`
Avatar string `json:"photo_small"`
} `json:"me"`
} `json:"data"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
if !extracted.Data.Me.Enabled {
return nil, errors.New("the monday.com user account is not enabled")
}
user := &AuthUser{
Id: extracted.Data.Me.Id,
Name: extracted.Data.Me.Name,
AvatarURL: extracted.Data.Me.Avatar,
RawUser: rawUser,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}
if extracted.Data.Me.IsVerified {
user.Email = extracted.Data.Me.Email
}
user.Expiry, _ = types.ParseDateTime(token.Expiry)
return user, nil
}
// FetchRawUserData implements Provider.FetchRawUserData interface.
//
// monday.com doesn't have a UserInfo endpoint and information on the user
// is retrieved using their GraphQL API (https://developer.monday.com/api-reference/reference/me#queries)
func (p *Monday) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
query := []byte(`{"query": "query { me { id enabled name email is_verified photo_small }}"}`)
bodyReader := bytes.NewReader(query)
req, err := http.NewRequestWithContext(p.ctx, "POST", p.userInfoURL, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return p.sendRawUserInfoRequest(req, token)
}
+106
View File
@@ -0,0 +1,106 @@
package auth
import (
"context"
"encoding/json"
"net/http"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/oauth2"
)
func init() {
Providers[NameNotion] = wrapFactory(NewNotionProvider)
}
var _ Provider = (*Notion)(nil)
// NameNotion is the unique name of the Notion provider.
const NameNotion string = "notion"
// Notion allows authentication via Notion OAuth2.
type Notion struct {
BaseProvider
}
// NewNotionProvider creates new Notion provider instance with some defaults.
func NewNotionProvider() *Notion {
return &Notion{BaseProvider{
ctx: context.Background(),
displayName: "Notion",
pkce: true,
authURL: "https://api.notion.com/v1/oauth/authorize",
tokenURL: "https://api.notion.com/v1/oauth/token",
userInfoURL: "https://api.notion.com/v1/users/me",
}}
}
// FetchAuthUser returns an AuthUser instance based on the Notion's User api.
// API reference: https://developers.notion.com/reference/get-self
func (p *Notion) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token)
if err != nil {
return nil, err
}
rawUser := map[string]any{}
if err := json.Unmarshal(data, &rawUser); err != nil {
return nil, err
}
extracted := struct {
AvatarURL string `json:"avatar_url"`
Bot struct {
Owner struct {
Type string `json:"type"`
User struct {
AvatarURL string `json:"avatar_url"`
Id string `json:"id"`
Name string `json:"name"`
Person struct {
Email string `json:"email"`
} `json:"person"`
} `json:"user"`
} `json:"owner"`
WorkspaceName string `json:"workspace_name"`
} `json:"bot"`
Id string `json:"id"`
Name string `json:"name"`
Object string `json:"object"`
RequestId string `json:"request_id"`
Type string `json:"type"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: extracted.Bot.Owner.User.Id,
Name: extracted.Bot.Owner.User.Name,
Email: extracted.Bot.Owner.User.Person.Email,
AvatarURL: extracted.Bot.Owner.User.AvatarURL,
RawUser: rawUser,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}
user.Expiry, _ = types.ParseDateTime(token.Expiry)
return user, nil
}
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
//
// This differ from BaseProvider because Notion requires a version header for all requests
// (https://developers.notion.com/reference/versioning).
func (p *Notion) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Notion-Version", "2022-06-28")
return p.sendRawUserInfoRequest(req, token)
}
+190 -2
View File
@@ -2,9 +2,19 @@ package auth
import (
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
"golang.org/x/oauth2"
)
@@ -20,6 +30,13 @@ var _ Provider = (*OIDC)(nil)
const NameOIDC string = "oidc"
// OIDC allows authentication via OpenID Connect (OIDC) OAuth2 provider.
//
// If specified the user data is fetched from the userInfoURL.
// Otherwise - from the id_token payload.
//
// The provider support the following Extra config options:
// - "jwksURL" - url to the keys to validate the id_token signature (optional and used only when reading the user data from the id_token)
// - "issuers" - list of valid issuers for the iss id_token claim (optioanl and used only when reading the user data from the id_token)
type OIDC struct {
BaseProvider
}
@@ -41,8 +58,6 @@ func NewOIDCProvider() *OIDC {
// FetchAuthUser returns an AuthUser instance based the provider's user api.
//
// API reference: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
//
// @todo consider adding support for reading the user data from the id_token.
func (p *OIDC) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token)
if err != nil {
@@ -84,3 +99,176 @@ func (p *OIDC) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
return user, nil
}
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
//
// It either fetch the data from p.userInfoURL, or if not set - returns the id_token claims.
func (p *OIDC) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
if p.userInfoURL != "" {
return p.BaseProvider.FetchRawUserInfo(token)
}
claims, err := p.parseIdToken(token)
if err != nil {
return nil, err
}
return json.Marshal(claims)
}
func (p *OIDC) parseIdToken(token *oauth2.Token) (jwt.MapClaims, error) {
idToken := token.Extra("id_token").(string)
if idToken == "" {
return nil, errors.New("empty id_token")
}
claims := jwt.MapClaims{}
t, _, err := jwt.NewParser().ParseUnverified(idToken, claims)
if err != nil {
return nil, err
}
// validate common claims like exp, iat, etc.
err = claims.Valid()
if err != nil {
return nil, err
}
// validate aud
if !claims.VerifyAudience(p.clientId, true) {
return nil, errors.New("aud must be the developer's client_id")
}
// validate iss (if "issuers" extra config is set)
issuers := cast.ToStringSlice(p.Extra()["issuers"])
if len(issuers) > 0 {
var isIssValid bool
for _, issuer := range issuers {
if claims.VerifyIssuer(issuer, true) {
isIssValid = true
break
}
}
if !isIssValid {
return nil, fmt.Errorf("iss must be one of %v, got %#v", issuers, claims["iss"])
}
}
// validate signature (if "jwksURL" extra config is set)
//
// note: this step could be technically considered optional because we trust
// the token which is a result of direct TLS communication with the provider
// (see also https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation)
jwksURL := cast.ToString(p.Extra()["jwksURL"])
if jwksURL != "" {
kid, _ := t.Header["kid"].(string)
err = validateIdTokenSignature(p.ctx, idToken, jwksURL, kid)
if err != nil {
return nil, err
}
}
return claims, nil
}
func validateIdTokenSignature(ctx context.Context, idToken string, jwksURL string, kid string) error {
// fetch the public key set
// ---
if kid == "" {
return errors.New("missing kid header value")
}
key, err := fetchJWK(ctx, jwksURL, kid)
if err != nil {
return err
}
// decode the key params per RFC 7518 (https://tools.ietf.org/html/rfc7518#section-6.3)
// and construct a valid publicKey from them
// ---
exponent, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.E, "="))
if err != nil {
return err
}
modulus, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.N, "="))
if err != nil {
return err
}
publicKey := &rsa.PublicKey{
// https://tools.ietf.org/html/rfc7517#appendix-A.1
E: int(big.NewInt(0).SetBytes(exponent).Uint64()),
N: big.NewInt(0).SetBytes(modulus),
}
// verify the signiture
// ---
parser := jwt.NewParser(jwt.WithValidMethods([]string{key.Alg}))
parsedToken, err := parser.Parse(idToken, func(t *jwt.Token) (any, error) {
return publicKey, nil
})
if err != nil {
return err
}
if !parsedToken.Valid {
return errors.New("the parsed id_token is invalid")
}
return nil
}
type jwk struct {
Kty string
Kid string
Use string
Alg string
N string
E string
}
func fetchJWK(ctx context.Context, jwksURL string, kid string) (*jwk, error) {
req, err := http.NewRequestWithContext(ctx, "GET", jwksURL, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
rawBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
// http.Client.Get doesn't treat non 2xx responses as error
if res.StatusCode >= 400 {
return nil, fmt.Errorf(
"failed to verify the provided id_token (%d):\n%s",
res.StatusCode,
string(rawBody),
)
}
jwks := struct {
Keys []*jwk
}{}
if err := json.Unmarshal(rawBody, &jwks); err != nil {
return nil, err
}
for _, key := range jwks.Keys {
if key.Kid == kid {
return key, nil
}
}
return nil, fmt.Errorf("jwk with kid %q was not found", kid)
}
+3 -3
View File
@@ -85,11 +85,11 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
return user, nil
}
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface.
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
//
// This differ from baseProvider because Twitch requires the `Client-Id` header.
// This differ from BaseProvider because Twitch requires the Client-Id header.
func (p *Twitch) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
req, err := http.NewRequest("GET", p.userInfoURL, nil)
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -108,7 +108,7 @@ func (h *Hook[T]) Bind(handler *Handler[T]) string {
// The registered handler is added with a default 0 priority and the id will be autogenerated.
//
// If you want to register a handler with custom priority or id use the [Hook.Bind] method.
func (h *Hook[T]) BindFunc(fn func(T) error) string {
func (h *Hook[T]) BindFunc(fn func(e T) error) string {
return h.Bind(&Handler[T]{Func: fn})
}
+1 -1
View File
@@ -73,7 +73,7 @@ func (h *TaggedHook[T]) Bind(handler *Handler[T]) string {
//
// It is similar to [Hook.Bind] with the difference that the handler
// function is invoked only if the event data tags satisfy h.CanTriggerOn.
func (h *TaggedHook[T]) BindFunc(fn func(T) error) string {
func (h *TaggedHook[T]) BindFunc(fn func(e T) error) string {
return h.mainHook.BindFunc(func(e T) error {
if h.CanTriggerOn(e.Tags()) {
return fn(e)
+24 -3
View File
@@ -345,12 +345,33 @@ func (e *Event) InternalServerError(message string, errData any) *ApiError {
const DefaultMaxMemory = 32 << 20 // 32mb
// Supports the following content-types:
// BindBody unmarshal the request body into the provided dst.
//
// dst must be either a struct pointer or map[string]any.
//
// The rules how the body will be scanned depends on the request Content-Type.
//
// Currently the following Content-Types are supported:
// - application/json
// - multipart/form-data
// - application/x-www-form-urlencoded
// - text/xml, application/xml
// - multipart/form-data, application/x-www-form-urlencoded
//
// Respectively the following struct tags are supported (again, which one will be used depends on the Content-Type):
// - "json" (json body)- uses the builtin Go json package for unmarshaling.
// - "xml" (xml body) - uses the builtin Go xml package for unmarshaling.
// - "form" (form data) - utilizes the custom [router.UnmarshalRequestData] method.
//
// NB! When dst is a struct make sure that it doesn't have public fields
// that shouldn't be bindable and it is advisible such fields to be unexported
// or have a separate struct just for the binding. For example:
//
// data := struct{
// somethingPrivate string
//
// Title string `json:"title" form:"title"`
// Total int `json:"total" form:"total"`
// }
// err := e.BindBody(&data)
func (e *Event) BindBody(dst any) error {
if e.Request.ContentLength == 0 {
return nil
+24 -19
View File
@@ -45,8 +45,8 @@ func (group *RouterGroup[T]) Group(prefix string) *RouterGroup[T] {
// aka. executes in the order they were registered.
//
// If you need to specify a named middleware (ex. so that it can be removed)
// or middleware with custom exec prirority, use [Group.Bind] method.
func (group *RouterGroup[T]) BindFunc(middlewareFuncs ...func(T) error) *RouterGroup[T] {
// or middleware with custom exec prirority, use [RouterGroup.Bind] method.
func (group *RouterGroup[T]) BindFunc(middlewareFuncs ...func(e T) error) *RouterGroup[T] {
for _, m := range middlewareFuncs {
group.Middlewares = append(group.Middlewares, &hook.Handler[T]{Func: m})
}
@@ -115,7 +115,7 @@ func (group *RouterGroup[T]) Unbind(middlewareIds ...string) *RouterGroup[T] {
// meaning that only a top level group route could have HOST as part of the prefix.
//
// Returns the newly created route to allow attaching route-only middlewares.
func (group *RouterGroup[T]) Route(method string, path string, action func(T) error) *Route[T] {
func (group *RouterGroup[T]) Route(method string, path string, action func(e T) error) *Route[T] {
route := &Route[T]{
Method: method,
Path: path,
@@ -127,43 +127,48 @@ func (group *RouterGroup[T]) Route(method string, path string, action func(T) er
return route
}
// Any is a shorthand for [Group.AddRoute] with "" as route method (aka. matches any method).
func (group *RouterGroup[T]) Any(path string, action func(T) error) *Route[T] {
// Any is a shorthand for [RouterGroup.AddRoute] with "" as route method (aka. matches any method).
func (group *RouterGroup[T]) Any(path string, action func(e T) error) *Route[T] {
return group.Route("", path, action)
}
// GET is a shorthand for [Group.AddRoute] with GET as route method.
func (group *RouterGroup[T]) GET(path string, action func(T) error) *Route[T] {
// GET is a shorthand for [RouterGroup.AddRoute] with GET as route method.
func (group *RouterGroup[T]) GET(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodGet, path, action)
}
// POST is a shorthand for [Group.AddRoute] with POST as route method.
func (group *RouterGroup[T]) POST(path string, action func(T) error) *Route[T] {
// SEARCH is a shorthand for [RouterGroup.AddRoute] with SEARCH as route method.
func (group *RouterGroup[T]) SEARCH(path string, action func(e T) error) *Route[T] {
return group.Route("SEARCH", path, action)
}
// POST is a shorthand for [RouterGroup.AddRoute] with POST as route method.
func (group *RouterGroup[T]) POST(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodPost, path, action)
}
// DELETE is a shorthand for [Group.AddRoute] with DELETE as route method.
func (group *RouterGroup[T]) DELETE(path string, action func(T) error) *Route[T] {
// DELETE is a shorthand for [RouterGroup.AddRoute] with DELETE as route method.
func (group *RouterGroup[T]) DELETE(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodDelete, path, action)
}
// PATCH is a shorthand for [Group.AddRoute] with PATCH as route method.
func (group *RouterGroup[T]) PATCH(path string, action func(T) error) *Route[T] {
// PATCH is a shorthand for [RouterGroup.AddRoute] with PATCH as route method.
func (group *RouterGroup[T]) PATCH(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodPatch, path, action)
}
// PUT is a shorthand for [Group.AddRoute] with PUT as route method.
func (group *RouterGroup[T]) PUT(path string, action func(T) error) *Route[T] {
// PUT is a shorthand for [RouterGroup.AddRoute] with PUT as route method.
func (group *RouterGroup[T]) PUT(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodPut, path, action)
}
// HEAD is a shorthand for [Group.AddRoute] with HEAD as route method.
func (group *RouterGroup[T]) HEAD(path string, action func(T) error) *Route[T] {
// HEAD is a shorthand for [RouterGroup.AddRoute] with HEAD as route method.
func (group *RouterGroup[T]) HEAD(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodHead, path, action)
}
// OPTIONS is a shorthand for [Group.AddRoute] with OPTIONS as route method.
func (group *RouterGroup[T]) OPTIONS(path string, action func(T) error) *Route[T] {
// OPTIONS is a shorthand for [RouterGroup.AddRoute] with OPTIONS as route method.
func (group *RouterGroup[T]) OPTIONS(path string, action func(e T) error) *Route[T] {
return group.Route(http.MethodOptions, path, action)
}
+5
View File
@@ -260,6 +260,11 @@ func TestRouterGroupRouteAliases(t *testing.T) {
http.MethodGet,
"/test",
},
{
group.SEARCH("/test", testAction),
"SEARCH",
"/test",
},
{
group.POST("/test", testAction),
http.MethodPost,
+3 -3
View File
@@ -5,7 +5,7 @@ import "github.com/pocketbase/pocketbase/tools/hook"
type Route[T hook.Resolver] struct {
excludedMiddlewares map[string]struct{}
Action func(T) error
Action func(e T) error
Method string
Path string
Middlewares []*hook.Handler[T]
@@ -17,8 +17,8 @@ type Route[T hook.Resolver] struct {
// aka. executes in the order they were registered.
//
// If you need to specify a named middleware (ex. so that it can be removed)
// or middleware with custom exec prirority, use the [Bind] method.
func (route *Route[T]) BindFunc(middlewareFuncs ...func(T) error) *Route[T] {
// or middleware with custom exec prirority, use the [Route.Bind] method.
func (route *Route[T]) BindFunc(middlewareFuncs ...func(e T) error) *Route[T] {
for _, m := range middlewareFuncs {
route.Middlewares = append(route.Middlewares, &hook.Handler[T]{Func: m})
}
+2 -35
View File
@@ -4,12 +4,10 @@ import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"runtime"
"github.com/pocketbase/pocketbase/tools/hook"
)
@@ -44,6 +42,8 @@ type EventFactoryFunc[T hook.Resolver] func(w http.ResponseWriter, r *http.Reque
//
// http.ListenAndServe("localhost:8090", mux)
type Router[T hook.Resolver] struct {
// @todo consider renaming the type to just Group and replace the embed type
// with an alias after Go 1.24 adds support for generic type aliases
*RouterGroup[T]
eventFactory EventFactoryFunc[T]
@@ -127,12 +127,6 @@ func (r *Router[T]) loadMux(mux *http.ServeMux, group *RouterGroup[T], parents [
}
}
// add global panic-recover middleware
routeHook.Bind(&hook.Handler[T]{
Func: r.panicHandler,
Priority: -9999999, // before everything else
})
mux.HandleFunc(pattern, func(resp http.ResponseWriter, req *http.Request) {
// wrap the response to add write and status tracking
resp = &ResponseWriter{ResponseWriter: resp}
@@ -160,33 +154,6 @@ func (r *Router[T]) loadMux(mux *http.ServeMux, group *RouterGroup[T], parents [
return nil
}
// panicHandler registers a default panic-recover handling.
func (r *Router[T]) panicHandler(event T) (err error) {
// panic-recover
defer func() {
recoverResult := recover()
if recoverResult == nil {
return
}
recoverErr, ok := recoverResult.(error)
if !ok {
recoverErr = fmt.Errorf("%v", recoverResult)
} else if errors.Is(recoverErr, http.ErrAbortHandler) {
// don't recover ErrAbortHandler so the response to the client can be aborted
panic(recoverResult)
}
stack := make([]byte, 2<<10) // 2 KB
length := runtime.Stack(stack, true)
err = NewInternalServerError("", fmt.Errorf("[PANIC RECOVER] %w %s", recoverErr, stack[:length]))
}()
err = event.Next()
return err
}
func ErrorHandler(resp http.ResponseWriter, req *http.Request, err error) {
if err == nil {
return
-5
View File
@@ -64,10 +64,6 @@ func TestRouter(t *testing.T) {
calls += "/" + e.Request.PathValue("param")
return errors.New("test") // should be normalized to an ApiError
})
g1.GET("/panic", func(e *router.Event) error {
calls += "/panic"
panic("test")
})
mux, err := r.BuildMux()
if err != nil {
@@ -98,7 +94,6 @@ func TestRouter(t *testing.T) {
{http.MethodHead, "/a/b/1", "root_m:a_b_group_m:1_get_m:/1_get:cleanup"},
{http.MethodPost, "/a/b/1", "root_m:a_b_group_m:/1_post:cleanup"},
{http.MethodGet, "/a/b/456", "root_m:a_b_group_m:/456/error:cleanup"},
{http.MethodGet, "/a/b/panic", "root_m:a_b_group_m:/panic:cleanup"},
}
for _, s := range scenarios {
+13 -6
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"reflect"
"regexp"
"strconv"
)
@@ -302,12 +303,16 @@ func setRegularReflectedValue(rv reflect.Value, value string) error {
return nil
}
var inferNumberCharsRegex = regexp.MustCompile(`^[\-\.\d]+$`)
// In order to support more seamlessly both json and multipart/form-data requests,
// the following normalization rules are applied for plain multipart string values:
// - "true" is converted to the json `true`
// - "false" is converted to the json `false`
// - numeric (non-scientific) strings are converted to json number
// - any other string (empty string too) is left as it is
// - "true" is converted to the json "true"
// - "false" is converted to the json "false"
// - numeric strings are converted to json number ONLY if the resulted
// minimal number string representation is the same as the provided raw string
// (aka. scientific notations, "Infinity", "0.0", "0001", etc. are kept as string)
// - any other string (empty string too) is left as it is
func inferValue(raw string) any {
switch raw {
case "":
@@ -318,9 +323,11 @@ func inferValue(raw string) any {
return false
default:
// try to convert to number
if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') {
//
// note: expects the provided raw string to match exactly with the minimal string representation of the parsed float
if raw[0] == '-' || (raw[0] >= '0' && raw[0] <= '9') && inferNumberCharsRegex.Match([]byte(raw)) {
v, err := strconv.ParseFloat(raw, 64)
if err == nil {
if err == nil && strconv.FormatFloat(v, 'f', -1, 64) == raw {
return v
}
}
+27 -7
View File
@@ -17,12 +17,32 @@ func TestUnmarshalRequestData(t *testing.T) {
t.Parallel()
mapData := map[string][]string{
"number1": {"1"},
"number2": {"2", "3"},
"number3": {"2.1", "-3.4"},
"string0": {""},
"string1": {"a"},
"string2": {"b", "c"},
"number1": {"1"},
"number2": {"2", "3"},
"number3": {"2.1", "-3.4"},
"number4": {"0", "-0", "0.0001"},
"string0": {""},
"string1": {"a"},
"string2": {"b", "c"},
"string3": {
"0.0",
"-0.0",
"000.1",
"000001",
"-000001",
"1.6E-35",
"10e100",
"1_000_000",
"1.000.000",
" 123 ",
"0b1",
"0xFF",
"1234A",
"Infinity",
"-Infinity",
"undefined",
"null",
},
"bool1": {"true"},
"bool2": {"true", "false"},
"mixed": {"true", "123", "test"},
@@ -308,7 +328,7 @@ func TestUnmarshalRequestData(t *testing.T) {
name: "*map[string]any",
data: mapData,
dst: pointer(map[string]any{}),
result: `{"bool1":true,"bool2":[true,false],"json_a":null,"json_b":123,"json_c":[1,2,3],"mixed":[true,123,"test"],"number1":1,"number2":[2,3],"number3":[2.1,-3.4],"string0":"","string1":"a","string2":["b","c"]}`,
result: `{"bool1":true,"bool2":[true,false],"json_a":null,"json_b":123,"json_c":[1,2,3],"mixed":[true,123,"test"],"number1":1,"number2":[2,3],"number3":[2.1,-3.4],"number4":[0,-0,0.0001],"string0":"","string1":"a","string2":["b","c"],"string3":["0.0","-0.0","000.1","000001","-000001","1.6E-35","10e100","1_000_000","1.000.000"," 123 ","0b1","0xFF","1234A","Infinity","-Infinity","undefined","null"]}`,
},
{
name: "valid pointer struct (all fields)",
+9 -7
View File
@@ -151,17 +151,19 @@ func (s *Store[T]) Set(key string, value T) {
// GetOrSet retrieves a single existing value for the provided key
// or stores a new one if it doesn't exist.
func (s *Store[T]) GetOrSet(key string, setFunc func() T) T {
s.mu.Lock()
defer s.mu.Unlock()
if s.data == nil {
s.data = make(map[string]T)
}
// lock only reads to minimize locks contention
s.mu.RLock()
v, ok := s.data[key]
s.mu.RUnlock()
if !ok {
s.mu.Lock()
v = setFunc()
if s.data == nil {
s.data = make(map[string]T)
}
s.data[key] = v
s.mu.Unlock()
}
return v
+3 -4
View File
@@ -1,9 +1,8 @@
# all environments should start with 'PB_' prefix
PB_BACKEND_URL = "../"
PB_INSTALLER_PARAM = "pbinstal"
PB_MFA_DOCS = "https://pocketbase.io/docs/@todo"
PB_RATE_LIMIT_DOCS = "https://pocketbase.io/docs/@todo"
PB_OAUTH2_EXAMPLE = "https://pocketbase.io/docs/authentication/#oauth2-integration"
PB_MFA_DOCS = "https://pocketbase.io/docs/authentication/#multi-factor-authentication"
PB_OAUTH2_EXAMPLE = "https://pocketbase.io/docs/authentication/#authenticate-with-oauth2"
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/api-rules-and-filters/"
PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling/"
PB_PROTECTED_FILE_DOCS = "https://pocketbase.io/docs/files-handling/#protected-files"
@@ -11,4 +10,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.23.0-rc4"
PB_VERSION = "v0.23.0-rc9"
@@ -1,4 +1,4 @@
import{S as Ce,i as Be,s as Te,Q as Le,T as G,e as c,w as y,b as k,c as ae,f as h,g as u,h as a,m as ne,x as I,U as $e,V as Re,k as Se,W as Ue,n as Qe,t as J,a as N,o as d,d as ie,p as oe,C as je,r as O,u as qe,R as De}from"./index-C6KFFkct.js";import{F as Ee}from"./FieldsQueryParam-9HzYyPBO.js";function ye(n,s,l){const o=n.slice();return o[8]=s[l],o}function Me(n,s,l){const o=n.slice();return o[8]=s[l],o}function Ae(n,s){let l,o=s[8].code+"",p,b,i,f;function m(){return s[6](s[8])}return{key:n,first:null,c(){l=c("button"),p=y(o),b=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(v,w){u(v,l,w),a(l,p),a(l,b),i||(f=qe(l,"click",m),i=!0)},p(v,w){s=v,w&4&&o!==(o=s[8].code+"")&&I(p,o),w&6&&O(l,"active",s[1]===s[8].code)},d(v){v&&d(l),i=!1,f()}}}function Pe(n,s){let l,o,p,b;return o=new De({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ae(o.$$.fragment),p=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(i,f){u(i,l,f),ne(o,l,null),a(l,p),b=!0},p(i,f){s=i;const m={};f&4&&(m.content=s[8].body),o.$set(m),(!b||f&6)&&O(l,"active",s[1]===s[8].code)},i(i){b||(J(o.$$.fragment,i),b=!0)},o(i){N(o.$$.fragment,i),b=!1},d(i){i&&d(l),ie(o)}}}function Fe(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,w,g=n[0].name+"",V,ce,W,M,z,L,K,A,D,re,E,R,ue,X,F=n[0].name+"",Y,de,Z,S,x,P,ee,fe,te,T,le,U,se,C,Q,$=[],me=new Map,pe,j,_=[],be=new Map,B;M=new Le({props:{js:`
import{S as Ce,i as Be,s as Te,Q as Le,T as G,e as c,w as $,b as k,c as ae,f as h,g as d,h as a,m as ne,x as I,U as ye,V as Re,k as Se,W as Ue,n as Qe,t as J,a as N,o as u,d as ie,p as oe,C as je,r as O,u as qe,R as De}from"./index-Co7n6kj7.js";import{F as Ee}from"./FieldsQueryParam-DH-ZC4qv.js";function $e(n,s,l){const o=n.slice();return o[8]=s[l],o}function Me(n,s,l){const o=n.slice();return o[8]=s[l],o}function Ae(n,s){let l,o=s[8].code+"",p,b,i,f;function m(){return s[6](s[8])}return{key:n,first:null,c(){l=c("button"),p=$(o),b=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(v,w){d(v,l,w),a(l,p),a(l,b),i||(f=qe(l,"click",m),i=!0)},p(v,w){s=v,w&4&&o!==(o=s[8].code+"")&&I(p,o),w&6&&O(l,"active",s[1]===s[8].code)},d(v){v&&u(l),i=!1,f()}}}function Pe(n,s){let l,o,p,b;return o=new De({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ae(o.$$.fragment),p=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(i,f){d(i,l,f),ne(o,l,null),a(l,p),b=!0},p(i,f){s=i;const m={};f&4&&(m.content=s[8].body),o.$set(m),(!b||f&6)&&O(l,"active",s[1]===s[8].code)},i(i){b||(J(o.$$.fragment,i),b=!0)},o(i){N(o.$$.fragment,i),b=!1},d(i){i&&u(l),ie(o)}}}function Fe(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,w,g=n[0].name+"",V,ce,W,M,z,L,K,A,D,re,E,R,de,X,F=n[0].name+"",Y,ue,Z,S,x,P,ee,fe,te,T,le,U,se,C,Q,y=[],me=new Map,pe,j,_=[],be=new Map,B;M=new Le({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${n[3]}');
@@ -14,7 +14,7 @@ import{S as Ce,i as Be,s as Te,Q as Le,T as G,e as c,w as y,b as k,c as ae,f as
...
final result = await pb.collection('${(ge=n[0])==null?void 0:ge.name}').listAuthMethods();
`}}),T=new Ee({});let H=G(n[2]);const he=e=>e[8].code;for(let e=0;e<H.length;e+=1){let t=Me(n,H,e),r=he(t);me.set(r,$[e]=Ae(r,t))}let q=G(n[2]);const _e=e=>e[8].code;for(let e=0;e<q.length;e+=1){let t=ye(n,q,e),r=_e(t);be.set(r,_[e]=Pe(r,t))}return{c(){s=c("h3"),l=y("List auth methods ("),p=y(o),b=y(")"),i=k(),f=c("div"),m=c("p"),v=y("Returns a public list with all allowed "),w=c("strong"),V=y(g),ce=y(" authentication methods."),W=k(),ae(M.$$.fragment),z=k(),L=c("h6"),L.textContent="API details",K=k(),A=c("div"),D=c("strong"),D.textContent="GET",re=k(),E=c("div"),R=c("p"),ue=y("/api/collections/"),X=c("strong"),Y=y(F),de=y("/auth-methods"),Z=k(),S=c("div"),S.textContent="Query parameters",x=k(),P=c("table"),ee=c("thead"),ee.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr>',fe=k(),te=c("tbody"),ae(T.$$.fragment),le=k(),U=c("div"),U.textContent="Responses",se=k(),C=c("div"),Q=c("div");for(let e=0;e<$.length;e+=1)$[e].c();pe=k(),j=c("div");for(let e=0;e<_.length;e+=1)_[e].c();h(s,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(L,"class","m-b-xs"),h(D,"class","label label-primary"),h(E,"class","content"),h(A,"class","alert alert-info"),h(S,"class","section-title"),h(P,"class","table-compact table-border m-b-base"),h(U,"class","section-title"),h(Q,"class","tabs-header compact combined left"),h(j,"class","tabs-content"),h(C,"class","tabs")},m(e,t){u(e,s,t),a(s,l),a(s,p),a(s,b),u(e,i,t),u(e,f,t),a(f,m),a(m,v),a(m,w),a(w,V),a(m,ce),u(e,W,t),ne(M,e,t),u(e,z,t),u(e,L,t),u(e,K,t),u(e,A,t),a(A,D),a(A,re),a(A,E),a(E,R),a(R,ue),a(R,X),a(X,Y),a(R,de),u(e,Z,t),u(e,S,t),u(e,x,t),u(e,P,t),a(P,ee),a(P,fe),a(P,te),ne(T,te,null),u(e,le,t),u(e,U,t),u(e,se,t),u(e,C,t),a(C,Q);for(let r=0;r<$.length;r+=1)$[r]&&$[r].m(Q,null);a(C,pe),a(C,j);for(let r=0;r<_.length;r+=1)_[r]&&_[r].m(j,null);B=!0},p(e,[t]){var ve,we;(!B||t&1)&&o!==(o=e[0].name+"")&&I(p,o),(!B||t&1)&&g!==(g=e[0].name+"")&&I(V,g);const r={};t&9&&(r.js=`
`}}),T=new Ee({});let H=G(n[2]);const he=e=>e[8].code;for(let e=0;e<H.length;e+=1){let t=Me(n,H,e),r=he(t);me.set(r,y[e]=Ae(r,t))}let q=G(n[2]);const _e=e=>e[8].code;for(let e=0;e<q.length;e+=1){let t=$e(n,q,e),r=_e(t);be.set(r,_[e]=Pe(r,t))}return{c(){s=c("h3"),l=$("List auth methods ("),p=$(o),b=$(")"),i=k(),f=c("div"),m=c("p"),v=$("Returns a public list with all allowed "),w=c("strong"),V=$(g),ce=$(" authentication methods."),W=k(),ae(M.$$.fragment),z=k(),L=c("h6"),L.textContent="API details",K=k(),A=c("div"),D=c("strong"),D.textContent="GET",re=k(),E=c("div"),R=c("p"),de=$("/api/collections/"),X=c("strong"),Y=$(F),ue=$("/auth-methods"),Z=k(),S=c("div"),S.textContent="Query parameters",x=k(),P=c("table"),ee=c("thead"),ee.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr>',fe=k(),te=c("tbody"),ae(T.$$.fragment),le=k(),U=c("div"),U.textContent="Responses",se=k(),C=c("div"),Q=c("div");for(let e=0;e<y.length;e+=1)y[e].c();pe=k(),j=c("div");for(let e=0;e<_.length;e+=1)_[e].c();h(s,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(L,"class","m-b-xs"),h(D,"class","label label-primary"),h(E,"class","content"),h(A,"class","alert alert-info"),h(S,"class","section-title"),h(P,"class","table-compact table-border m-b-base"),h(U,"class","section-title"),h(Q,"class","tabs-header compact combined left"),h(j,"class","tabs-content"),h(C,"class","tabs")},m(e,t){d(e,s,t),a(s,l),a(s,p),a(s,b),d(e,i,t),d(e,f,t),a(f,m),a(m,v),a(m,w),a(w,V),a(m,ce),d(e,W,t),ne(M,e,t),d(e,z,t),d(e,L,t),d(e,K,t),d(e,A,t),a(A,D),a(A,re),a(A,E),a(E,R),a(R,de),a(R,X),a(X,Y),a(R,ue),d(e,Z,t),d(e,S,t),d(e,x,t),d(e,P,t),a(P,ee),a(P,fe),a(P,te),ne(T,te,null),d(e,le,t),d(e,U,t),d(e,se,t),d(e,C,t),a(C,Q);for(let r=0;r<y.length;r+=1)y[r]&&y[r].m(Q,null);a(C,pe),a(C,j);for(let r=0;r<_.length;r+=1)_[r]&&_[r].m(j,null);B=!0},p(e,[t]){var ve,we;(!B||t&1)&&o!==(o=e[0].name+"")&&I(p,o),(!B||t&1)&&g!==(g=e[0].name+"")&&I(V,g);const r={};t&9&&(r.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[3]}');
@@ -30,4 +30,10 @@ import{S as Ce,i as Be,s as Te,Q as Le,T as G,e as c,w as y,b as k,c as ae,f as
...
final result = await pb.collection('${(we=e[0])==null?void 0:we.name}').listAuthMethods();
`),M.$set(r),(!B||t&1)&&F!==(F=e[0].name+"")&&I(Y,F),t&6&&(H=G(e[2]),$=$e($,t,he,1,e,H,me,Q,Re,Ae,null,Me)),t&6&&(q=G(e[2]),Se(),_=$e(_,t,_e,1,e,q,be,j,Ue,Pe,null,ye),Qe())},i(e){if(!B){J(M.$$.fragment,e),J(T.$$.fragment,e);for(let t=0;t<q.length;t+=1)J(_[t]);B=!0}},o(e){N(M.$$.fragment,e),N(T.$$.fragment,e);for(let t=0;t<_.length;t+=1)N(_[t]);B=!1},d(e){e&&(d(s),d(i),d(f),d(W),d(z),d(L),d(K),d(A),d(Z),d(S),d(x),d(P),d(le),d(U),d(se),d(C)),ie(M,e),ie(T);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function He(n,s,l){let o,{collection:p}=s,b=200,i=[],f={},m=!1;v();async function v(){l(5,m=!0);try{l(4,f=await oe.collection(p.name).listAuthMethods())}catch(g){oe.error(g)}l(5,m=!1)}const w=g=>l(1,b=g.code);return n.$$set=g=>{"collection"in g&&l(0,p=g.collection)},n.$$.update=()=>{n.$$.dirty&48&&l(2,i=[{code:200,body:m?"...":JSON.stringify(f,null,2)}])},l(3,o=je.getApiExampleUrl(oe.baseURL)),[p,b,i,o,f,m,w]}class Je extends Ce{constructor(s){super(),Be(this,s,He,Fe,Te,{collection:0})}}export{Je as default};
`),M.$set(r),(!B||t&1)&&F!==(F=e[0].name+"")&&I(Y,F),t&6&&(H=G(e[2]),y=ye(y,t,he,1,e,H,me,Q,Re,Ae,null,Me)),t&6&&(q=G(e[2]),Se(),_=ye(_,t,_e,1,e,q,be,j,Ue,Pe,null,$e),Qe())},i(e){if(!B){J(M.$$.fragment,e),J(T.$$.fragment,e);for(let t=0;t<q.length;t+=1)J(_[t]);B=!0}},o(e){N(M.$$.fragment,e),N(T.$$.fragment,e);for(let t=0;t<_.length;t+=1)N(_[t]);B=!1},d(e){e&&(u(s),u(i),u(f),u(W),u(z),u(L),u(K),u(A),u(Z),u(S),u(x),u(P),u(le),u(U),u(se),u(C)),ie(M,e),ie(T);for(let t=0;t<y.length;t+=1)y[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function He(n,s,l){let o,{collection:p}=s,b=200,i=[],f={},m=!1;v();async function v(){l(5,m=!0);try{l(4,f=await oe.collection(p.name).listAuthMethods())}catch(g){oe.error(g)}l(5,m=!1)}const w=g=>l(1,b=g.code);return n.$$set=g=>{"collection"in g&&l(0,p=g.collection)},n.$$.update=()=>{n.$$.dirty&48&&l(2,i=[{code:200,body:m?"...":JSON.stringify(f,null,2)},{code:404,body:`
{
"code": 404,
"message": "Missing collection context.",
"data": {}
}
`}])},l(3,o=je.getApiExampleUrl(oe.baseURL)),[p,b,i,o,f,m,w]}class Je extends Ce{constructor(s){super(),Be(this,s,He,Fe,Te,{collection:0})}}export{Je as default};
@@ -1,4 +1,4 @@
import{S as Qe,i as je,s as Je,Q as Ke,R as Ne,T as J,e as s,w as k,b as p,c as K,f as b,g as d,h as o,m as W,x as de,U as Oe,V as We,k as Ie,W as Ge,n as Xe,t as E,a as U,o as u,d as I,C as Ve,p as Ye,r as G,u as Ze}from"./index-C6KFFkct.js";import{F as et}from"./FieldsQueryParam-9HzYyPBO.js";function Ee(r,a,l){const n=r.slice();return n[5]=a[l],n}function Ue(r,a,l){const n=r.slice();return n[5]=a[l],n}function xe(r,a){let l,n=a[5].code+"",m,_,i,h;function g(){return a[4](a[5])}return{key:r,first:null,c(){l=s("button"),m=k(n),_=p(),b(l,"class","tab-item"),G(l,"active",a[1]===a[5].code),this.first=l},m(v,w){d(v,l,w),o(l,m),o(l,_),i||(h=Ze(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&G(l,"active",a[1]===a[5].code)},d(v){v&&u(l),i=!1,h()}}}function ze(r,a){let l,n,m,_;return n=new Ne({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),K(n.$$.fragment),m=p(),b(l,"class","tab-item"),G(l,"active",a[1]===a[5].code),this.first=l},m(i,h){d(i,l,h),W(n,l,null),o(l,m),_=!0},p(i,h){a=i;const g={};h&4&&(g.content=a[5].body),n.$set(g),(!_||h&6)&&G(l,"active",a[1]===a[5].code)},i(i){_||(E(n.$$.fragment,i),_=!0)},o(i){U(n.$$.fragment,i),_=!1},d(i){i&&u(l),I(n)}}}function tt(r){var De,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,M,X,S,x,ue,z,q,pe,Y,N=r[0].name+"",Z,he,fe,Q,ee,D,te,T,oe,be,F,C,ae,me,le,_e,f,ke,P,ge,ve,$e,se,ye,ne,Se,we,Te,re,Ce,Re,A,ie,H,ce,R,L,y=[],Pe=new Map,Ae,O,$=[],Be=new Map,B;v=new Ke({props:{js:`
import{S as Qe,i as je,s as Je,Q as Ke,R as Ne,T as J,e as s,w as k,b as p,c as K,f as b,g as d,h as o,m as W,x as de,U as Oe,V as We,k as Ie,W as Ge,n as Xe,t as E,a as U,o as u,d as I,C as Ve,p as Ye,r as G,u as Ze}from"./index-Co7n6kj7.js";import{F as et}from"./FieldsQueryParam-DH-ZC4qv.js";function Ee(r,a,l){const n=r.slice();return n[5]=a[l],n}function Ue(r,a,l){const n=r.slice();return n[5]=a[l],n}function xe(r,a){let l,n=a[5].code+"",m,_,i,h;function g(){return a[4](a[5])}return{key:r,first:null,c(){l=s("button"),m=k(n),_=p(),b(l,"class","tab-item"),G(l,"active",a[1]===a[5].code),this.first=l},m(v,w){d(v,l,w),o(l,m),o(l,_),i||(h=Ze(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&G(l,"active",a[1]===a[5].code)},d(v){v&&u(l),i=!1,h()}}}function ze(r,a){let l,n,m,_;return n=new Ne({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),K(n.$$.fragment),m=p(),b(l,"class","tab-item"),G(l,"active",a[1]===a[5].code),this.first=l},m(i,h){d(i,l,h),W(n,l,null),o(l,m),_=!0},p(i,h){a=i;const g={};h&4&&(g.content=a[5].body),n.$set(g),(!_||h&6)&&G(l,"active",a[1]===a[5].code)},i(i){_||(E(n.$$.fragment,i),_=!0)},o(i){U(n.$$.fragment,i),_=!1},d(i){i&&u(l),I(n)}}}function tt(r){var De,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,M,X,S,x,ue,z,q,pe,Y,N=r[0].name+"",Z,he,fe,Q,ee,D,te,T,oe,be,F,C,ae,me,le,_e,f,ke,P,ge,ve,$e,se,ye,ne,Se,we,Te,re,Ce,Re,A,ie,H,ce,R,L,y=[],Pe=new Map,Ae,O,$=[],Be=new Map,B;v=new Ke({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${r[3]}');
@@ -1,4 +1,4 @@
import{S as xe,i as Ee,s as Je,Q as Qe,R as je,T as z,e as o,w as k,b as h,c as I,f as p,g as r,h as a,m as K,x as pe,U as Ue,V as Ne,k as ze,W as Ie,n as Ke,t as j,a as x,o as c,d as G,C as Be,p as Ge,r as X,u as Xe}from"./index-C6KFFkct.js";import{F as Ye}from"./FieldsQueryParam-9HzYyPBO.js";function Fe(s,l,n){const i=s.slice();return i[5]=l[n],i}function Le(s,l,n){const i=s.slice();return i[5]=l[n],i}function He(s,l){let n,i=l[5].code+"",f,g,d,b;function _(){return l[4](l[5])}return{key:s,first:null,c(){n=o("button"),f=k(i),g=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(v,O){r(v,n,O),a(n,f),a(n,g),d||(b=Xe(n,"click",_),d=!0)},p(v,O){l=v,O&4&&i!==(i=l[5].code+"")&&pe(f,i),O&6&&X(n,"active",l[1]===l[5].code)},d(v){v&&c(n),d=!1,b()}}}function Ve(s,l){let n,i,f,g;return i=new je({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),I(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){r(d,n,b),K(i,n,null),a(n,f),g=!0},p(d,b){l=d;const _={};b&4&&(_.content=l[5].body),i.$set(_),(!g||b&6)&&X(n,"active",l[1]===l[5].code)},i(d){g||(j(i.$$.fragment,d),g=!0)},o(d){x(i.$$.fragment,d),g=!1},d(d){d&&c(n),G(i)}}}function Ze(s){let l,n,i=s[0].name+"",f,g,d,b,_,v,O,D,Y,A,E,be,J,P,me,Z,Q=s[0].name+"",ee,fe,te,M,ae,W,le,U,ne,y,oe,ge,B,S,se,ke,ie,_e,m,ve,C,we,$e,Oe,re,Ae,ce,ye,Se,Te,de,Ce,qe,q,ue,F,he,T,L,$=[],Re=new Map,De,H,w=[],Pe=new Map,R;v=new Qe({props:{js:`
import{S as xe,i as Ee,s as Je,Q as Qe,R as je,T as z,e as o,w as k,b as h,c as I,f as p,g as r,h as a,m as K,x as pe,U as Ue,V as Ne,k as ze,W as Ie,n as Ke,t as j,a as x,o as c,d as G,C as Be,p as Ge,r as X,u as Xe}from"./index-Co7n6kj7.js";import{F as Ye}from"./FieldsQueryParam-DH-ZC4qv.js";function Fe(s,l,n){const i=s.slice();return i[5]=l[n],i}function Le(s,l,n){const i=s.slice();return i[5]=l[n],i}function He(s,l){let n,i=l[5].code+"",f,g,d,b;function _(){return l[4](l[5])}return{key:s,first:null,c(){n=o("button"),f=k(i),g=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(v,O){r(v,n,O),a(n,f),a(n,g),d||(b=Xe(n,"click",_),d=!0)},p(v,O){l=v,O&4&&i!==(i=l[5].code+"")&&pe(f,i),O&6&&X(n,"active",l[1]===l[5].code)},d(v){v&&c(n),d=!1,b()}}}function Ve(s,l){let n,i,f,g;return i=new je({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),I(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){r(d,n,b),K(i,n,null),a(n,f),g=!0},p(d,b){l=d;const _={};b&4&&(_.content=l[5].body),i.$set(_),(!g||b&6)&&X(n,"active",l[1]===l[5].code)},i(d){g||(j(i.$$.fragment,d),g=!0)},o(d){x(i.$$.fragment,d),g=!1},d(d){d&&c(n),G(i)}}}function Ze(s){let l,n,i=s[0].name+"",f,g,d,b,_,v,O,D,Y,A,E,be,J,P,me,Z,Q=s[0].name+"",ee,fe,te,M,ae,W,le,U,ne,y,oe,ge,B,S,se,ke,ie,_e,m,ve,C,we,$e,Oe,re,Ae,ce,ye,Se,Te,de,Ce,qe,q,ue,F,he,T,L,$=[],Re=new Map,De,H,w=[],Pe=new Map,R;v=new Qe({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${s[3]}');
@@ -46,7 +46,7 @@ import{S as xe,i as Ee,s as Je,Q as Qe,R as je,T as z,e as o,w as k,b as h,c as
// "logout"
pb.authStore.clear();
`}}),C=new je({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new Ye({props:{prefix:"record."}});let N=z(s[2]);const Me=e=>e[5].code;for(let e=0;e<N.length;e+=1){let t=Le(s,N,e),u=Me(t);Re.set(u,$[e]=He(u,t))}let V=z(s[2]);const We=e=>e[5].code;for(let e=0;e<V.length;e+=1){let t=Fe(s,V,e),u=We(t);Pe.set(u,w[e]=Ve(u,t))}return{c(){l=o("h3"),n=k("Auth with OAuth2 ("),f=k(i),g=k(")"),d=h(),b=o("div"),b.innerHTML=`<p>Authenticate with an OAuth2 provider and returns a new auth token and record data.</p> <p>For more details please check the
<a href="https://pocketbase.io/docs/authentication/#oauth2-integration" target="_blank" rel="noopener noreferrer">OAuth2 integration documentation
<a href="https://pocketbase.io/docs/authentication/#authenticate-with-oauth2" target="_blank" rel="noopener noreferrer">OAuth2 integration documentation
</a>.</p>`,_=h(),I(v.$$.fragment),O=h(),D=o("h6"),D.textContent="API details",Y=h(),A=o("div"),E=o("strong"),E.textContent="POST",be=h(),J=o("div"),P=o("p"),me=k("/api/collections/"),Z=o("strong"),ee=k(Q),fe=k("/auth-with-oauth2"),te=h(),M=o("div"),M.textContent="Body Parameters",ae=h(),W=o("table"),W.innerHTML=`<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>provider</span></div></td> <td><span class="label">String</span></td> <td>The name of the OAuth2 client provider (eg. &quot;google&quot;).</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>code</span></div></td> <td><span class="label">String</span></td> <td>The authorization code returned from the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>codeVerifier</span></div></td> <td><span class="label">String</span></td> <td>The code verifier sent with the initial request as part of the code_challenge.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>redirectURL</span></div></td> <td><span class="label">String</span></td> <td>The redirect url sent with the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-warning">Optional</span> <span>createData</span></div></td> <td><span class="label">Object</span></td> <td><p>Optional data that will be used when creating the auth record on OAuth2 sign-up.</p> <p>The created auth record must comply with the same requirements and validations in the
regular <strong>create</strong> action.
<br/> <em>The data can only be in <code>json</code>, aka. <code>multipart/form-data</code> and files
@@ -1,4 +1,4 @@
import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as z,U as G,V as ge,k as Z,W as ke,n as x,t as L,a as J,o as _,C as oe,r as Y,u as ae,R as we,c as K,m as Q,d as X,Q as $e,X as se,p as Te,Y as ne}from"./index-C6KFFkct.js";function ie(s,t,e){const a=s.slice();return a[4]=t[e],a}function ce(s,t,e){const a=s.slice();return a[4]=t[e],a}function re(s,t){let e,a=t[4].code+"",d,c,r,n;function u(){return t[3](t[4])}return{key:s,first:null,c(){e=p("button"),d=V(a),c=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(m,q){b(m,e,q),h(e,d),h(e,c),r||(n=ae(e,"click",u),r=!0)},p(m,q){t=m,q&4&&a!==(a=t[4].code+"")&&z(d,a),q&6&&Y(e,"active",t[1]===t[4].code)},d(m){m&&_(e),r=!1,n()}}}function de(s,t){let e,a,d,c;return a=new we({props:{content:t[4].body}}),{key:s,first:null,c(){e=p("div"),K(a.$$.fragment),d=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(r,n){b(r,e,n),Q(a,e,null),h(e,d),c=!0},p(r,n){t=r;const u={};n&4&&(u.content=t[4].body),a.$set(u),(!c||n&6)&&Y(e,"active",t[1]===t[4].code)},i(r){c||(L(a.$$.fragment,r),c=!0)},o(r){J(a.$$.fragment,r),c=!1},d(r){r&&_(e),X(a)}}}function Pe(s){let t,e,a,d,c,r,n,u=s[0].name+"",m,q,W,C,B,A,H,R,I,y,P,w=[],$=new Map,E,D,k=[],N=new Map,M,i=U(s[2]);const v=l=>l[4].code;for(let l=0;l<i.length;l+=1){let o=ce(s,i,l),f=v(o);$.set(f,w[l]=re(f,o))}let O=U(s[2]);const j=l=>l[4].code;for(let l=0;l<O.length;l+=1){let o=ie(s,O,l),f=j(o);N.set(f,k[l]=de(f,o))}return{c(){t=p("div"),e=p("strong"),e.textContent="POST",a=S(),d=p("div"),c=p("p"),r=V("/api/collections/"),n=p("strong"),m=V(u),q=V("/auth-with-otp"),W=S(),C=p("div"),C.textContent="Body Parameters",B=S(),A=p("table"),A.innerHTML='<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>otpId</span></div></td> <td><span class="label">String</span></td> <td>The id of the OTP request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>password</span></div></td> <td><span class="label">String</span></td> <td>The one-time password.</td></tr></tbody>',H=S(),R=p("div"),R.textContent="Responses",I=S(),y=p("div"),P=p("div");for(let l=0;l<w.length;l+=1)w[l].c();E=S(),D=p("div");for(let l=0;l<k.length;l+=1)k[l].c();g(e,"class","label label-primary"),g(d,"class","content"),g(t,"class","alert alert-success"),g(C,"class","section-title"),g(A,"class","table-compact table-border m-b-base"),g(R,"class","section-title"),g(P,"class","tabs-header compact combined left"),g(D,"class","tabs-content"),g(y,"class","tabs")},m(l,o){b(l,t,o),h(t,e),h(t,a),h(t,d),h(d,c),h(c,r),h(c,n),h(n,m),h(c,q),b(l,W,o),b(l,C,o),b(l,B,o),b(l,A,o),b(l,H,o),b(l,R,o),b(l,I,o),b(l,y,o),h(y,P);for(let f=0;f<w.length;f+=1)w[f]&&w[f].m(P,null);h(y,E),h(y,D);for(let f=0;f<k.length;f+=1)k[f]&&k[f].m(D,null);M=!0},p(l,[o]){(!M||o&1)&&u!==(u=l[0].name+"")&&z(m,u),o&6&&(i=U(l[2]),w=G(w,o,v,1,l,i,$,P,ge,re,null,ce)),o&6&&(O=U(l[2]),Z(),k=G(k,o,j,1,l,O,N,D,ke,de,null,ie),x())},i(l){if(!M){for(let o=0;o<O.length;o+=1)L(k[o]);M=!0}},o(l){for(let o=0;o<k.length;o+=1)J(k[o]);M=!1},d(l){l&&(_(t),_(W),_(C),_(B),_(A),_(H),_(R),_(I),_(y));for(let o=0;o<w.length;o+=1)w[o].d();for(let o=0;o<k.length;o+=1)k[o].d()}}}function Oe(s,t,e){let{collection:a}=t,d=200,c=[];const r=n=>e(1,d=n.code);return s.$$set=n=>{"collection"in n&&e(0,a=n.collection)},s.$$.update=()=>{s.$$.dirty&1&&e(2,c=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:oe.dummyCollectionRecord(a)},null,2)},{code:400,body:`
import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as z,U as G,V as ge,k as Z,W as ke,n as x,t as L,a as N,o as _,C as oe,r as Y,u as ae,R as we,c as K,m as Q,d as X,Q as $e,X as se,p as Te,Y as ne}from"./index-Co7n6kj7.js";function ie(s,t,e){const a=s.slice();return a[4]=t[e],a}function ce(s,t,e){const a=s.slice();return a[4]=t[e],a}function re(s,t){let e,a=t[4].code+"",d,c,r,n;function u(){return t[3](t[4])}return{key:s,first:null,c(){e=p("button"),d=V(a),c=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(m,q){b(m,e,q),h(e,d),h(e,c),r||(n=ae(e,"click",u),r=!0)},p(m,q){t=m,q&4&&a!==(a=t[4].code+"")&&z(d,a),q&6&&Y(e,"active",t[1]===t[4].code)},d(m){m&&_(e),r=!1,n()}}}function de(s,t){let e,a,d,c;return a=new we({props:{content:t[4].body}}),{key:s,first:null,c(){e=p("div"),K(a.$$.fragment),d=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(r,n){b(r,e,n),Q(a,e,null),h(e,d),c=!0},p(r,n){t=r;const u={};n&4&&(u.content=t[4].body),a.$set(u),(!c||n&6)&&Y(e,"active",t[1]===t[4].code)},i(r){c||(L(a.$$.fragment,r),c=!0)},o(r){N(a.$$.fragment,r),c=!1},d(r){r&&_(e),X(a)}}}function Pe(s){let t,e,a,d,c,r,n,u=s[0].name+"",m,q,M,C,B,A,H,R,W,y,P,w=[],$=new Map,E,D,k=[],J=new Map,I,i=U(s[2]);const v=l=>l[4].code;for(let l=0;l<i.length;l+=1){let o=ce(s,i,l),f=v(o);$.set(f,w[l]=re(f,o))}let O=U(s[2]);const j=l=>l[4].code;for(let l=0;l<O.length;l+=1){let o=ie(s,O,l),f=j(o);J.set(f,k[l]=de(f,o))}return{c(){t=p("div"),e=p("strong"),e.textContent="POST",a=S(),d=p("div"),c=p("p"),r=V("/api/collections/"),n=p("strong"),m=V(u),q=V("/auth-with-otp"),M=S(),C=p("div"),C.textContent="Body Parameters",B=S(),A=p("table"),A.innerHTML='<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>otpId</span></div></td> <td><span class="label">String</span></td> <td>The id of the OTP request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>password</span></div></td> <td><span class="label">String</span></td> <td>The one-time password.</td></tr></tbody>',H=S(),R=p("div"),R.textContent="Responses",W=S(),y=p("div"),P=p("div");for(let l=0;l<w.length;l+=1)w[l].c();E=S(),D=p("div");for(let l=0;l<k.length;l+=1)k[l].c();g(e,"class","label label-primary"),g(d,"class","content"),g(t,"class","alert alert-success"),g(C,"class","section-title"),g(A,"class","table-compact table-border m-b-base"),g(R,"class","section-title"),g(P,"class","tabs-header compact combined left"),g(D,"class","tabs-content"),g(y,"class","tabs")},m(l,o){b(l,t,o),h(t,e),h(t,a),h(t,d),h(d,c),h(c,r),h(c,n),h(n,m),h(c,q),b(l,M,o),b(l,C,o),b(l,B,o),b(l,A,o),b(l,H,o),b(l,R,o),b(l,W,o),b(l,y,o),h(y,P);for(let f=0;f<w.length;f+=1)w[f]&&w[f].m(P,null);h(y,E),h(y,D);for(let f=0;f<k.length;f+=1)k[f]&&k[f].m(D,null);I=!0},p(l,[o]){(!I||o&1)&&u!==(u=l[0].name+"")&&z(m,u),o&6&&(i=U(l[2]),w=G(w,o,v,1,l,i,$,P,ge,re,null,ce)),o&6&&(O=U(l[2]),Z(),k=G(k,o,j,1,l,O,J,D,ke,de,null,ie),x())},i(l){if(!I){for(let o=0;o<O.length;o+=1)L(k[o]);I=!0}},o(l){for(let o=0;o<k.length;o+=1)N(k[o]);I=!1},d(l){l&&(_(t),_(M),_(C),_(B),_(A),_(H),_(R),_(W),_(y));for(let o=0;o<w.length;o+=1)w[o].d();for(let o=0;o<k.length;o+=1)k[o].d()}}}function Oe(s,t,e){let{collection:a}=t,d=200,c=[];const r=n=>e(1,d=n.code);return s.$$set=n=>{"collection"in n&&e(0,a=n.collection)},s.$$.update=()=>{s.$$.dirty&1&&e(2,c=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:oe.dummyCollectionRecord(a)},null,2)},{code:400,body:`
{
"code": 400,
"message": "Failed to authenticate.",
@@ -9,7 +9,7 @@ import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as
}
}
}
`}])},[a,d,c,r]}class Se extends ee{constructor(t){super(),te(this,t,Oe,Pe,le,{collection:0})}}function ue(s,t,e){const a=s.slice();return a[4]=t[e],a}function he(s,t,e){const a=s.slice();return a[4]=t[e],a}function pe(s,t){let e,a=t[4].code+"",d,c,r,n;function u(){return t[3](t[4])}return{key:s,first:null,c(){e=p("button"),d=V(a),c=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(m,q){b(m,e,q),h(e,d),h(e,c),r||(n=ae(e,"click",u),r=!0)},p(m,q){t=m,q&4&&a!==(a=t[4].code+"")&&z(d,a),q&6&&Y(e,"active",t[1]===t[4].code)},d(m){m&&_(e),r=!1,n()}}}function fe(s,t){let e,a,d,c;return a=new we({props:{content:t[4].body}}),{key:s,first:null,c(){e=p("div"),K(a.$$.fragment),d=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(r,n){b(r,e,n),Q(a,e,null),h(e,d),c=!0},p(r,n){t=r;const u={};n&4&&(u.content=t[4].body),a.$set(u),(!c||n&6)&&Y(e,"active",t[1]===t[4].code)},i(r){c||(L(a.$$.fragment,r),c=!0)},o(r){J(a.$$.fragment,r),c=!1},d(r){r&&_(e),X(a)}}}function ye(s){let t,e,a,d,c,r,n,u=s[0].name+"",m,q,W,C,B,A,H,R,I,y,P,w=[],$=new Map,E,D,k=[],N=new Map,M,i=U(s[2]);const v=l=>l[4].code;for(let l=0;l<i.length;l+=1){let o=he(s,i,l),f=v(o);$.set(f,w[l]=pe(f,o))}let O=U(s[2]);const j=l=>l[4].code;for(let l=0;l<O.length;l+=1){let o=ue(s,O,l),f=j(o);N.set(f,k[l]=fe(f,o))}return{c(){t=p("div"),e=p("strong"),e.textContent="POST",a=S(),d=p("div"),c=p("p"),r=V("/api/collections/"),n=p("strong"),m=V(u),q=V("/request-otp"),W=S(),C=p("div"),C.textContent="Body Parameters",B=S(),A=p("table"),A.innerHTML='<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>email</span></div></td> <td><span class="label">String</span></td> <td>The auth record email address to send the OTP request (if exists).</td></tr></tbody>',H=S(),R=p("div"),R.textContent="Responses",I=S(),y=p("div"),P=p("div");for(let l=0;l<w.length;l+=1)w[l].c();E=S(),D=p("div");for(let l=0;l<k.length;l+=1)k[l].c();g(e,"class","label label-primary"),g(d,"class","content"),g(t,"class","alert alert-success"),g(C,"class","section-title"),g(A,"class","table-compact table-border m-b-base"),g(R,"class","section-title"),g(P,"class","tabs-header compact combined left"),g(D,"class","tabs-content"),g(y,"class","tabs")},m(l,o){b(l,t,o),h(t,e),h(t,a),h(t,d),h(d,c),h(c,r),h(c,n),h(n,m),h(c,q),b(l,W,o),b(l,C,o),b(l,B,o),b(l,A,o),b(l,H,o),b(l,R,o),b(l,I,o),b(l,y,o),h(y,P);for(let f=0;f<w.length;f+=1)w[f]&&w[f].m(P,null);h(y,E),h(y,D);for(let f=0;f<k.length;f+=1)k[f]&&k[f].m(D,null);M=!0},p(l,[o]){(!M||o&1)&&u!==(u=l[0].name+"")&&z(m,u),o&6&&(i=U(l[2]),w=G(w,o,v,1,l,i,$,P,ge,pe,null,he)),o&6&&(O=U(l[2]),Z(),k=G(k,o,j,1,l,O,N,D,ke,fe,null,ue),x())},i(l){if(!M){for(let o=0;o<O.length;o+=1)L(k[o]);M=!0}},o(l){for(let o=0;o<k.length;o+=1)J(k[o]);M=!1},d(l){l&&(_(t),_(W),_(C),_(B),_(A),_(H),_(R),_(I),_(y));for(let o=0;o<w.length;o+=1)w[o].d();for(let o=0;o<k.length;o+=1)k[o].d()}}}function qe(s,t,e){let{collection:a}=t,d=200,c=[];const r=n=>e(1,d=n.code);return s.$$set=n=>{"collection"in n&&e(0,a=n.collection)},e(2,c=[{code:200,body:JSON.stringify({otpId:oe.randomString(15)},null,2)},{code:400,body:`
`}])},[a,d,c,r]}class Se extends ee{constructor(t){super(),te(this,t,Oe,Pe,le,{collection:0})}}function ue(s,t,e){const a=s.slice();return a[4]=t[e],a}function he(s,t,e){const a=s.slice();return a[4]=t[e],a}function pe(s,t){let e,a=t[4].code+"",d,c,r,n;function u(){return t[3](t[4])}return{key:s,first:null,c(){e=p("button"),d=V(a),c=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(m,q){b(m,e,q),h(e,d),h(e,c),r||(n=ae(e,"click",u),r=!0)},p(m,q){t=m,q&4&&a!==(a=t[4].code+"")&&z(d,a),q&6&&Y(e,"active",t[1]===t[4].code)},d(m){m&&_(e),r=!1,n()}}}function fe(s,t){let e,a,d,c;return a=new we({props:{content:t[4].body}}),{key:s,first:null,c(){e=p("div"),K(a.$$.fragment),d=S(),g(e,"class","tab-item"),Y(e,"active",t[1]===t[4].code),this.first=e},m(r,n){b(r,e,n),Q(a,e,null),h(e,d),c=!0},p(r,n){t=r;const u={};n&4&&(u.content=t[4].body),a.$set(u),(!c||n&6)&&Y(e,"active",t[1]===t[4].code)},i(r){c||(L(a.$$.fragment,r),c=!0)},o(r){N(a.$$.fragment,r),c=!1},d(r){r&&_(e),X(a)}}}function ye(s){let t,e,a,d,c,r,n,u=s[0].name+"",m,q,M,C,B,A,H,R,W,y,P,w=[],$=new Map,E,D,k=[],J=new Map,I,i=U(s[2]);const v=l=>l[4].code;for(let l=0;l<i.length;l+=1){let o=he(s,i,l),f=v(o);$.set(f,w[l]=pe(f,o))}let O=U(s[2]);const j=l=>l[4].code;for(let l=0;l<O.length;l+=1){let o=ue(s,O,l),f=j(o);J.set(f,k[l]=fe(f,o))}return{c(){t=p("div"),e=p("strong"),e.textContent="POST",a=S(),d=p("div"),c=p("p"),r=V("/api/collections/"),n=p("strong"),m=V(u),q=V("/request-otp"),M=S(),C=p("div"),C.textContent="Body Parameters",B=S(),A=p("table"),A.innerHTML='<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>email</span></div></td> <td><span class="label">String</span></td> <td>The auth record email address to send the OTP request (if exists).</td></tr></tbody>',H=S(),R=p("div"),R.textContent="Responses",W=S(),y=p("div"),P=p("div");for(let l=0;l<w.length;l+=1)w[l].c();E=S(),D=p("div");for(let l=0;l<k.length;l+=1)k[l].c();g(e,"class","label label-primary"),g(d,"class","content"),g(t,"class","alert alert-success"),g(C,"class","section-title"),g(A,"class","table-compact table-border m-b-base"),g(R,"class","section-title"),g(P,"class","tabs-header compact combined left"),g(D,"class","tabs-content"),g(y,"class","tabs")},m(l,o){b(l,t,o),h(t,e),h(t,a),h(t,d),h(d,c),h(c,r),h(c,n),h(n,m),h(c,q),b(l,M,o),b(l,C,o),b(l,B,o),b(l,A,o),b(l,H,o),b(l,R,o),b(l,W,o),b(l,y,o),h(y,P);for(let f=0;f<w.length;f+=1)w[f]&&w[f].m(P,null);h(y,E),h(y,D);for(let f=0;f<k.length;f+=1)k[f]&&k[f].m(D,null);I=!0},p(l,[o]){(!I||o&1)&&u!==(u=l[0].name+"")&&z(m,u),o&6&&(i=U(l[2]),w=G(w,o,v,1,l,i,$,P,ge,pe,null,he)),o&6&&(O=U(l[2]),Z(),k=G(k,o,j,1,l,O,J,D,ke,fe,null,ue),x())},i(l){if(!I){for(let o=0;o<O.length;o+=1)L(k[o]);I=!0}},o(l){for(let o=0;o<k.length;o+=1)N(k[o]);I=!1},d(l){l&&(_(t),_(M),_(C),_(B),_(A),_(H),_(R),_(W),_(y));for(let o=0;o<w.length;o+=1)w[o].d();for(let o=0;o<k.length;o+=1)k[o].d()}}}function qe(s,t,e){let{collection:a}=t,d=200,c=[];const r=n=>e(1,d=n.code);return s.$$set=n=>{"collection"in n&&e(0,a=n.collection)},e(2,c=[{code:200,body:JSON.stringify({otpId:oe.randomString(15)},null,2)},{code:400,body:`
{
"code": 400,
"message": "An error occurred while validating the submitted data.",
@@ -26,7 +26,7 @@ import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as
"message": "You've send too many OTP requests, please try again later.",
"data": {}
}
`}]),[a,d,c,r]}class Ae extends ee{constructor(t){super(),te(this,t,qe,ye,le,{collection:0})}}function me(s,t,e){const a=s.slice();return a[5]=t[e],a[7]=e,a}function be(s,t,e){const a=s.slice();return a[5]=t[e],a[7]=e,a}function _e(s){let t,e,a,d,c;function r(){return s[4](s[7])}return{c(){t=p("button"),e=p("div"),e.textContent=`${s[5].title}`,a=S(),g(e,"class","txt"),g(t,"class","tab-item"),Y(t,"active",s[1]==s[7])},m(n,u){b(n,t,u),h(t,e),h(t,a),d||(c=ae(t,"click",r),d=!0)},p(n,u){s=n,u&2&&Y(t,"active",s[1]==s[7])},d(n){n&&_(t),d=!1,c()}}}function ve(s){let t,e,a,d;var c=s[5].component;function r(n,u){return{props:{collection:n[0]}}}return c&&(e=ne(c,r(s))),{c(){t=p("div"),e&&K(e.$$.fragment),a=S(),g(t,"class","tab-item"),Y(t,"active",s[1]==s[7])},m(n,u){b(n,t,u),e&&Q(e,t,null),h(t,a),d=!0},p(n,u){if(c!==(c=n[5].component)){if(e){Z();const m=e;J(m.$$.fragment,1,0,()=>{X(m,1)}),x()}c?(e=ne(c,r(n)),K(e.$$.fragment),L(e.$$.fragment,1),Q(e,t,a)):e=null}else if(c){const m={};u&1&&(m.collection=n[0]),e.$set(m)}(!d||u&2)&&Y(t,"active",n[1]==n[7])},i(n){d||(e&&L(e.$$.fragment,n),d=!0)},o(n){e&&J(e.$$.fragment,n),d=!1},d(n){n&&_(t),e&&X(e)}}}function Re(s){var D,k,N,M;let t,e,a=s[0].name+"",d,c,r,n,u,m,q,W,C,B,A,H,R,I;m=new $e({props:{js:`
`}]),[a,d,c,r]}class Ae extends ee{constructor(t){super(),te(this,t,qe,ye,le,{collection:0})}}function me(s,t,e){const a=s.slice();return a[5]=t[e],a[7]=e,a}function be(s,t,e){const a=s.slice();return a[5]=t[e],a[7]=e,a}function _e(s){let t,e,a,d,c;function r(){return s[4](s[7])}return{c(){t=p("button"),e=p("div"),e.textContent=`${s[5].title}`,a=S(),g(e,"class","txt"),g(t,"class","tab-item"),Y(t,"active",s[1]==s[7])},m(n,u){b(n,t,u),h(t,e),h(t,a),d||(c=ae(t,"click",r),d=!0)},p(n,u){s=n,u&2&&Y(t,"active",s[1]==s[7])},d(n){n&&_(t),d=!1,c()}}}function ve(s){let t,e,a,d;var c=s[5].component;function r(n,u){return{props:{collection:n[0]}}}return c&&(e=ne(c,r(s))),{c(){t=p("div"),e&&K(e.$$.fragment),a=S(),g(t,"class","tab-item"),Y(t,"active",s[1]==s[7])},m(n,u){b(n,t,u),e&&Q(e,t,null),h(t,a),d=!0},p(n,u){if(c!==(c=n[5].component)){if(e){Z();const m=e;N(m.$$.fragment,1,0,()=>{X(m,1)}),x()}c?(e=ne(c,r(n)),K(e.$$.fragment),L(e.$$.fragment,1),Q(e,t,a)):e=null}else if(c){const m={};u&1&&(m.collection=n[0]),e.$set(m)}(!d||u&2)&&Y(t,"active",n[1]==n[7])},i(n){d||(e&&L(e.$$.fragment,n),d=!0)},o(n){e&&N(e.$$.fragment,n),d=!1},d(n){n&&_(t),e&&X(e)}}}function Re(s){var D,k,J,I;let t,e,a=s[0].name+"",d,c,r,n,u,m,q,M,C,B,A,H,R,W;m=new $e({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${s[2]}');
@@ -59,12 +59,12 @@ import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as
...
// send OTP email to the provided auth record
final req = await pb.collection('${(N=s[0])==null?void 0:N.name}').requestOTP('test@example.com');
final req = await pb.collection('${(J=s[0])==null?void 0:J.name}').requestOTP('test@example.com');
// ... show a screen/popup to enter the password from the email ...
// authenticate with the requested OTP id and the email password
final authData = await pb.collection('${(M=s[0])==null?void 0:M.name}').authWithOTP(
final authData = await pb.collection('${(I=s[0])==null?void 0:I.name}').authWithOTP(
req.otpId,
"YOUR_OTP",
);
@@ -76,7 +76,8 @@ import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as
// "logout"
pb.authStore.clear();
`}});let y=U(s[3]),P=[];for(let i=0;i<y.length;i+=1)P[i]=_e(be(s,y,i));let w=U(s[3]),$=[];for(let i=0;i<w.length;i+=1)$[i]=ve(me(s,w,i));const E=i=>J($[i],1,1,()=>{$[i]=null});return{c(){t=p("h3"),e=V("Auth with OTP ("),d=V(a),c=V(")"),r=S(),n=p("div"),n.innerHTML="<p>Authenticate with an one-time password (OTP).</p>",u=S(),K(m.$$.fragment),q=S(),W=p("h6"),W.textContent="API details",C=S(),B=p("div"),A=p("div");for(let i=0;i<P.length;i+=1)P[i].c();H=S(),R=p("div");for(let i=0;i<$.length;i+=1)$[i].c();g(t,"class","m-b-sm"),g(n,"class","content txt-lg m-b-sm"),g(W,"class","m-b-xs"),g(A,"class","tabs-header compact"),g(R,"class","tabs-content"),g(B,"class","tabs")},m(i,v){b(i,t,v),h(t,e),h(t,d),h(t,c),b(i,r,v),b(i,n,v),b(i,u,v),Q(m,i,v),b(i,q,v),b(i,W,v),b(i,C,v),b(i,B,v),h(B,A);for(let O=0;O<P.length;O+=1)P[O]&&P[O].m(A,null);h(B,H),h(B,R);for(let O=0;O<$.length;O+=1)$[O]&&$[O].m(R,null);I=!0},p(i,[v]){var j,l,o,f;(!I||v&1)&&a!==(a=i[0].name+"")&&z(d,a);const O={};if(v&5&&(O.js=`
`}});let y=U(s[3]),P=[];for(let i=0;i<y.length;i+=1)P[i]=_e(be(s,y,i));let w=U(s[3]),$=[];for(let i=0;i<w.length;i+=1)$[i]=ve(me(s,w,i));const E=i=>N($[i],1,1,()=>{$[i]=null});return{c(){t=p("h3"),e=V("Auth with OTP ("),d=V(a),c=V(")"),r=S(),n=p("div"),n.innerHTML=`<p>Authenticate with an one-time password (OTP).</p> <p>Note that when requesting an OTP we return an <code>otpId</code> even if a user with the provided email
doesn&#39;t exist as a very basic enumeration protection.</p>`,u=S(),K(m.$$.fragment),q=S(),M=p("h6"),M.textContent="API details",C=S(),B=p("div"),A=p("div");for(let i=0;i<P.length;i+=1)P[i].c();H=S(),R=p("div");for(let i=0;i<$.length;i+=1)$[i].c();g(t,"class","m-b-sm"),g(n,"class","content txt-lg m-b-sm"),g(M,"class","m-b-xs"),g(A,"class","tabs-header compact"),g(R,"class","tabs-content"),g(B,"class","tabs")},m(i,v){b(i,t,v),h(t,e),h(t,d),h(t,c),b(i,r,v),b(i,n,v),b(i,u,v),Q(m,i,v),b(i,q,v),b(i,M,v),b(i,C,v),b(i,B,v),h(B,A);for(let O=0;O<P.length;O+=1)P[O]&&P[O].m(A,null);h(B,H),h(B,R);for(let O=0;O<$.length;O+=1)$[O]&&$[O].m(R,null);W=!0},p(i,[v]){var j,l,o,f;(!W||v&1)&&a!==(a=i[0].name+"")&&z(d,a);const O={};if(v&5&&(O.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[2]}');
@@ -126,4 +127,4 @@ import{S as ee,i as te,s as le,T as U,e as p,b as S,w as V,f as g,g as b,h,x as
// "logout"
pb.authStore.clear();
`),m.$set(O),v&10){y=U(i[3]);let T;for(T=0;T<y.length;T+=1){const F=be(i,y,T);P[T]?P[T].p(F,v):(P[T]=_e(F),P[T].c(),P[T].m(A,null))}for(;T<P.length;T+=1)P[T].d(1);P.length=y.length}if(v&11){w=U(i[3]);let T;for(T=0;T<w.length;T+=1){const F=me(i,w,T);$[T]?($[T].p(F,v),L($[T],1)):($[T]=ve(F),$[T].c(),L($[T],1),$[T].m(R,null))}for(Z(),T=w.length;T<$.length;T+=1)E(T);x()}},i(i){if(!I){L(m.$$.fragment,i);for(let v=0;v<w.length;v+=1)L($[v]);I=!0}},o(i){J(m.$$.fragment,i),$=$.filter(Boolean);for(let v=0;v<$.length;v+=1)J($[v]);I=!1},d(i){i&&(_(t),_(r),_(n),_(u),_(q),_(W),_(C),_(B)),X(m,i),se(P,i),se($,i)}}}function Ce(s,t,e){let a,{collection:d}=t;const c=[{title:"OTP Request",component:Ae},{title:"OTP Auth",component:Se}];let r=0;const n=u=>e(1,r=u);return s.$$set=u=>{"collection"in u&&e(0,d=u.collection)},e(2,a=oe.getApiExampleUrl(Te.baseURL)),[d,r,a,c,n]}class De extends ee{constructor(t){super(),te(this,t,Ce,Re,le,{collection:0})}}export{De as default};
`),m.$set(O),v&10){y=U(i[3]);let T;for(T=0;T<y.length;T+=1){const F=be(i,y,T);P[T]?P[T].p(F,v):(P[T]=_e(F),P[T].c(),P[T].m(A,null))}for(;T<P.length;T+=1)P[T].d(1);P.length=y.length}if(v&11){w=U(i[3]);let T;for(T=0;T<w.length;T+=1){const F=me(i,w,T);$[T]?($[T].p(F,v),L($[T],1)):($[T]=ve(F),$[T].c(),L($[T],1),$[T].m(R,null))}for(Z(),T=w.length;T<$.length;T+=1)E(T);x()}},i(i){if(!W){L(m.$$.fragment,i);for(let v=0;v<w.length;v+=1)L($[v]);W=!0}},o(i){N(m.$$.fragment,i),$=$.filter(Boolean);for(let v=0;v<$.length;v+=1)N($[v]);W=!1},d(i){i&&(_(t),_(r),_(n),_(u),_(q),_(M),_(C),_(B)),X(m,i),se(P,i),se($,i)}}}function Ce(s,t,e){let a,{collection:d}=t;const c=[{title:"OTP Request",component:Ae},{title:"OTP Auth",component:Se}];let r=0;const n=u=>e(1,r=u);return s.$$set=u=>{"collection"in u&&e(0,d=u.collection)},e(2,a=oe.getApiExampleUrl(Te.baseURL)),[d,r,a,c,n]}class De extends ee{constructor(t){super(),te(this,t,Ce,Re,le,{collection:0})}}export{De as default};
@@ -1,4 +1,4 @@
import{S as kt,i as gt,s as vt,Q as St,T as L,R as _t,e as s,w as f,b as u,c as ae,f as k,g as c,h as t,m as oe,x as G,U as ct,V as wt,k as yt,W as $t,n as Pt,t as X,a as z,o as d,d as se,X as Rt,C as dt,p as Ct,r as ne,u as Tt}from"./index-C6KFFkct.js";import{F as Ot}from"./FieldsQueryParam-9HzYyPBO.js";function pt(i,o,a){const n=i.slice();return n[7]=o[a],n}function ut(i,o,a){const n=i.slice();return n[7]=o[a],n}function ht(i,o,a){const n=i.slice();return n[12]=o[a],n[14]=a,n}function At(i){let o;return{c(){o=f("or")},m(a,n){c(a,o,n)},d(a){a&&d(o)}}}function bt(i){let o,a,n=i[12]+"",m,b=i[14]>0&&At();return{c(){b&&b.c(),o=u(),a=s("strong"),m=f(n)},m(r,h){b&&b.m(r,h),c(r,o,h),c(r,a,h),t(a,m)},p(r,h){h&2&&n!==(n=r[12]+"")&&G(m,n)},d(r){r&&(d(o),d(a)),b&&b.d(r)}}}function ft(i,o){let a,n=o[7].code+"",m,b,r,h;function g(){return o[6](o[7])}return{key:i,first:null,c(){a=s("button"),m=f(n),b=u(),k(a,"class","tab-item"),ne(a,"active",o[2]===o[7].code),this.first=a},m($,_){c($,a,_),t(a,m),t(a,b),r||(h=Tt(a,"click",g),r=!0)},p($,_){o=$,_&8&&n!==(n=o[7].code+"")&&G(m,n),_&12&&ne(a,"active",o[2]===o[7].code)},d($){$&&d(a),r=!1,h()}}}function mt(i,o){let a,n,m,b;return n=new _t({props:{content:o[7].body}}),{key:i,first:null,c(){a=s("div"),ae(n.$$.fragment),m=u(),k(a,"class","tab-item"),ne(a,"active",o[2]===o[7].code),this.first=a},m(r,h){c(r,a,h),oe(n,a,null),t(a,m),b=!0},p(r,h){o=r;const g={};h&8&&(g.content=o[7].body),n.$set(g),(!b||h&12)&&ne(a,"active",o[2]===o[7].code)},i(r){b||(X(n.$$.fragment,r),b=!0)},o(r){z(n.$$.fragment,r),b=!1},d(r){r&&d(a),se(n)}}}function Dt(i){var ot,st;let o,a,n=i[0].name+"",m,b,r,h,g,$,_,Z=i[1].join("/")+"",ie,De,re,We,ce,R,de,q,pe,C,x,Ue,ee,H,Fe,ue,te=i[0].name+"",he,Me,be,j,fe,T,me,Be,V,O,_e,Le,ke,qe,Y,ge,He,ve,Se,E,we,A,ye,je,N,D,$e,Ve,Pe,Ye,v,Ee,F,Ne,Qe,Ie,Re,Je,Ce,Ke,Xe,ze,Te,Ge,Ze,M,Oe,Q,Ae,W,I,P=[],xe=new Map,et,J,w=[],tt=new Map,U;R=new St({props:{js:`
import{S as kt,i as gt,s as vt,Q as St,T as L,R as _t,e as s,w as f,b as u,c as ae,f as k,g as c,h as t,m as oe,x as G,U as ct,V as wt,k as yt,W as $t,n as Pt,t as X,a as z,o as d,d as se,X as Rt,C as dt,p as Ct,r as ne,u as Tt}from"./index-Co7n6kj7.js";import{F as Ot}from"./FieldsQueryParam-DH-ZC4qv.js";function pt(i,o,a){const n=i.slice();return n[7]=o[a],n}function ut(i,o,a){const n=i.slice();return n[7]=o[a],n}function ht(i,o,a){const n=i.slice();return n[12]=o[a],n[14]=a,n}function At(i){let o;return{c(){o=f("or")},m(a,n){c(a,o,n)},d(a){a&&d(o)}}}function bt(i){let o,a,n=i[12]+"",m,b=i[14]>0&&At();return{c(){b&&b.c(),o=u(),a=s("strong"),m=f(n)},m(r,h){b&&b.m(r,h),c(r,o,h),c(r,a,h),t(a,m)},p(r,h){h&2&&n!==(n=r[12]+"")&&G(m,n)},d(r){r&&(d(o),d(a)),b&&b.d(r)}}}function ft(i,o){let a,n=o[7].code+"",m,b,r,h;function g(){return o[6](o[7])}return{key:i,first:null,c(){a=s("button"),m=f(n),b=u(),k(a,"class","tab-item"),ne(a,"active",o[2]===o[7].code),this.first=a},m($,_){c($,a,_),t(a,m),t(a,b),r||(h=Tt(a,"click",g),r=!0)},p($,_){o=$,_&8&&n!==(n=o[7].code+"")&&G(m,n),_&12&&ne(a,"active",o[2]===o[7].code)},d($){$&&d(a),r=!1,h()}}}function mt(i,o){let a,n,m,b;return n=new _t({props:{content:o[7].body}}),{key:i,first:null,c(){a=s("div"),ae(n.$$.fragment),m=u(),k(a,"class","tab-item"),ne(a,"active",o[2]===o[7].code),this.first=a},m(r,h){c(r,a,h),oe(n,a,null),t(a,m),b=!0},p(r,h){o=r;const g={};h&8&&(g.content=o[7].body),n.$set(g),(!b||h&12)&&ne(a,"active",o[2]===o[7].code)},i(r){b||(X(n.$$.fragment,r),b=!0)},o(r){z(n.$$.fragment,r),b=!1},d(r){r&&d(a),se(n)}}}function Dt(i){var ot,st;let o,a,n=i[0].name+"",m,b,r,h,g,$,_,Z=i[1].join("/")+"",ie,De,re,We,ce,R,de,q,pe,C,x,Ue,ee,H,Fe,ue,te=i[0].name+"",he,Me,be,j,fe,T,me,Be,V,O,_e,Le,ke,qe,Y,ge,He,ve,Se,E,we,A,ye,je,N,D,$e,Ve,Pe,Ye,v,Ee,F,Ne,Qe,Ie,Re,Je,Ce,Ke,Xe,ze,Te,Ge,Ze,M,Oe,Q,Ae,W,I,P=[],xe=new Map,et,J,w=[],tt=new Map,U;R=new St({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i[5]}');
+157
View File
@@ -0,0 +1,157 @@
import{S as It,i as Lt,s as jt,Q as At,R as Ht,T as Y,e as o,w as _,b as i,c as Re,f as b,g as d,h as t,m as Te,j as Mt,l as Nt,x as Ut,U as Pt,V as zt,k as Jt,W as Wt,n as Kt,t as Z,a as x,o as u,d as Pe,C as Ft,p as Gt,r as ee,u as Qt}from"./index-Co7n6kj7.js";function Bt(a,s,n){const c=a.slice();return c[6]=s[n],c}function Ot(a,s,n){const c=a.slice();return c[6]=s[n],c}function Et(a,s){let n,c,y;function h(){return s[5](s[6])}return{key:a,first:null,c(){n=o("button"),n.textContent=`${s[6].code} `,b(n,"class","tab-item"),ee(n,"active",s[1]===s[6].code),this.first=n},m(r,f){d(r,n,f),c||(y=Qt(n,"click",h),c=!0)},p(r,f){s=r,f&10&&ee(n,"active",s[1]===s[6].code)},d(r){r&&u(n),c=!1,y()}}}function St(a,s){let n,c,y,h;return c=new Ht({props:{content:s[6].body}}),{key:a,first:null,c(){n=o("div"),Re(c.$$.fragment),y=i(),b(n,"class","tab-item"),ee(n,"active",s[1]===s[6].code),this.first=n},m(r,f){d(r,n,f),Te(c,n,null),t(n,y),h=!0},p(r,f){s=r,(!h||f&10)&&ee(n,"active",s[1]===s[6].code)},i(r){h||(Z(c.$$.fragment,r),h=!0)},o(r){x(c.$$.fragment,r),h=!1},d(r){r&&u(n),Pe(c)}}}function Vt(a){var pt,mt,bt,ft,ht,_t,yt,kt;let s,n,c=a[0].name+"",y,h,r,f,F,g,U,Fe,P,B,Be,O,Oe,Ee,te,le,w,oe,E,ae,S,se,H,ne,z,ie,q,ce,Se,re,I,J,He,k,W,Ie,de,Le,D,K,je,ue,Ae,G,Me,pe,Ne,v,Ue,me,ze,Je,We,Q,Ke,V,Ge,be,Qe,fe,Ve,he,Xe,p,_e,Ye,ye,Ze,ke,xe,$e,et,ge,tt,Ce,lt,ot,at,De,st,R,ve,L,we,T,j,C=[],nt=new Map,it,A,$=[],ct=new Map,M,qe,rt;w=new At({props:{js:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${a[2]}');
...
const batch = pb.createBatch();
batch.collection('${(pt=a[0])==null?void 0:pt.name}').create({ ... });
batch.collection('${(mt=a[0])==null?void 0:mt.name}').update("RECORD_ID", { ... });
batch.collection('${(bt=a[0])==null?void 0:bt.name}').delete("RECORD_ID");
batch.collection('${(ft=a[0])==null?void 0:ft.name}').upsert({ ... });
const result = await batch.send();
`,dart:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${a[2]}');
...
final batch = pb.createBatch();
batch.collection('${(ht=a[0])==null?void 0:ht.name}').create(body: { ... });
batch.collection('${(_t=a[0])==null?void 0:_t.name}').update('RECORD_ID', body: { ... });
batch.collection('${(yt=a[0])==null?void 0:yt.name}').delete('RECORD_ID');
batch.collection('${(kt=a[0])==null?void 0:kt.name}').upsert(body: { ... });
final result = await batch.send();
`}}),R=new Ht({props:{language:"javascript",content:`
const formData = new FormData();
formData.append("@jsonPayload", JSON.stringify({
requests: [
{
method: "POST",
url: "/api/collections/${a[0].name}/records?fields=id",
body: { someField: "test1" }
},
{
method: "PATCH",
url: "/api/collections/${a[0].name}/records/RECORD_ID",
body: { someField: "test2" }
}
]
}))
// file for the first request
formData.append("requests.0.someFileField", new File(...))
// file for the second request
formData.append("requests.1.someFileField", new File(...))
`}});let X=Y(a[3]);const dt=e=>e[6].code;for(let e=0;e<X.length;e+=1){let l=Ot(a,X,e),m=dt(l);nt.set(m,C[e]=Et(m,l))}let N=Y(a[3]);const ut=e=>e[6].code;for(let e=0;e<N.length;e+=1){let l=Bt(a,N,e),m=ut(l);ct.set(m,$[e]=St(m,l))}return{c(){s=o("h3"),n=_("Batch create/update/upsert/delete ("),y=_(c),h=_(")"),r=i(),f=o("div"),f.innerHTML="<p>Batch and transactional create/update/upsert/delete of multiple records in a single request.</p>",F=i(),g=o("div"),U=o("div"),U.innerHTML='<i class="ri-error-warning-line"></i>',Fe=i(),P=o("div"),B=o("p"),Be=_(`The batch Web API need to be explicitly enabled and configured from the
`),O=o("a"),O.textContent="Dashboard settings",Oe=_("."),Ee=i(),te=o("p"),te.textContent=`Because this endpoint process the requests in a single transaction, it could degrade the
performance of your application if not used with proper care and configuration (e.g. too large
allowed execution timeout, large body size limit, etc.).`,le=i(),Re(w.$$.fragment),oe=i(),E=o("h6"),E.textContent="API details",ae=i(),S=o("div"),S.innerHTML='<strong class="label label-primary">POST</strong> <div class="content">/api/batch</div>',se=i(),H=o("div"),H.textContent="Body Parameters",ne=i(),z=o("p"),z.innerHTML=`Body parameters could be sent as <em>application/json</em> or <em>multipart/form-data</em>.
<br/>
File upload is supported only via <em>multipart/form-data</em> (see below for more details).`,ie=i(),q=o("table"),ce=o("thead"),ce.innerHTML='<tr><th>Param</th> <th width="80%">Description</th></tr>',Se=i(),re=o("tbody"),I=o("tr"),J=o("td"),J.innerHTML='<div class="flex txt-nowrap"><span class="label label-success">Required</span> <span>requests</span></div>',He=i(),k=o("td"),W=o("span"),W.textContent="Array<Request>",Ie=_(` - List of the requests to process.
`),de=o("p"),de.textContent="The supported batch request actions are:",Le=i(),D=o("ul"),K=o("li"),je=_("record create - "),ue=o("code"),ue.textContent="POST /api/collections/{collection}/records",Ae=i(),G=o("li"),Me=_(`record update -
`),pe=o("code"),pe.textContent="PATCH /api/collections/{collection}/records/{id}",Ne=i(),v=o("li"),Ue=_("record upsert - "),me=o("code"),me.textContent="PUT /api/collections/{collection}/records",ze=i(),Je=o("br"),We=i(),Q=o("small"),Q.innerHTML='(the body must have <code class="txt-sm">id</code> field)',Ke=i(),V=o("li"),Ge=_(`record delete -
`),be=o("code"),be.textContent="DELETE /api/collections/{collection}/records/{id}",Qe=i(),fe=o("p"),fe.textContent="Each batch Request element have the following properties:",Ve=i(),he=o("ul"),he.innerHTML=`<li><code>url path</code> <em>(could include query parameters)</em></li> <li><code>method</code> <em>(GET, POST, PUT, PATCH, DELETE)</em></li> <li><code>headers</code> <br/> <em>(if the <code>POST /api/batch</code> call has <code>Authorization</code> header it
is forwarded to all batch requests automatically)</em></li> <li><code>body</code></li>`,Xe=i(),p=o("p"),_e=o("strong"),_e.textContent="NB!",Ye=_(` When the batch request is send as
`),ye=o("code"),ye.textContent="multipart/form-data",Ze=_(`, the regular batch action fields are expected to be
submitted as serailized json under the `),ke=o("code"),ke.textContent="@jsonPayload",xe=_(` field and file keys need
to follow the pattern `),$e=o("code"),$e.textContent="requests.N.fileField",et=_(` or
`),ge=o("code"),ge.textContent="requests[N].fileField",tt=i(),Ce=o("em"),Ce.textContent=`(this is usually handled transparently by the SDKs when their specific object notation
is used)
`,lt=_(`.
`),ot=o("br"),at=_(`
If you don't use the SDKs or prefer manually to construct the `),De=o("code"),De.textContent="FormData",st=_(`
body, then it could look something like:
`),Re(R.$$.fragment),ve=i(),L=o("div"),L.textContent="Responses",we=i(),T=o("div"),j=o("div");for(let e=0;e<C.length;e+=1)C[e].c();it=i(),A=o("div");for(let e=0;e<$.length;e+=1)$[e].c();b(s,"class","m-b-sm"),b(f,"class","content txt-lg m-b-sm"),b(U,"class","icon"),b(O,"href","/settings"),b(P,"class","content"),b(g,"class","alert alert-warning"),b(E,"class","m-b-xs"),b(S,"class","api-route alert alert-success"),b(H,"class","section-title"),b(J,"valign","top"),b(W,"class","label"),b(Q,"class","txt-hint"),b(q,"class","table-compact table-border m-t-xs m-b-base"),b(L,"class","section-title"),b(j,"class","tabs-header compact combined left"),b(A,"class","tabs-content"),b(T,"class","tabs")},m(e,l){d(e,s,l),t(s,n),t(s,y),t(s,h),d(e,r,l),d(e,f,l),d(e,F,l),d(e,g,l),t(g,U),t(g,Fe),t(g,P),t(P,B),t(B,Be),t(B,O),t(B,Oe),t(P,Ee),t(P,te),d(e,le,l),Te(w,e,l),d(e,oe,l),d(e,E,l),d(e,ae,l),d(e,S,l),d(e,se,l),d(e,H,l),d(e,ne,l),d(e,z,l),d(e,ie,l),d(e,q,l),t(q,ce),t(q,Se),t(q,re),t(re,I),t(I,J),t(I,He),t(I,k),t(k,W),t(k,Ie),t(k,de),t(k,Le),t(k,D),t(D,K),t(K,je),t(K,ue),t(D,Ae),t(D,G),t(G,Me),t(G,pe),t(D,Ne),t(D,v),t(v,Ue),t(v,me),t(v,ze),t(v,Je),t(v,We),t(v,Q),t(D,Ke),t(D,V),t(V,Ge),t(V,be),t(k,Qe),t(k,fe),t(k,Ve),t(k,he),t(k,Xe),t(k,p),t(p,_e),t(p,Ye),t(p,ye),t(p,Ze),t(p,ke),t(p,xe),t(p,$e),t(p,et),t(p,ge),t(p,tt),t(p,Ce),t(p,lt),t(p,ot),t(p,at),t(p,De),t(p,st),Te(R,p,null),d(e,ve,l),d(e,L,l),d(e,we,l),d(e,T,l),t(T,j);for(let m=0;m<C.length;m+=1)C[m]&&C[m].m(j,null);t(T,it),t(T,A);for(let m=0;m<$.length;m+=1)$[m]&&$[m].m(A,null);M=!0,qe||(rt=Mt(Nt.call(null,O)),qe=!0)},p(e,[l]){var gt,Ct,Dt,vt,wt,qt,Rt,Tt;(!M||l&1)&&c!==(c=e[0].name+"")&&Ut(y,c);const m={};l&5&&(m.js=`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${e[2]}');
...
const batch = pb.createBatch();
batch.collection('${(gt=e[0])==null?void 0:gt.name}').create({ ... });
batch.collection('${(Ct=e[0])==null?void 0:Ct.name}').update("RECORD_ID", { ... });
batch.collection('${(Dt=e[0])==null?void 0:Dt.name}').delete("RECORD_ID");
batch.collection('${(vt=e[0])==null?void 0:vt.name}').upsert({ ... });
const result = await batch.send();
`),l&5&&(m.dart=`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${e[2]}');
...
final batch = pb.createBatch();
batch.collection('${(wt=e[0])==null?void 0:wt.name}').create(body: { ... });
batch.collection('${(qt=e[0])==null?void 0:qt.name}').update('RECORD_ID', body: { ... });
batch.collection('${(Rt=e[0])==null?void 0:Rt.name}').delete('RECORD_ID');
batch.collection('${(Tt=e[0])==null?void 0:Tt.name}').upsert(body: { ... });
final result = await batch.send();
`),w.$set(m);const $t={};l&1&&($t.content=`
const formData = new FormData();
formData.append("@jsonPayload", JSON.stringify({
requests: [
{
method: "POST",
url: "/api/collections/${e[0].name}/records?fields=id",
body: { someField: "test1" }
},
{
method: "PATCH",
url: "/api/collections/${e[0].name}/records/RECORD_ID",
body: { someField: "test2" }
}
]
}))
// file for the first request
formData.append("requests.0.someFileField", new File(...))
// file for the second request
formData.append("requests.1.someFileField", new File(...))
`),R.$set($t),l&10&&(X=Y(e[3]),C=Pt(C,l,dt,1,e,X,nt,j,zt,Et,null,Ot)),l&10&&(N=Y(e[3]),Jt(),$=Pt($,l,ut,1,e,N,ct,A,Wt,St,null,Bt),Kt())},i(e){if(!M){Z(w.$$.fragment,e),Z(R.$$.fragment,e);for(let l=0;l<N.length;l+=1)Z($[l]);M=!0}},o(e){x(w.$$.fragment,e),x(R.$$.fragment,e);for(let l=0;l<$.length;l+=1)x($[l]);M=!1},d(e){e&&(u(s),u(r),u(f),u(F),u(g),u(le),u(oe),u(E),u(ae),u(S),u(se),u(H),u(ne),u(z),u(ie),u(q),u(ve),u(L),u(we),u(T)),Pe(w,e),Pe(R);for(let l=0;l<C.length;l+=1)C[l].d();for(let l=0;l<$.length;l+=1)$[l].d();qe=!1,rt()}}}function Xt(a,s,n){let c,y,{collection:h}=s,r=200,f=[];const F=g=>n(1,r=g.code);return a.$$set=g=>{"collection"in g&&n(0,h=g.collection)},a.$$.update=()=>{a.$$.dirty&1&&n(4,y=Ft.dummyCollectionRecord(h)),a.$$.dirty&17&&h!=null&&h.id&&(f.push({code:200,body:JSON.stringify([y,Object.assign({},y,{id:y+"2"})],null,2)}),f.push({code:400,body:`
{
"status": 400,
"message": "Batch transaction failed.",
"data": {
"requests": {
"1": {
"code": "batch_request_failed",
"message": "Batch request failed.",
"response": {
"status": 400,
"message": "Failed to create record.",
"data": {
"id": {
"code": "validation_min_text_constraint",
"message": "Must be at least 3 character(s).",
"params": { "min": 3 }
}
}
}
}
}
}
}
`}),f.push({code:403,body:`
{
"code": 403,
"message": "Batch requests are not allowed.",
"data": {}
}
`}))},n(2,c=Ft.getApiExampleUrl(Gt.baseURL)),[h,r,c,f,y,F]}class Zt extends It{constructor(s){super(),Lt(this,s,Xt,Vt,jt,{collection:0})}}export{Zt as default};
@@ -1,4 +1,4 @@
import{S as wt,i as Rt,s as Yt,e as Tt,f as Wt,Z as OO,g as _t,y as BO,o as qt,J as vt,N as Ut,O as zt,L as Vt,C as jt,P as Gt}from"./index-C6KFFkct.js";import{P as Ct,N as At,w as Et,D as Nt,x as qO,T as tO,I as vO,y as B,z as l,A as Mt,L as J,B as L,F as v,G as K,H as UO,J as F,v as G,K as _e,M as It,O as Dt,Q as qe,R as ve,U as Ue,E as q,V as ze,W as S,X as Bt,Y as Jt,b as C,e as Lt,f as Kt,g as Ft,i as Ht,j as Oa,k as ea,u as ta,l as aa,m as ra,r as ia,n as sa,o as oa,c as la,d as na,s as ca,h as Qa,a as ha,p as ua,q as JO,C as eO}from"./index-B5ReTu-C.js";var LO={};class sO{constructor(O,a,t,r,s,i,o,n,Q,u=0,c){this.p=O,this.stack=a,this.state=t,this.reducePos=r,this.pos=s,this.score=i,this.buffer=o,this.bufferBase=n,this.curContext=Q,this.lookAhead=u,this.parent=c}toString(){return`[${this.stack.filter((O,a)=>a%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,a,t=0){let r=O.parser.context;return new sO(O,[],a,t,t,0,[],0,r?new KO(r,r.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,a){this.stack.push(this.state,a,this.bufferBase+this.buffer.length),this.state=O}reduce(O){var a;let t=O>>19,r=O&65535,{parser:s}=this.p,i=this.reducePos<this.pos-25;i&&this.setLookAhead(this.pos);let o=s.dynamicPrecedence(r);if(o&&(this.score+=o),t==0){this.pushState(s.getGoto(this.state,r,!0),this.reducePos),r<s.minRepeatTerm&&this.storeNode(r,this.reducePos,this.reducePos,i?8:4,!0),this.reduceContext(r,this.reducePos);return}let n=this.stack.length-(t-1)*3-(O&262144?6:0),Q=n?this.stack[n-2]:this.p.ranges[0].from,u=this.reducePos-Q;u>=2e3&&!(!((a=this.p.parser.nodeSet.types[r])===null||a===void 0)&&a.isAnonymous)&&(Q==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=u):this.p.lastBigReductionSize<u&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=Q,this.p.lastBigReductionSize=u));let c=n?this.stack[n-1]:0,f=this.bufferBase+this.buffer.length-c;if(r<s.minRepeatTerm||O&131072){let h=s.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(r,Q,h,f+4,!0)}if(O&262144)this.state=this.stack[n];else{let h=this.stack[n-3];this.state=s.getGoto(h,r,!0)}for(;this.stack.length>n;)this.stack.pop();this.reduceContext(r,Q)}storeNode(O,a,t,r=4,s=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let i=this,o=this.buffer.length;if(o==0&&i.parent&&(o=i.bufferBase-i.parent.bufferBase,i=i.parent),o>0&&i.buffer[o-4]==0&&i.buffer[o-1]>-1){if(a==t)return;if(i.buffer[o-2]>=a){i.buffer[o-2]=t;return}}}if(!s||this.pos==t)this.buffer.push(O,a,t,r);else{let i=this.buffer.length;if(i>0&&this.buffer[i-4]!=0){let o=!1;for(let n=i;n>0&&this.buffer[n-2]>t;n-=4)if(this.buffer[n-1]>=0){o=!0;break}if(o)for(;i>0&&this.buffer[i-2]>t;)this.buffer[i]=this.buffer[i-4],this.buffer[i+1]=this.buffer[i-3],this.buffer[i+2]=this.buffer[i-2],this.buffer[i+3]=this.buffer[i-1],i-=4,r>4&&(r-=4)}this.buffer[i]=O,this.buffer[i+1]=a,this.buffer[i+2]=t,this.buffer[i+3]=r}}shift(O,a,t,r){if(O&131072)this.pushState(O&65535,this.pos);else if(O&262144)this.pos=r,this.shiftContext(a,t),a<=this.p.parser.maxNode&&this.buffer.push(a,t,r,4);else{let s=O,{parser:i}=this.p;(r>this.pos||a<=i.maxNode)&&(this.pos=r,i.stateFlag(s,1)||(this.reducePos=r)),this.pushState(s,t),this.shiftContext(a,t),a<=i.maxNode&&this.buffer.push(a,t,r,4)}}apply(O,a,t,r){O&65536?this.reduce(O):this.shift(O,a,t,r)}useNode(O,a){let t=this.p.reused.length-1;(t<0||this.p.reused[t]!=O)&&(this.p.reused.push(O),t++);let r=this.pos;this.reducePos=this.pos=r+O.length,this.pushState(a,r),this.buffer.push(t,r,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,a=O.buffer.length;for(;a>0&&O.buffer[a-2]>O.reducePos;)a-=4;let t=O.buffer.slice(a),r=O.bufferBase+a;for(;O&&r==O.bufferBase;)O=O.parent;return new sO(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,t,r,this.curContext,this.lookAhead,O)}recoverByDelete(O,a){let t=O<=this.p.parser.maxNode;t&&this.storeNode(O,this.pos,a,4),this.storeNode(0,this.pos,a,t?8:4),this.pos=this.reducePos=a,this.score-=190}canShift(O){for(let a=new pa(this);;){let t=this.p.parser.stateSlot(a.state,4)||this.p.parser.hasAction(a.state,O);if(t==0)return!1;if(!(t&65536))return!0;a.reduce(t)}}recoverByInsert(O){if(this.stack.length>=300)return[];let a=this.p.parser.nextStates(this.state);if(a.length>8||this.stack.length>=120){let r=[];for(let s=0,i;s<a.length;s+=2)(i=a[s+1])!=this.state&&this.p.parser.hasAction(i,O)&&r.push(a[s],i);if(this.stack.length<120)for(let s=0;r.length<8&&s<a.length;s+=2){let i=a[s+1];r.some((o,n)=>n&1&&o==i)||r.push(a[s],i)}a=r}let t=[];for(let r=0;r<a.length&&t.length<4;r+=2){let s=a[r+1];if(s==this.state)continue;let i=this.split();i.pushState(s,this.pos),i.storeNode(0,i.pos,i.pos,4,!0),i.shiftContext(a[r],this.pos),i.reducePos=this.pos,i.Line truncated
import{S as wt,i as Rt,s as Yt,e as Tt,f as Wt,Z as OO,g as _t,y as BO,o as qt,J as vt,N as Ut,O as zt,L as Vt,C as jt,P as Gt}from"./index-Co7n6kj7.js";import{P as Ct,N as At,w as Et,D as Nt,x as qO,T as tO,I as vO,y as B,z as l,A as Mt,L as J,B as L,F as v,G as K,H as UO,J as F,v as G,K as _e,M as It,O as Dt,Q as qe,R as ve,U as Ue,E as q,V as ze,W as S,X as Bt,Y as Jt,b as C,e as Lt,f as Kt,g as Ft,i as Ht,j as Oa,k as ea,u as ta,l as aa,m as ra,r as ia,n as sa,o as oa,c as la,d as na,s as ca,h as Qa,a as ha,p as ua,q as JO,C as eO}from"./index-B5ReTu-C.js";var LO={};class sO{constructor(O,a,t,r,s,i,o,n,Q,u=0,c){this.p=O,this.stack=a,this.state=t,this.reducePos=r,this.pos=s,this.score=i,this.buffer=o,this.bufferBase=n,this.curContext=Q,this.lookAhead=u,this.parent=c}toString(){return`[${this.stack.filter((O,a)=>a%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(O,a,t=0){let r=O.parser.context;return new sO(O,[],a,t,t,0,[],0,r?new KO(r,r.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(O,a){this.stack.push(this.state,a,this.bufferBase+this.buffer.length),this.state=O}reduce(O){var a;let t=O>>19,r=O&65535,{parser:s}=this.p,i=this.reducePos<this.pos-25;i&&this.setLookAhead(this.pos);let o=s.dynamicPrecedence(r);if(o&&(this.score+=o),t==0){this.pushState(s.getGoto(this.state,r,!0),this.reducePos),r<s.minRepeatTerm&&this.storeNode(r,this.reducePos,this.reducePos,i?8:4,!0),this.reduceContext(r,this.reducePos);return}let n=this.stack.length-(t-1)*3-(O&262144?6:0),Q=n?this.stack[n-2]:this.p.ranges[0].from,u=this.reducePos-Q;u>=2e3&&!(!((a=this.p.parser.nodeSet.types[r])===null||a===void 0)&&a.isAnonymous)&&(Q==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=u):this.p.lastBigReductionSize<u&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=Q,this.p.lastBigReductionSize=u));let c=n?this.stack[n-1]:0,f=this.bufferBase+this.buffer.length-c;if(r<s.minRepeatTerm||O&131072){let h=s.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(r,Q,h,f+4,!0)}if(O&262144)this.state=this.stack[n];else{let h=this.stack[n-3];this.state=s.getGoto(h,r,!0)}for(;this.stack.length>n;)this.stack.pop();this.reduceContext(r,Q)}storeNode(O,a,t,r=4,s=!1){if(O==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let i=this,o=this.buffer.length;if(o==0&&i.parent&&(o=i.bufferBase-i.parent.bufferBase,i=i.parent),o>0&&i.buffer[o-4]==0&&i.buffer[o-1]>-1){if(a==t)return;if(i.buffer[o-2]>=a){i.buffer[o-2]=t;return}}}if(!s||this.pos==t)this.buffer.push(O,a,t,r);else{let i=this.buffer.length;if(i>0&&this.buffer[i-4]!=0){let o=!1;for(let n=i;n>0&&this.buffer[n-2]>t;n-=4)if(this.buffer[n-1]>=0){o=!0;break}if(o)for(;i>0&&this.buffer[i-2]>t;)this.buffer[i]=this.buffer[i-4],this.buffer[i+1]=this.buffer[i-3],this.buffer[i+2]=this.buffer[i-2],this.buffer[i+3]=this.buffer[i-1],i-=4,r>4&&(r-=4)}this.buffer[i]=O,this.buffer[i+1]=a,this.buffer[i+2]=t,this.buffer[i+3]=r}}shift(O,a,t,r){if(O&131072)this.pushState(O&65535,this.pos);else if(O&262144)this.pos=r,this.shiftContext(a,t),a<=this.p.parser.maxNode&&this.buffer.push(a,t,r,4);else{let s=O,{parser:i}=this.p;(r>this.pos||a<=i.maxNode)&&(this.pos=r,i.stateFlag(s,1)||(this.reducePos=r)),this.pushState(s,t),this.shiftContext(a,t),a<=i.maxNode&&this.buffer.push(a,t,r,4)}}apply(O,a,t,r){O&65536?this.reduce(O):this.shift(O,a,t,r)}useNode(O,a){let t=this.p.reused.length-1;(t<0||this.p.reused[t]!=O)&&(this.p.reused.push(O),t++);let r=this.pos;this.reducePos=this.pos=r+O.length,this.pushState(a,r),this.buffer.push(t,r,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,O,this,this.p.stream.reset(this.pos-O.length)))}split(){let O=this,a=O.buffer.length;for(;a>0&&O.buffer[a-2]>O.reducePos;)a-=4;let t=O.buffer.slice(a),r=O.bufferBase+a;for(;O&&r==O.bufferBase;)O=O.parent;return new sO(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,t,r,this.curContext,this.lookAhead,O)}recoverByDelete(O,a){let t=O<=this.p.parser.maxNode;t&&this.storeNode(O,this.pos,a,4),this.storeNode(0,this.pos,a,t?8:4),this.pos=this.reducePos=a,this.score-=190}canShift(O){for(let a=new pa(this);;){let t=this.p.parser.stateSlot(a.state,4)||this.p.parser.hasAction(a.state,O);if(t==0)return!1;if(!(t&65536))return!0;a.reduce(t)}}recoverByInsert(O){if(this.stack.length>=300)return[];let a=this.p.parser.nextStates(this.state);if(a.length>8||this.stack.length>=120){let r=[];for(let s=0,i;s<a.length;s+=2)(i=a[s+1])!=this.state&&this.p.parser.hasAction(i,O)&&r.push(a[s],i);if(this.stack.length<120)for(let s=0;r.length<8&&s<a.length;s+=2){let i=a[s+1];r.some((o,n)=>n&1&&o==i)||r.push(a[s],i)}a=r}let t=[];for(let r=0;r<a.length&&t.length<4;r+=2){let s=a[r+1];if(s==this.state)continue;let i=this.split();i.pushState(s,this.pos),i.storeNode(0,i.pos,i.pos,4,!0),i.shiftContext(a[r],this.pos),i.reducePos=this.pos,i.Line truncated
\${}
} catch (\${error}) {
\${}
@@ -1,4 +1,4 @@
import{S as $t,i as qt,s as Tt,Q as St,C as ee,T as ue,R as Ct,e as s,w as _,b as p,c as $e,f as w,g as r,h as i,m as qe,x as oe,U as Ve,V as pt,k as Ot,W as Mt,n as Pt,t as ye,a as ve,o as d,d as Te,p as Ft,r as Se,u as Lt,y as we,E as Ht}from"./index-C6KFFkct.js";import{F as Rt}from"./FieldsQueryParam-9HzYyPBO.js";function mt(a,e,t){const l=a.slice();return l[10]=e[t],l}function bt(a,e,t){const l=a.slice();return l[10]=e[t],l}function _t(a,e,t){const l=a.slice();return l[15]=e[t],l}function kt(a){let e;return{c(){e=s("p"),e.innerHTML="Requires superuser <code>Authorization:TOKEN</code> header",w(e,"class","txt-hint txt-sm txt-right")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function ht(a){let e,t,l,u,c,f,b,m,$,h,g,B,S,O,R,M,U,J,T,W,P,q,k,F,te,K,I,re,Y,x,G;function fe(y,C){var V,z,H;return C&1&&(f=null),f==null&&(f=!!((H=(z=(V=y[0])==null?void 0:V.fields)==null?void 0:z.find(xt))!=null&&H.required)),f?Bt:At}let le=fe(a,-1),E=le(a);function X(y,C){var V,z,H;return C&1&&(U=null),U==null&&(U=!!((H=(z=(V=y[0])==null?void 0:V.fields)==null?void 0:z.find(Yt))!=null&&H.required)),U?Vt:jt}let Z=X(a,-1),L=Z(a);return{c(){e=s("tr"),e.innerHTML='<td colspan="3" class="txt-hint txt-bold">Auth specific fields</td>',t=p(),l=s("tr"),u=s("td"),c=s("div"),E.c(),b=p(),m=s("span"),m.textContent="email",$=p(),h=s("td"),h.innerHTML='<span class="label">String</span>',g=p(),B=s("td"),B.textContent="Auth record email address.",S=p(),O=s("tr"),R=s("td"),M=s("div"),L.c(),J=p(),T=s("span"),T.textContent="emailVisibility",W=p(),P=s("td"),P.innerHTML='<span class="label">Boolean</span>',q=p(),k=s("td"),k.textContent="Whether to show/hide the auth record email when fetching the record data.",F=p(),te=s("tr"),te.innerHTML='<td><div class="inline-flex"><span class="label label-success">Required</span> <span>password</span></div></td> <td><span class="label">String</span></td> <td>Auth record password.</td>',K=p(),I=s("tr"),I.innerHTML='<td><div class="inline-flex"><span class="label label-success">Required</span> <span>passwordConfirm</span></div></td> <td><span class="label">String</span></td> <td>Auth record password confirmation.</td>',re=p(),Y=s("tr"),Y.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span> <span>verified</span></div></td> <td><span class="label">Boolean</span></td> <td>Indicates whether the auth record is verified or not.
import{S as $t,i as qt,s as Tt,Q as St,C as ee,T as ue,R as Ct,e as s,w as _,b as p,c as $e,f as w,g as r,h as i,m as qe,x as oe,U as Ve,V as pt,k as Ot,W as Mt,n as Pt,t as ye,a as ve,o as d,d as Te,p as Ft,r as Se,u as Lt,y as we,E as Ht}from"./index-Co7n6kj7.js";import{F as Rt}from"./FieldsQueryParam-DH-ZC4qv.js";function mt(a,e,t){const l=a.slice();return l[10]=e[t],l}function bt(a,e,t){const l=a.slice();return l[10]=e[t],l}function _t(a,e,t){const l=a.slice();return l[15]=e[t],l}function kt(a){let e;return{c(){e=s("p"),e.innerHTML="Requires superuser <code>Authorization:TOKEN</code> header",w(e,"class","txt-hint txt-sm txt-right")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function ht(a){let e,t,l,u,c,f,b,m,$,h,g,B,S,O,R,M,U,J,T,W,P,q,k,F,te,K,I,re,Y,x,G;function fe(y,C){var V,z,H;return C&1&&(f=null),f==null&&(f=!!((H=(z=(V=y[0])==null?void 0:V.fields)==null?void 0:z.find(xt))!=null&&H.required)),f?Bt:At}let le=fe(a,-1),E=le(a);function X(y,C){var V,z,H;return C&1&&(U=null),U==null&&(U=!!((H=(z=(V=y[0])==null?void 0:V.fields)==null?void 0:z.find(Yt))!=null&&H.required)),U?Vt:jt}let Z=X(a,-1),L=Z(a);return{c(){e=s("tr"),e.innerHTML='<td colspan="3" class="txt-hint txt-bold">Auth specific fields</td>',t=p(),l=s("tr"),u=s("td"),c=s("div"),E.c(),b=p(),m=s("span"),m.textContent="email",$=p(),h=s("td"),h.innerHTML='<span class="label">String</span>',g=p(),B=s("td"),B.textContent="Auth record email address.",S=p(),O=s("tr"),R=s("td"),M=s("div"),L.c(),J=p(),T=s("span"),T.textContent="emailVisibility",W=p(),P=s("td"),P.innerHTML='<span class="label">Boolean</span>',q=p(),k=s("td"),k.textContent="Whether to show/hide the auth record email when fetching the record data.",F=p(),te=s("tr"),te.innerHTML='<td><div class="inline-flex"><span class="label label-success">Required</span> <span>password</span></div></td> <td><span class="label">String</span></td> <td>Auth record password.</td>',K=p(),I=s("tr"),I.innerHTML='<td><div class="inline-flex"><span class="label label-success">Required</span> <span>passwordConfirm</span></div></td> <td><span class="label">String</span></td> <td>Auth record password confirmation.</td>',re=p(),Y=s("tr"),Y.innerHTML=`<td><div class="inline-flex"><span class="label label-warning">Optional</span> <span>verified</span></div></td> <td><span class="label">Boolean</span></td> <td>Indicates whether the auth record is verified or not.
<br/>
This field can be set only by superusers or auth records with &quot;Manage&quot; access.</td>`,x=p(),G=s("tr"),G.innerHTML='<td colspan="3" class="txt-hint txt-bold">Other fields</td>',w(c,"class","inline-flex"),w(M,"class","inline-flex")},m(y,C){r(y,e,C),r(y,t,C),r(y,l,C),i(l,u),i(u,c),E.m(c,null),i(c,b),i(c,m),i(l,$),i(l,h),i(l,g),i(l,B),r(y,S,C),r(y,O,C),i(O,R),i(R,M),L.m(M,null),i(M,J),i(M,T),i(O,W),i(O,P),i(O,q),i(O,k),r(y,F,C),r(y,te,C),r(y,K,C),r(y,I,C),r(y,re,C),r(y,Y,C),r(y,x,C),r(y,G,C)},p(y,C){le!==(le=fe(y,C))&&(E.d(1),E=le(y),E&&(E.c(),E.m(c,b))),Z!==(Z=X(y,C))&&(L.d(1),L=Z(y),L&&(L.c(),L.m(M,J)))},d(y){y&&(d(e),d(t),d(l),d(S),d(O),d(F),d(te),d(K),d(I),d(re),d(Y),d(x),d(G)),E.d(),L.d()}}}function At(a){let e;return{c(){e=s("span"),e.textContent="Optional",w(e,"class","label label-warning")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function Bt(a){let e;return{c(){e=s("span"),e.textContent="Required",w(e,"class","label label-success")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function jt(a){let e;return{c(){e=s("span"),e.textContent="Optional",w(e,"class","label label-warning")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function Vt(a){let e;return{c(){e=s("span"),e.textContent="Required",w(e,"class","label label-success")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function Nt(a){let e;return{c(){e=s("span"),e.textContent="Required",w(e,"class","label label-success")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function Dt(a){let e;return{c(){e=s("span"),e.textContent="Optional",w(e,"class","label label-warning")},m(t,l){r(t,e,l)},d(t){t&&d(e)}}}function Jt(a){let e,t=a[15].maxSelect===1?"id":"ids",l,u;return{c(){e=_("Relation record "),l=_(t),u=_(".")},m(c,f){r(c,e,f),r(c,l,f),r(c,u,f)},p(c,f){f&64&&t!==(t=c[15].maxSelect===1?"id":"ids")&&oe(l,t)},d(c){c&&(d(e),d(l),d(u))}}}function Et(a){let e,t,l,u,c,f,b,m,$;return{c(){e=_("File object."),t=s("br"),l=_(`
Set to empty value (`),u=s("code"),u.textContent="null",c=_(", "),f=s("code"),f.textContent='""',b=_(" or "),m=s("code"),m.textContent="[]",$=_(`) to delete
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more